From bcefe3b4c47d7d7fc4dde596eb003e8e8b9eb8cf Mon Sep 17 00:00:00 2001 From: KatKatKateryna <89912278+KatKatKateryna@users.noreply.github.com> Date: Tue, 1 Apr 2025 11:36:04 +0100 Subject: [PATCH] feat (autocad): region and hatches conversions (#681) * clean send (except closed circular arcs) * split functions * include inner loops for complex regions * reorient only polycurves with arcs * regions to host * merge conflict * substract regions on receive * optimize * add checks * fix icurve, receive hatches * hatch receive * reduce dataObject conversions * finally hatches are recorded in a database * regrouped * simplify * rename * send hatch (only the first) * remove hatches for now * comment * fixed icurve converter and reference in DataObject converter * reformat * hatch to speckle * hatch to host * hatch receive works * set solid pattern on receive * send properly and throw if complex hacth * calculate mesh area from region * prevent Autocad crash by catching exceptions in the middle of transaction * sending both polylines and curves for hatches * boolean operation for sending hatches * turned 2d Hatch curves into 3d - now Brep creation doesn't fail! * circles handled * construct both polyline and vertices in parallel * handling splines on receive * comments * don't reverse proper segments * basic comment fixes * open block table for write more concise * use top-level transaction --------- Co-authored-by: Claire Kuang --- ...Speckle.Converters.AutocadShared.projitems | 7 +- .../ToHost/Geometry/DataObjectConverter.cs | 57 ++------ .../ToHost/Geometry/RegionToHostConverter.cs | 33 +++++ .../ToHost/Raw/ICurveToHostRawConverter.cs | 56 ++++++++ .../Raw/RegionHatchToHostRawConverter.cs | 112 ++++++++++++++++ .../ToHost/Raw/RegionToHostRawConverter.cs | 89 +++++++++++++ .../Geometry/HatchToSpeckleConverter.cs | 119 +++++++++++++++++ .../Geometry/RegionToSpeckleConverter.cs | 124 +++++++++++++++++- 8 files changed, 549 insertions(+), 48 deletions(-) create mode 100644 Converters/Autocad/Speckle.Converters.AutocadShared/ToHost/Geometry/RegionToHostConverter.cs create mode 100644 Converters/Autocad/Speckle.Converters.AutocadShared/ToHost/Raw/ICurveToHostRawConverter.cs create mode 100644 Converters/Autocad/Speckle.Converters.AutocadShared/ToHost/Raw/RegionHatchToHostRawConverter.cs create mode 100644 Converters/Autocad/Speckle.Converters.AutocadShared/ToHost/Raw/RegionToHostRawConverter.cs create mode 100644 Converters/Autocad/Speckle.Converters.AutocadShared/ToSpeckle/Geometry/HatchToSpeckleConverter.cs diff --git a/Converters/Autocad/Speckle.Converters.AutocadShared/Speckle.Converters.AutocadShared.projitems b/Converters/Autocad/Speckle.Converters.AutocadShared/Speckle.Converters.AutocadShared.projitems index 452b5b533..f4722bef0 100644 --- a/Converters/Autocad/Speckle.Converters.AutocadShared/Speckle.Converters.AutocadShared.projitems +++ b/Converters/Autocad/Speckle.Converters.AutocadShared/Speckle.Converters.AutocadShared.projitems @@ -32,6 +32,10 @@ + + + + @@ -44,6 +48,7 @@ + @@ -77,4 +82,4 @@ - \ No newline at end of file + diff --git a/Converters/Autocad/Speckle.Converters.AutocadShared/ToHost/Geometry/DataObjectConverter.cs b/Converters/Autocad/Speckle.Converters.AutocadShared/ToHost/Geometry/DataObjectConverter.cs index 4181d0b38..133638a5c 100644 --- a/Converters/Autocad/Speckle.Converters.AutocadShared/ToHost/Geometry/DataObjectConverter.cs +++ b/Converters/Autocad/Speckle.Converters.AutocadShared/ToHost/Geometry/DataObjectConverter.cs @@ -1,5 +1,6 @@ using Speckle.Converters.Common; using Speckle.Converters.Common.Objects; +using Speckle.Objects; using Speckle.Objects.Data; using Speckle.Sdk.Common.Exceptions; using Speckle.Sdk.Models; @@ -9,46 +10,31 @@ namespace Speckle.Converters.AutocadShared.ToHost.Geometry; [NameAndRankValue(typeof(DataObject), NameAndRankValueAttribute.SPECKLE_DEFAULT_RANK)] public class DataObjectConverter : IToHostTopLevelConverter, ITypedConverter> { - private readonly ITypedConverter _arcConverter; + private readonly ITypedConverter> _curveConverter; private readonly ITypedConverter> _brepXConverter; - private readonly ITypedConverter _circleConverter; - private readonly ITypedConverter _curveConverter; - private readonly ITypedConverter _ellipseConverter; private readonly ITypedConverter> _extrusionXConverter; - private readonly ITypedConverter _lineConverter; private readonly ITypedConverter _meshConverter; private readonly ITypedConverter _pointConverter; - private readonly ITypedConverter> _polycurveConverter; - private readonly ITypedConverter _polylineConverter; private readonly ITypedConverter> _subDXConverter; + private readonly ITypedConverter _regionConverter; public DataObjectConverter( - ITypedConverter arcConverter, + ITypedConverter> curveConverter, ITypedConverter> brepXConverter, - ITypedConverter circleConverter, - ITypedConverter curveConverter, - ITypedConverter ellipseConverter, ITypedConverter> extrusionXConverter, - ITypedConverter lineConverter, ITypedConverter meshConverter, ITypedConverter pointConverter, - ITypedConverter> polycurveConverter, - ITypedConverter polylineConverter, - ITypedConverter> subDXConverter + ITypedConverter> subDXConverter, + ITypedConverter regionConverter ) { - _arcConverter = arcConverter; - _brepXConverter = brepXConverter; - _circleConverter = circleConverter; _curveConverter = curveConverter; - _ellipseConverter = ellipseConverter; + _brepXConverter = brepXConverter; _extrusionXConverter = extrusionXConverter; - _lineConverter = lineConverter; _meshConverter = meshConverter; _pointConverter = pointConverter; - _polycurveConverter = polycurveConverter; - _polylineConverter = polylineConverter; _subDXConverter = subDXConverter; + _regionConverter = regionConverter; } public object Convert(Base target) => Convert((DataObject)target); @@ -67,54 +53,39 @@ public class DataObjectConverter : IToHostTopLevelConverter, ITypedConverter +{ + private readonly ITypedConverter _regionConverter; + private readonly ITypedConverter _hatchConverter; + + public RegionToHostConverter( + ITypedConverter regionConverter, + ITypedConverter hatchConverter + ) + { + _regionConverter = regionConverter; + _hatchConverter = hatchConverter; + } + + public object Convert(Base target) => Convert((SOG.Region)target); + + public ADB.Entity Convert(SOG.Region target) + { + // Generalizing return type as Entity, because it can be a simple Region, or a Hatch + if (target.hasHatchPattern) + { + return _hatchConverter.Convert(target); + } + return _regionConverter.Convert(target); + } +} diff --git a/Converters/Autocad/Speckle.Converters.AutocadShared/ToHost/Raw/ICurveToHostRawConverter.cs b/Converters/Autocad/Speckle.Converters.AutocadShared/ToHost/Raw/ICurveToHostRawConverter.cs new file mode 100644 index 000000000..48fec94d9 --- /dev/null +++ b/Converters/Autocad/Speckle.Converters.AutocadShared/ToHost/Raw/ICurveToHostRawConverter.cs @@ -0,0 +1,56 @@ +using Speckle.Converters.Common.Objects; +using Speckle.Objects; +using Speckle.Sdk.Common.Exceptions; +using Speckle.Sdk.Models; + +namespace Speckle.Converters.AutocadShared.ToHost.Raw; + +public class ICurveToHostRawConverter : ITypedConverter> +{ + private readonly ITypedConverter _lineConverter; + private readonly ITypedConverter _arcConverter; + private readonly ITypedConverter _ellipseConverter; + private readonly ITypedConverter _circleConverter; + private readonly ITypedConverter _polylineConverter; + private readonly ITypedConverter> _polycurveConverter; + private readonly ITypedConverter _curveConverter; + + public ICurveToHostRawConverter( + ITypedConverter lineConverter, + ITypedConverter arcConverter, + ITypedConverter ellipseConverter, + ITypedConverter circleConverter, + ITypedConverter polylineConverter, + ITypedConverter> polycurveConverter, + ITypedConverter curveConverter + ) + { + _lineConverter = lineConverter; + _arcConverter = arcConverter; + _ellipseConverter = ellipseConverter; + _circleConverter = circleConverter; + _polylineConverter = polylineConverter; + _polycurveConverter = polycurveConverter; + _curveConverter = curveConverter; + } + + /// + /// Converts a given ICurve object to a list of ADB.Curve. + /// + /// The ICurve object to convert. + /// The converted list of ADB.Curve. + /// Thrown when the conversion is not supported for the given type of curve. + /// ⚠️ This conversion does NOT perform scaling. + public List<(ADB.Entity, Base)> Convert(ICurve target) => + target switch + { + SOG.Line line => new() { (_lineConverter.Convert(line), line) }, + SOG.Arc arc => new() { (_arcConverter.Convert(arc), arc) }, + SOG.Circle circle => new() { (_circleConverter.Convert(circle), circle) }, + SOG.Ellipse ellipse => new() { (_ellipseConverter.Convert(ellipse), ellipse) }, + SOG.Polyline polyline => new() { (_polylineConverter.Convert(polyline), polyline) }, + SOG.Curve curve => new() { (_curveConverter.Convert(curve), curve) }, + SOG.Polycurve polycurve => _polycurveConverter.Convert(polycurve), + _ => throw new ValidationException($"Unable to convert curves of type {target.GetType().Name}") + }; +} diff --git a/Converters/Autocad/Speckle.Converters.AutocadShared/ToHost/Raw/RegionHatchToHostRawConverter.cs b/Converters/Autocad/Speckle.Converters.AutocadShared/ToHost/Raw/RegionHatchToHostRawConverter.cs new file mode 100644 index 000000000..eb1afbae6 --- /dev/null +++ b/Converters/Autocad/Speckle.Converters.AutocadShared/ToHost/Raw/RegionHatchToHostRawConverter.cs @@ -0,0 +1,112 @@ +using Speckle.Converters.Common; +using Speckle.Converters.Common.Objects; +using Speckle.Objects; +using Speckle.Sdk.Common.Exceptions; +using Speckle.Sdk.Models; + +namespace Speckle.Converters.Autocad.ToHost.Raw; + +public class RegionHatchToHostRawConverter : ITypedConverter +{ + private readonly ITypedConverter> _curveConverter; + private readonly IConverterSettingsStore _settingsStore; + + public RegionHatchToHostRawConverter( + ITypedConverter> curveConverter, + IConverterSettingsStore settingsStore + ) + { + _curveConverter = curveConverter; + _settingsStore = settingsStore; + } + + public ADB.Hatch Convert(SOG.Region target) + { + // Access a top-level transaction + ADB.Transaction tr = _settingsStore.Current.Document.TransactionManager.TopTransaction; + var btr = (ADB.BlockTableRecord) + tr.GetObject(_settingsStore.Current.Document.Database.CurrentSpaceId, ADB.OpenMode.ForWrite); + + // initialize Hatch, append to blockTableRecord + ADB.Hatch acHatch = new(); + btr.AppendEntity(acHatch); + tr.AddNewlyCreatedDBObject(acHatch, true); + + // Set essential properties of the hatch object + acHatch.SetDatabaseDefaults(); + acHatch.SetHatchPattern(ADB.HatchPatternType.PreDefined, "SOLID"); + + // Associative property must be set after the hatch object is + // appended to the block table record and before AppendLoop + acHatch.Associative = true; + + // convert and assign boundary loop + ConvertAndAssignHatchLoop(btr, tr, acHatch, target.boundary, ADB.HatchLoopTypes.External); + foreach (var loop in target.innerLoops) + { + ConvertAndAssignHatchLoop(btr, tr, acHatch, loop, ADB.HatchLoopTypes.Outermost); + } + + return acHatch; + } + + private void ConvertAndAssignHatchLoop( + ADB.BlockTableRecord acBlkTblRec, + ADB.Transaction acTrans, + ADB.Hatch hatch, + ICurve curve, + ADB.HatchLoopTypes loopType + ) + { + // convert loop, add to ObjectIdCollection + var convertedCurve = _curveConverter.Convert(curve); + CheckForNonPlanarLoops(convertedCurve); + var dbCurve = (ADB.Curve)convertedCurve[0].Item1; + + // If Spline, turn into segmented polyline - this is how AutoCAD imports Hatches with Curve boundaries from Rhino + if (dbCurve is ADB.Spline spline) + { + if (spline.NurbsData.Degree == 1) + { + // for simple polylines ".ToPolylineWithPrecision" distorts the shape, so just applying a list of vertices + dbCurve = new ADB.Polyline3d(ADB.Poly3dType.SimplePoly, spline.NurbsData.GetControlPoints(), true); + } + else + { + dbCurve = spline.ToPolylineWithPrecision(10, false, false); + } + } + using ADB.ObjectIdCollection tempDBObjColl = CreateTempObjectIdCollection(acBlkTblRec, acTrans, dbCurve); + + // append loop: possible Autodesk.AutoCAD.Runtime.Exception: eInvalidInput + hatch.AppendLoop(loopType, tempDBObjColl); + hatch.EvaluateHatch(true); + dbCurve.Erase(); + } + + private ADB.ObjectIdCollection CreateTempObjectIdCollection( + ADB.BlockTableRecord acBlkTblRec, + ADB.Transaction acTrans, + ADB.Entity loopEntity + ) + { + // Add the new curve object to the block table record and the transaction + acBlkTblRec.AppendEntity(loopEntity); + acTrans.AddNewlyCreatedDBObject(loopEntity, true); + + // Adds the entity to an object id array + ADB.ObjectIdCollection tempDBObjColl = new(); + tempDBObjColl.Add(loopEntity.ObjectId); + + return tempDBObjColl; + } + + private void CheckForNonPlanarLoops(List<(ADB.Entity, Base)> convertedResult) + { + if (convertedResult.Count != 1) + { + // this will only be the case if it was a non-planar Polycurve: throw error + throw new ConversionException($"Non-planar Polycurve cannot be used as a Region loop: {convertedResult}"); + } + } +} diff --git a/Converters/Autocad/Speckle.Converters.AutocadShared/ToHost/Raw/RegionToHostRawConverter.cs b/Converters/Autocad/Speckle.Converters.AutocadShared/ToHost/Raw/RegionToHostRawConverter.cs new file mode 100644 index 000000000..b63889aeb --- /dev/null +++ b/Converters/Autocad/Speckle.Converters.AutocadShared/ToHost/Raw/RegionToHostRawConverter.cs @@ -0,0 +1,89 @@ +using Speckle.Converters.Common.Objects; +using Speckle.Objects; +using Speckle.Sdk.Common.Exceptions; +using Speckle.Sdk.Models; + +namespace Speckle.Converters.Autocad.ToHost.Raw; + +public class RegionToHostRawConverter : ITypedConverter +{ + private readonly ITypedConverter> _curveConverter; + + public RegionToHostRawConverter(ITypedConverter> curveConverter) + { + _curveConverter = curveConverter; + } + + public ADB.Region Convert(SOG.Region target) + { + // Notes from docs: The curveSegments must contain only Line, Arc, Ellipse, Circle, Spline, Polyline3d, or Polyline2d objects. + // The objects in curveSegments must be opened for read and not for write. If the objects are opened, calling this function will crash AutoCAD. + + // Converted boundary + List<(ADB.Entity, Base)> convertedBoundary = _curveConverter.Convert(target.boundary); + ADB.Curve nativeBoundary = ValidateCurve(convertedBoundary); + + // Converted loops + var nativeLoops = new List(); + foreach (var loop in target.innerLoops) + { + List<(ADB.Entity, Base)> convertedLoop = _curveConverter.Convert(loop); + nativeLoops.Add(ValidateCurve(convertedLoop)); + } + + // Add boundary to the ADB.DBObjectCollection + // Calculate the outer region, method should return an array with 1 region + // https://help.autodesk.com/view/OARX/2025/ENU/?guid=GUID-684E602E-3555-4370-BCDC-1CE594676C43 + ADB.DBObjectCollection boundaryDBObjColl = new(); + boundaryDBObjColl.Add(nativeBoundary); + using (ADB.DBObjectCollection outerRegionColl = ADB.Region.CreateFromCurves(boundaryDBObjColl)) + { + if (outerRegionColl.Count != 1) + { + throw new ConversionException( + $"Region conversion failed for {target}: unexpected number of shapes generated ({outerRegionColl.Count}). Make sure that input loops are planar, closed, non self-intersecting curves." + ); + } + if (outerRegionColl[0] is ADB.Region adbRegion) + { + // Create and subtract the inner loops' regions, iterate through each + foreach (var nativeLoop in nativeLoops) + { + // Same as above: Add loop segments to the ADB.DBObjectCollection + // Calculate the inner region, method should return an array with 1 region + ADB.DBObjectCollection loopDBObjColl = new(); + loopDBObjColl.Add(nativeLoop); + using (ADB.DBObjectCollection innerRegionColl = ADB.Region.CreateFromCurves(loopDBObjColl)) + { + if (innerRegionColl.Count != 1) + { + throw new ConversionException( + $"Region conversion failed for {target}: unexpected number of shapes generated ({innerRegionColl.Count}). Make sure that input loops are planar, closed, non self-intersecting curves." + ); + } + if (innerRegionColl[0] is ADB.Region adbInnerRegion) + { + // substract region from Boundary region + adbRegion.BooleanOperation(ADB.BooleanOperationType.BoolSubtract, adbInnerRegion); + adbInnerRegion.Dispose(); + } + } + } + + return adbRegion; + } + } + + throw new ConversionException($"Region conversion failed: {target}"); + } + + private ADB.Curve ValidateCurve(List<(ADB.Entity, Base)> convertedResult) + { + if (convertedResult.Count != 1) + { + // this will only be the case if it was a non-planar Polycurve: throw error + throw new ConversionException($"Non-planar Polycurve cannot be used as a Region loop: {convertedResult}"); + } + return (ADB.Curve)convertedResult[0].Item1; + } +} diff --git a/Converters/Autocad/Speckle.Converters.AutocadShared/ToSpeckle/Geometry/HatchToSpeckleConverter.cs b/Converters/Autocad/Speckle.Converters.AutocadShared/ToSpeckle/Geometry/HatchToSpeckleConverter.cs new file mode 100644 index 000000000..3d724b445 --- /dev/null +++ b/Converters/Autocad/Speckle.Converters.AutocadShared/ToSpeckle/Geometry/HatchToSpeckleConverter.cs @@ -0,0 +1,119 @@ +using Speckle.Converters.Common; +using Speckle.Converters.Common.Objects; +using Speckle.Sdk.Common.Exceptions; +using Speckle.Sdk.Models; + +namespace Speckle.Converters.Autocad.Geometry; + +[NameAndRankValue(typeof(ADB.Hatch), NameAndRankValueAttribute.SPECKLE_DEFAULT_RANK)] +public class HatchToSpeckleConverter : IToSpeckleTopLevelConverter, ITypedConverter +{ + private readonly ITypedConverter _regionConverter; + + public HatchToSpeckleConverter(ITypedConverter regionConverter) + { + _regionConverter = regionConverter; + } + + public Base Convert(object target) => Convert((ADB.Hatch)target); + + public SOG.Region Convert(ADB.Hatch target) + { + ADB.Region? regionToConvert = null; + + for (int i = 0; i < target.NumberOfLoops; i++) + { + // Create 3d polyline from the HatchLoop + ADB.HatchLoop loop = target.GetLoopAt(i); + ADB.Curve polyline = PolylineFromLoop(loop); + ADB.DBObjectCollection objCollection = new(); + objCollection.Add(polyline); + + // Convert polyline into an individual Region + using (ADB.DBObjectCollection regionCollection = ADB.Region.CreateFromCurves(objCollection)) + { + if (regionCollection.Count != 1) + { + throw new ConversionException( + $"Hatch conversion failed {target}: unexpected number of regions generated from 1 hatch loop" + ); + } + ADB.Region loopRegion = (ADB.Region)regionCollection[0]; + + // Assign first loop as the main Region, other Regions will be subtracted from it + if (i == 0) + { + regionToConvert = loopRegion; + } + else + { + if (regionToConvert == null) + { + throw new ConversionException($"Hatch conversion failed: {target}"); + } + // subtract region from Boundary region + double areaBefore = regionToConvert.Area; + regionToConvert.BooleanOperation(ADB.BooleanOperationType.BoolSubtract, loopRegion); + + // check if the region did not change after subtraction: means the loop was a separate hatch part + if (Math.Abs(areaBefore - regionToConvert.Area) < 0.00001) + { + throw new ConversionException($"Composite hatches are not supported: {target}"); + } + } + } + } + + if (regionToConvert == null) + { + throw new ConversionException($"Hatch conversion failed: {target}"); + } + + // convert and store Regions + SOG.Region convertedRegion = _regionConverter.Convert(regionToConvert); + convertedRegion.hasHatchPattern = true; + + return convertedRegion; + } + + private ADB.Curve PolylineFromLoop(ADB.HatchLoop loop) + { + if (loop.IsPolyline) + { + // disposable object, wrapping into "using" + using (AG.Point3dCollection vertices = new()) + { + // collect vertices and construct a polyline simultaneously, it will be clear what to use after iterating + ADB.Polyline polyline = new() { Closed = true }; + + int count = 0; + foreach (ADB.BulgeVertex bVertex in loop.Polyline) + { + // don't add the end point that's the same as the start point + AG.Point3d newPt = new(bVertex.Vertex.X, bVertex.Vertex.Y, 0); + if (count == 0 || vertices[0].DistanceTo(newPt) > 0.00001) + { + vertices.Add(newPt); + polyline.AddVertexAt(count, bVertex.Vertex, bVertex.Bulge, 0, 0); + count++; + } + } + + // if only 2 points, that's a circle + if (vertices.Count == 2) + { + AG.Point3d centerPt = + new( + vertices[0].X + (vertices[1].X - vertices[0].X) / 2, + vertices[0].Y + (vertices[1].Y - vertices[0].Y) / 2, + 0 + ); + return new ADB.Circle(centerPt, AG.Vector3d.ZAxis, vertices[0].DistanceTo(vertices[1]) / 2); + } + return polyline; + } + } + + throw new ConversionException("Hatch loop conversion failed."); + } +} diff --git a/Converters/Autocad/Speckle.Converters.AutocadShared/ToSpeckle/Geometry/RegionToSpeckleConverter.cs b/Converters/Autocad/Speckle.Converters.AutocadShared/ToSpeckle/Geometry/RegionToSpeckleConverter.cs index 72ba00eef..25ac8acd8 100644 --- a/Converters/Autocad/Speckle.Converters.AutocadShared/ToSpeckle/Geometry/RegionToSpeckleConverter.cs +++ b/Converters/Autocad/Speckle.Converters.AutocadShared/ToSpeckle/Geometry/RegionToSpeckleConverter.cs @@ -1,24 +1,40 @@ using Speckle.Converters.Common; using Speckle.Converters.Common.Objects; +using Speckle.Objects; using Speckle.Sdk.Common.Exceptions; using Speckle.Sdk.Models; namespace Speckle.Converters.Autocad.Geometry; [NameAndRankValue(typeof(ADB.Region), NameAndRankValueAttribute.SPECKLE_DEFAULT_RANK)] -public class RegionToSpeckleConverter : IToSpeckleTopLevelConverter, ITypedConverter +public class RegionToSpeckleConverter : IToSpeckleTopLevelConverter, ITypedConverter { private readonly ITypedConverter _brepConverter; + private readonly ITypedConverter _lineConverter; + private readonly ITypedConverter _arcConverter; + private readonly ITypedConverter _circleConverter; + private readonly IConverterSettingsStore _settingsStore; - public RegionToSpeckleConverter(ITypedConverter brepConverter) + public RegionToSpeckleConverter( + ITypedConverter brepConverter, + ITypedConverter lineConverter, + ITypedConverter arcConverter, + ITypedConverter circleConverter, + IConverterSettingsStore settingsStore + ) { _brepConverter = brepConverter; + _lineConverter = lineConverter; + _arcConverter = arcConverter; + _circleConverter = circleConverter; + _settingsStore = settingsStore; } public Base Convert(object target) => Convert((ADB.Region)target); - public SOG.Mesh Convert(ADB.Region target) + public SOG.Region Convert(ADB.Region target) { + // generate Mesh for displayValue using ABR.Brep brep = new(target); if (brep.IsNull) { @@ -28,6 +44,106 @@ public class RegionToSpeckleConverter : IToSpeckleTopLevelConverter, ITypedConve SOG.Mesh mesh = _brepConverter.Convert(brep); mesh.area = target.Area; - return mesh; + // get all brep loops: can consist of LineSegment3d or CircularArc3d edges + var brepLoops = brep + .Complexes.SelectMany(complex => complex.Shells) + .SelectMany(shell => shell.Faces) + .SelectMany(face => face.Loops); + + // Get and convert boundary and inner loops + var boundary = GetConvertedLoops(brepLoops, true)[0]; + var innerLoops = GetConvertedLoops(brepLoops, false); + + return new SOG.Region() + { + boundary = boundary, + innerLoops = innerLoops, + hasHatchPattern = false, + displayValue = [mesh], + units = _settingsStore.Current.SpeckleUnits + }; + } + + private List GetConvertedLoops(IEnumerable brepLoops, bool getOuterLoop) + { + var loops = new List(); + foreach (var loop in brepLoops) + { + bool outer = loop.LoopType == ABR.LoopType.LoopExterior; + + // continue only if the loop type is as requester (outer or inner) + if ((outer && getOuterLoop) || (!outer && !getOuterLoop)) + { + // create segment collection for the current loop + var segments = new List(); + foreach (var edge in loop.Edges) + { + var curve = edge.Curve; + if (curve is AG.ExternalCurve3d xCurve && xCurve.IsNativeCurve) + { + segments.Add(xCurve.NativeCurve); + } + else + { + throw new ConversionException("Unsupported curve type for Region conversion"); + } + } + // reverse segment collection with arcs in case end-start points of subsequent segments don't match + if ( + segments.Any(x => x is AG.CircularArc3d) + && segments.Count > 1 + && Math.Abs(segments[0].EndPoint.DistanceTo(segments[1].StartPoint)) > 0.00001 + ) + { + segments.Reverse(); + } + + // convert segments to Speckle Polycurve or Circle + var convertedLoop = ConvertSegmentsToICurve(segments); + loops.Add(convertedLoop); + } + } + + return loops; + } + + private ICurve ConvertSegmentsToICurve(List segments) + { + ICurve convertedLoop; + + // Handle edge case: if the segment is a closed Arc, then use Circle conversion to create a valid shape. + // Also, closed arcs cause errors when receiving in other host apps, like Rhino. + if (segments.Count == 1 && segments[0] is AG.CircularArc3d arc && arc.StartAngle + arc.EndAngle == 0) + { + convertedLoop = _circleConverter.Convert( + new ADB.Circle(arc.GetPlane().PointOnPlane, arc.GetPlane().Normal, arc.Radius) + ); + } + // otherwise, just construct a Polycurve from subsequent segments + else + { + // Maybe we need to convert to AutoCAD Polycurve + convertedLoop = new SOG.Polycurve() + { + segments = segments.Select(x => ConvertSegment(x)).ToList(), + closed = true, + units = _settingsStore.Current.SpeckleUnits + }; + } + + return convertedLoop; + } + + private ICurve ConvertSegment(AG.Curve3d curve) + { + switch (curve) + { + case AG.LineSegment3d line: + return _lineConverter.Convert(line); + case AG.CircularArc3d arc: + return _arcConverter.Convert(arc); + } + + throw new ConversionException($"Unsupported curve type for Region conversion: {curve}"); } }