Get Region Outer Boundary

Get Region Outer Boundary

Ysuuny
Contributor Contributor
2,837 Views
19 Replies
Message 1 of 20

Get Region Outer Boundary

Ysuuny
Contributor
Contributor

Ysuuny_0-1732757075376.png

( I'm reposting it because I posted it wrong somewhere else )

 

Hello!! 

I'm developing it with civil3d,autocad c# and it's not solved, so I'm leaving a post.

I only want to bring the outer line (polyline) from the region with a hole in the middle

- I explode the region, but it's hard to join because it's remain to the middle line
- I've also checked if it's in the area based on the point criteria, but it's not working well

I also referred to this library(GeometryExtensionsR19, brep) and worked hard...

 

Please help me,,

(Code Flow)

1. select Region

2. expolde region

3. get first line / and join ==> i want to outer line, but get  inner line...

0 Likes
2,838 Views
19 Replies
Replies (19)
Message 2 of 20

kerry_w_brown
Advisor
Advisor

It may be benificial if you provide a sample mini drawing

 

>>> I also referred to this library(GeometryExtensionsR19, brep)

Is the the Library from Gilles ?
so I assume 2013,2014, or is it acad 2019 ?

Regards,

 


// Called Kerry or kdub in my other life.

Everything will work just as you expect it to, unless your expectations are incorrect. ~ kdub
Sometimes the question is more important than the answer. ~ kdub

NZST UTC+12 : class keyThumper<T> : Lazy<T>;      another  Swamper
0 Likes
Message 3 of 20

Ysuuny
Contributor
Contributor

I use Autocad 2023! 

And I'll make and upload the example soon. Thank you

0 Likes
Message 4 of 20

norman.yuan
Mentor
Mentor

After exploding, you should get a collection of curves (usually lines and arcs). You can simply take out any one from the collection, and then loop through the rest and test if the other curve connects to it (i.e. their Start/EndPoints meet). If connected, take out the connected curve out of the collection and join them together. Loop through the collection again (already 2 curves less) to find next connected one, remove from the collection and join them...keep doing the same until no connected curve found. Now you have a closed curve. If there are still curves left in the collection, you repeat the operation... Eventually, you end up with one or more closed curves, depending on if the region has islands inside or not. Finally, if you get more than one closed curve, the one with largest area is the outer boundary curve.

 

Norman Yuan

Drive CAD With Code

EESignature

0 Likes
Message 5 of 20

ActivistInvestor
Mentor
Mentor

See if this helps:

 

 

using System;
using System.Collections.Generic;
using System.Linq;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.BoundaryRepresentation;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;

public static class RegionLoopExtractionExample
{ 
   [CommandMethod("GetTheOuterLoop")]
   public static void GetTheOuterLoop()
   {
      Document doc = Application.DocumentManager.MdiActiveDocument;
      Editor ed = doc.Editor;
      Database db = doc.Database;
      PromptEntityOptions peo = new PromptEntityOptions("\nSelect a region: ");
      peo.SetRejectMessage("\nInvalid select, requires a region");
      peo.AddAllowedClass(typeof(Region), true);
      var per = ed.GetEntity(peo);
      if(per.Status != PromptStatus.OK)
         return;
      try
      {
         using(var tr = doc.TransactionManager.StartTransaction())
         {
            var btr = (BlockTableRecord)tr.GetObject(db.CurrentSpaceId,
               OpenMode.ForWrite);
            Region region = (Region)tr.GetObject(per.ObjectId, OpenMode.ForRead);
            var geCurves = region.GetGeCurves(true);
            if(!geCurves.Any())
               return;
            var curves = geCurves.Select(c => Curve.CreateFromGeCurve(c));
            try
            {
               // If all elements are Lines or Arcs, then create
               // a Polyline. Otherwise, create a Spline:

               bool pline = curves.All(c => c is Line || c is Arc);
               if(!pline)   // must create a Spline
               {
                  var first = curves.First();
                  var spline = first as Spline ?? first.Spline;
                  var rest = curves.Skip(1);
                  if(rest.Any())
                     spline.JoinEntities(rest.ToArray());
                  btr.AppendEntity(spline);
                  spline.ColorIndex = 1;
                  tr.AddNewlyCreatedDBObject(spline, true);
               }
               else  // create a polyline
               {
                  var cc3d = new CompositeCurve3d(geCurves.ToArray());
                  var polyline = Curve.CreateFromGeCurve(cc3d);
                  btr.AppendEntity(polyline);
                  tr.AddNewlyCreatedDBObject(polyline, true);
                  polyline.ColorIndex = 1;
               }
               tr.Commit();
            }
            finally
            {
               curves.DisposeItems();
            }
         }
      }
      catch(System.Exception ex)
      {
         ed.WriteMessage(ex.ToString());
      }
   }

   public static void DisposeItems<T>(this IEnumerable<T> items) where T: IDisposable
   {
      foreach(var item in items)
         item.Dispose();
   }

   /// <summary>
   /// Extension method that extracts Curve3d geometry 
   /// from a region. The argument specifies if only the 
   /// outer loop is to be extracted, or all loops.
   /// </summary>

   public static IEnumerable<Curve3d> GetGeCurves(this Region region, 
      bool outerLoopOnly = true)
   {
      using(Brep brep = new Brep(region))
      {
         foreach(var loop in brep.Complexes
            .SelectMany(complex => complex.Shells)
            .SelectMany(shell => shell.Faces)
            .SelectMany(face => face.Loops))
         {
            bool outer = loop.LoopType == LoopType.LoopExterior;
            if(!outerLoopOnly || outer)
            {
               foreach(var edge in loop.Edges)
               {
                  var curve = edge.Curve;
                  if(curve is ExternalCurve3d xCurve)
                  {
                     if(xCurve.IsNativeCurve)
                     {
                        yield return xCurve.NativeCurve;
                     }
                     else
                        throw new NotSupportedException();
                  }
               }
            }
            if(outerLoopOnly && outer)
               yield break;
         }
      }
   }
}

 

0 Likes
Message 6 of 20

_gile
Consultant
Consultant

Hi,

Here's a method inspired by the Region.GetCurves one from GeometryExtension.

As is, it throws an exception if there's more then one outer loop (i.e., not contiguous unioned regions).

It returns a sequence of curves which may contains a single curve (Circle, closed Ellipse or Spline, Polyline).

public static IEnumerable<Curve> GetExteriorLoopCurves(Region region, Tolerance tolerance)
{
    using (var brep = new Brep(region))
    {
        // Check if there's only one outer loop
        if (1 < brep.Complexes.Count())
            throw new ArgumentException("Too many outer loops.");

        // Get the outer loop Curve3d squence
        var outerLoop = brep.Faces.First().Loops.First(l => l.LoopType == LoopType.LoopExterior);
        var curves3d = outerLoop.Edges.Select(edge => ((ExternalCurve3d)edge.Curve).NativeCurve);

        // Convert the Curve3d squence in one or more Curve objects
        if (curves3d.Count() == 1)
        {
            yield return Curve.CreateFromGeCurve(curves3d.First());
        }
        else
        {
            // Create a Polyline if all Curve3d are lines or arcs
            if (curves3d.All(curve3d => curve3d is CircularArc3d || curve3d is LineSegment3d))
            {
                // Order all edges by coincident start / end points
                var list = curves3d.ToList();
                int count = list.Count;
                var array = new Curve3d[count];
                int i = 0;
                array[0] = list[0];
                list.RemoveAt(0);
                int index;
                while (i < count - 1)
                {
                    var pt = array[i++].EndPoint;
                    if ((index = list.FindIndex(c => c.StartPoint.IsEqualTo(pt, tolerance))) != -1)
                        array[i] = list[index];
                    else if ((index = list.FindIndex(c => c.EndPoint.IsEqualTo(pt, tolerance))) != -1)
                        array[i] = list[index].GetReverseParameterCurve();
                    else
                        throw new ArgumentException("Not contiguous curves.");
                    list.RemoveAt(index);
                }
                var pline = (Polyline)Curve.CreateFromGeCurve(new CompositeCurve3d(array));
                pline.Closed = true;
                yield return pline;
            }
            else
            {
                foreach (Curve3d curve3d in curves3d)
                {
                    yield return Curve.CreateFromGeCurve(curve3d);
                }
            }
        }
    }
}

 

Testing command:

[CommandMethod("GETOUTERLOOP")]
public static void GetRegionOuterLoop()
{
    var doc = Application.DocumentManager.MdiActiveDocument;
    var db = doc.Database;
    var ed = doc.Editor;

    var peo = new PromptEntityOptions("\nSelect Region: ");
    peo.SetRejectMessage("\nSelected object is not a Region.");
    peo.AddAllowedClass(typeof(Region), true);
    var per = ed.GetEntity(peo);
    if (per.Status != PromptStatus.OK) 
        return;

    try
    {
        using (var tr = db.TransactionManager.StartTransaction())
        {
            var region = (Region)tr.GetObject(per.ObjectId, OpenMode.ForRead);
            var currentSpace = (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
            foreach (Curve curve in GetExteriorLoopCurves(region, new Tolerance(1e-12, 1e-9)))
            {
                curve.SetDatabaseDefaults();
                currentSpace.AppendEntity(curve);
                tr.AddNewlyCreatedDBObject(curve, true);
            }
            tr.Commit();
        }
    }
    catch (System.Exception ex)
    {
        ed.WriteMessage($"\n{ex.Message}");
    }
}


Gilles Chanteau
Programmation AutoCAD LISP/.NET
GileCAD
GitHub

0 Likes
Message 7 of 20

_gile
Consultant
Consultant

@ActivistInvestor

The Polyline returned by passing the Curve3d sequence as is to the CompositeCurve3d constructor may be incorrect because the Curve3d sequence have to be ordered by contiguous points and some segments may have to be reversed.

_gile_0-1732779513762.png

 



Gilles Chanteau
Programmation AutoCAD LISP/.NET
GileCAD
GitHub

0 Likes
Message 8 of 20

_gile
Consultant
Consultant

I added a Region.GetCurvesByLoop method in GeometryExtensions.

This method returns an IEnumerable of ValueTuple (LoopType, Curve[]) for each loop of the region which can be used to filter the curves by LoopType.

[CommandMethod("DRAWOUTERLOOPS")]
public static void DrawRegionOuterLoops()
{
    var doc = Application.DocumentManager.MdiActiveDocument;
    var db = doc.Database;
    var ed = doc.Editor;

    var peo = new PromptEntityOptions("\nSelect Region: ");
    peo.SetRejectMessage("\nSelected object is not a Region.");
    peo.AddAllowedClass(typeof(Region), true);
    var per = ed.GetEntity(peo);
    if (per.Status != PromptStatus.OK)
        return;

    try
    {
        using (var tr = db.TransactionManager.StartTransaction())
        {
            var region = (Region)tr.GetObject(per.ObjectId, OpenMode.ForRead);
            var currentSpace = (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
            foreach ((var loopType, var curves) in region.GetCurvesByLoop(new Tolerance(1e-12, 1e-9)))
            {
                if (loopType == LoopType.LoopExterior)
                {
                    foreach (Curve curve in curves)
                    {
                        currentSpace.AppendEntity(curve);
                        tr.AddNewlyCreatedDBObject(curve, true);
                    }
                }
                else
                {
                    foreach (Curve curve in curves)
                    {
                        curve.Dispose();
                    }
                }
            }
            tr.Commit();
        }
    }
    catch (System.Exception ex)
    {
        ed.WriteMessage($"\n{ex.Message}");
    }
}

 



Gilles Chanteau
Programmation AutoCAD LISP/.NET
GileCAD
GitHub

0 Likes
Message 9 of 20

ActivistInvestor
Mentor
Mentor

Hi @_gile, Thanks for pointing out that omission in my example. I didn't include the code I use to deal with discontiguous polyline segments because it is a bit more-complicated than your solution, and is also heavily-dependent on other library code that would have had to go along for the ride, and removing that dependence turned out to be fairly involved.

 

But since you brought it up, I took the time to rip out all of the dependent code and replace calls to other dependent code that I can't as easily share, and added the result to the refactored code below (not thoroughly-tested).

 

Below is a refactored example, that deals with discontiguous polyline segments efficiently, and also deals with multiple disjoint outer loops. Sharing the code I've written over the years in a way that allows it to be easily usable can be difficult, so I can't always put it on the table without justifying the time required to do that.

 

 

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.BoundaryRepresentation;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;

public static class RegionLoopExtractionExample
{
   const int PARALLELIZATION_THRESHOLD = 250;

   /// <summary>
   /// Refactored example that extracts all outer
   /// loops of a selected Region, and includes code 
   /// that efficiently deals with discontiguous or
   /// unordered polyline segments.
   /// </summary>
   
   [CommandMethod("GetTheOuterLoops", CommandFlags.Redraw)]
   public static void GetTheOuterLoops()
   {
      Document doc = Application.DocumentManager.MdiActiveDocument;
      Editor ed = doc.Editor;
      Database db = doc.Database;
      PromptEntityOptions peo = new PromptEntityOptions("\nSelect a region: ");
      peo.SetRejectMessage("\nInvalid selection, requires a region");
      peo.AddAllowedClass(typeof(Region), true);
      var per = ed.GetEntity(peo);
      if(per.Status != PromptStatus.OK)
         return;
      try
      {
         using(var tr = doc.TransactionManager.StartTransaction())
         {
            var btr = (BlockTableRecord)tr.GetObject(db.CurrentSpaceId,
               OpenMode.ForWrite);
            Region region = (Region)tr.GetObject(per.ObjectId, OpenMode.ForRead);
            List<ObjectId> ids = new List<ObjectId>();
            using(var brep = new Brep(region))
            {
               foreach(Curve curve in brep.Complexes.Select(GetOuterLoop))
               {
                  btr.AppendEntity(curve);
                  tr.AddNewlyCreatedDBObject(curve, true);
                  ids.Add(curve.ObjectId);
               }
            }
            tr.Commit();
            ed.WriteMessage($"\nExtracted {ids.Count} loop(s).");
            if(ids.Count > 0)
               ed.SetImpliedSelection(ids.ToArray());
         }
      }
      catch(System.Exception ex)
      {
         ed.WriteMessage(ex.ToString());
      }
   }

   /// <summary>
   /// Gets the outerloop of the given complex as a 
   /// single closed curve:
   /// </summary>

   public static Curve GetOuterLoop(this Complex complex)
   {
      if(complex is null)
         throw new ArgumentNullException(nameof(complex));

      var geCurves = complex.Shells
         .SelectMany(shell => shell.Faces)
         .SelectMany(face => face.Loops)
         .First(loop => loop.LoopType == LoopType.LoopExterior)
         .GetGeCurves()
         .ToArray();

      if(geCurves.Length == 0)
         throw new InvalidOperationException("no curves");

      if(IsPolyline(geCurves))   // create polyline
      {
         return Curve.CreateFromGeCurve(
            new CompositeCurve3d(
               geCurves.Normalize()));
      }
      else  // create a spline
      {
         var curves = Array.ConvertAll(geCurves, Curve.CreateFromGeCurve);
         Curve first = curves[0];
         var spline = first as Spline ?? first.Spline;
         if(curves.Length > 1)
         {
            var slice = curves.AsSpan(1).ToArray();
            try
            {
               spline.JoinEntities(slice);
            }
            finally
            {
               slice.DisposeItems();
            }
         }
         return spline;
      }
   }

   /// <summary>
   /// Indicates if the contents of the input sequence
   /// can be converted to a Polyline.
   /// 
   /// Requires C# 10.0 or later.
   /// </summary>
   /// <param name="curves"></param>
   /// <returns></returns>

   public static bool IsPolyline(this IEnumerable<Curve3d> curves)
   {
      if(curves is null)
         throw new ArgumentNullException(nameof(curves));
      return curves.AsParallel().All(c => c is LineSegment3d or CircularArc3d);
   }

   /// <summary>
   /// Disposes the elements in the input sequence.
   /// 
   /// (Cannot parallelize this)
   /// </summary>
   /// <typeparam name="T"></typeparam>
   /// <param name="items"></param>
   
   public static void DisposeItems<T>(this IEnumerable<T> items) where T : IDisposable
   {
      if(items != null)
      {
         foreach(var item in items)
            item.Dispose();
      }
   }

   /// <summary>
   /// Validates a curve for use in a boundary context.
   /// 
   /// Note: 
   /// 
   /// This is expensive, and should only be used on 
   /// user-selected curves. For curves obtained from
   /// a Brep (e.g., edges), this is mostly-pointless
   /// as the curves would not be part of a loop in 
   /// the first place if they fail this.
   /// </summary>
   /// <param name="curve"></param>
   /// <param name="rejectClosed"></param>
   /// <param name="rejectSelfIntersecting"></param>
   /// <exception cref="ArgumentNullException"></exception>
   /// <exception cref="ArgumentException"></exception>

   public static void AssertIsValid(this Curve3d curve,
      bool rejectClosed = true,
      bool rejectSelfIntersecting = true)
   {
      if(curve is null)
         throw new ArgumentNullException(nameof(curve));
      var tolerance = Tolerance.Global.EqualPoint;
      // disqualify degenerate curves first:
      if(curve.IsDegenerate(out var entity))
         throw new ArgumentException("degenerate curve");
      var iv = curve.GetInterval();
      // disqualify unbounded curves
      if(iv.IsUnbounded)
         throw new ArgumentException("unbounded curve");
      // disqualify zero-length curves
      if(curve.GetLength(iv.LowerBound, iv.UpperBound, tolerance) < tolerance)
         throw new ArgumentException("Zero-length curve");
      // disqualify closed curves if rejectClosed == true
      if(rejectClosed && curve.IsClosed())
         throw new ArgumentException("closed curve");
      // disqualify non-planar curves
      if(!curve.IsPlanar(out Plane plane))
         throw new ArgumentException("non-planar curve");
      // disqualify self-intersecting curves if rejectSelfIntersecting is true:
      if(rejectSelfIntersecting)
      {
         var cci = new CurveCurveIntersector3d(curve, curve, plane.Normal);
         if(cci.NumberOfIntersectionPoints > 0)
            throw new ArgumentException("self-intersecting curve");
      }
   }

   /// <summary>
   /// Ensures that the result is enumerated in order of
   /// traversal, with coincident start/endpoints. This
   /// method rearranges the order of, and reverses the 
   /// direction of input curves as needed.
   /// </summary>
   /// <param name="curves">The unordered set of curves</param>
   /// <param name="validate">True = validate each curve
   /// (should only be used for user-selected curves,
   /// but not for curves coming from a BRep)</param>
   /// <returns>The input curves in order of traversal</returns>
   /// <exception cref="ArgumentNullException"></exception>


   public static Curve3d[] Normalize(this IEnumerable<Curve3d> curves, bool validate = false)
   {
      if(curves is null)
         throw new ArgumentNullException(nameof(curves));

      var input = curves as Curve3d[] ?? curves.ToArray();
      if(input.Length < 2)
         return input;
      
      if(validate)
         input.ForEach(crv => crv.AssertIsValid());

      var visited = new bool[input.Length];
      var output = new Curve3d[input.Length];
      var reverse = new bool[input.Length];

      output[0] = input[0];
      reverse[0] = false;
      visited[0] = true;

      int idx = 1;
      var spInput = input.AsSpan();
      var spOutput = output.AsSpan();
      var spFlags = reverse.AsSpan();
      var spVisited = visited.AsSpan();

      while(idx < input.Length)
      {
         int pos = idx - 1;
         Point3d curEndPoint = GetEndPoint(spOutput[pos], spFlags[pos]);
         bool found = false;

         for(int i = 0; i < input.Length; i++)
         {
            if(spVisited[i])
               continue;

            var current = spInput[i];
            var startPoint = current.StartPoint;
            var endPoint = current.EndPoint;

            if(curEndPoint.IsEqualTo(startPoint))
            {
               spOutput[idx] = current;
               spFlags[idx] = false;
               spVisited[i] = true;
               found = true;
               break;
            }
            else if(curEndPoint.IsEqualTo(endPoint))
            {
               spOutput[idx] = current;
               spFlags[idx] = true;
               spVisited[i] = true;
               found = true;
               break;
            }
         }

         if(!found)
            throw new InvalidOperationException("Non-contiguous curves");

         idx++;
      }
      var result = new Curve3d[output.Length];
      output.ForEach((crv, i) => result[i] = ReverseIf(crv, reverse[i]));
      return result;
   }

   static Curve3d ReverseIf(Curve3d curve, bool reverse)
   {
      if(curve is null) 
         throw new ArgumentNullException(nameof(curve));
      return reverse ? curve.GetReverseParameterCurve() : curve;
   }

   static Point3d GetEndPoint(Curve3d curve, bool isReversed)
   {
      if(curve is null)
         throw new ArgumentNullException(nameof(curve));
      return isReversed ? curve.StartPoint : curve.EndPoint;
   }


   /// <summary>
   /// Extension method that obtains edge geometry 
   /// of a single BoundaryLoop.
   /// 
   /// Can be used in conjunction with with the 
   /// GetLoops() method to get the geometry of
   /// all or selected loops.
   /// 
   /// For example, one can use:
   ///  
   ///   brep.GetLoops().SelectMany(GetGeCurves);
   ///   
   /// to get all GeCurves in the Region's Brep.
   /// 
   /// Or, one can call this on any Complex in a
   /// brep to get only the loops within same.
   /// 
   /// Note: This method targets Brep complexes
   /// rather than Regions, so that it can use 
   /// deferred execution within the scope of the
   /// containing Brep.
   ///   
   /// </summary>

   public static IEnumerable<Curve3d> GetGeCurves(this BoundaryLoop loop)
   {
      if(loop is null)
         throw new ArgumentNullException(nameof(loop));
      var edges = loop.Edges.ToArray();
      var result = new Curve3d[edges.Length];
      edges.ForEach((edge, i) =>
      {
         if(edge.Curve is ExternalCurve3d crv && crv.IsNativeCurve)
            result[i] = crv.NativeCurve;
         else
            throw new NotSupportedException();
      });
      return result;
   }

   /// <summary>
   /// Conditional parallel execution based on array size:
   /// 
   /// The PARALLELIZATION_THRESHOLD constant determines the 
   /// point at which the operation is done in parallel. If 
   /// the array length is > PARALLELIZATION_THRESHOLD, the 
   /// operation is done in parallel.
   /// 
   /// If the operation is not done in parallel, it uses a
   /// Span<T> to access the array elements.
   /// </summary>

   public static void ForEach<T>(this T[] array, Action<T> action)
   {
      if(array is null)
         throw new ArgumentNullException(nameof(array));
      if(action is null)
         throw new ArgumentNullException(nameof(action));
      if(array.Length > PARALLELIZATION_THRESHOLD)
      {
         var options = new ParallelOptions
         {
            MaxDegreeOfParallelism = Environment.ProcessorCount
         };
         Parallel.For(0, array.Length, options, i => action(array[i]));
      }
      else
      {
         var span = array.AsSpan();
         for(int i = 0; i < span.Length; i++)
            action(span[i]);
      }
   }

   /// <summary>
   /// Same as above except the action also takes the index
   /// of the array element.
   /// </summary>

   public static void ForEach<T>(this T[] array, Action<T, int> action)
   {

      if(array is null)
         throw new ArgumentNullException(nameof(array));
      if(action is null)
         throw new ArgumentNullException(nameof(action));
      if(array.Length > PARALLELIZATION_THRESHOLD)
      {
         var options = new ParallelOptions
         {
            MaxDegreeOfParallelism = Environment.ProcessorCount
         };
         Parallel.For(0, array.Length, options, i => action(array[i], i));
      }
      else
      {
         var span = array.AsSpan();
         for(int i = 0; i < span.Length; i++)
            action(span[i], i);
      }

   }
}

 

 

Message 10 of 20

_gile
Consultant
Consultant

@ActivistInvestor 

Every time I feel like I've published a finished code, you show me how it could be more efficient and robust.

Thanks Tony, you're "The Master".



Gilles Chanteau
Programmation AutoCAD LISP/.NET
GileCAD
GitHub

Message 11 of 20

ActivistInvestor
Mentor
Mentor

@_gile,  Thank you for the compliment, but I probably don't deserve it, and have to confess that I had Claude give me some helpful tips on how to optimize the Normalize() method. How it helped in optimizing that method was it's advise to avoid calls to List<T>.RemoveAt() and use an array of bool to keep track of what elements had already been processed.

0 Likes
Message 12 of 20

ActivistInvestor
Mentor
Mentor

@_gile wrote:

I added a Region.GetCurvesByLoop method in GeometryExtensions.

 


Hi @_gile . I looked at your code, to find out why the bug in my code creates a 1-vertex polyline when there's only a single CircularArc3d or LineSegment3d in the loop, and noticed something that stuck out. So, let me offer this little bit of advice.

 

public static IEnumerable<(LoopType, Curve[])> GetCurvesByLoop(this Region region, Tolerance tolerance)
{
   System.ArgumentNullException.ThrowIfNull(region);
   using var brep = new Brep(region);
   foreach (var loop in brep.Faces.SelectMany(f => f.Loops))
   {
         var curves3d = loop.Edges.Select(edge => ((ExternalCurve3d)edge.Curve).NativeCurve);
         if (curves3d.Count() == 1)   <----- LOOK

 

You're calling Enumerable.Count(), which must call IEnumerator.MoveNext() once for each element in the collection. There could be many. 

 

If you want to know if an IEnumerable<T> contains only a single element, without having to count the entire sequence, you can use:

 

if (!curves3d.Skip(1).Any()) ...

 

The above requires exactly 2 calls to MoveNext() regardless of how many elements are in the sequence. 

0 Likes
Message 13 of 20

_gile
Consultant
Consultant

@ActivistInvestor

Thanks for this advice.

I'll adopt it and the List<T>.RemoveAt replacement.



Gilles Chanteau
Programmation AutoCAD LISP/.NET
GileCAD
GitHub

0 Likes
Message 14 of 20

ActivistInvestor
Mentor
Mentor

@_gile wrote:

@ActivistInvestor

Thanks for this advice.

I'll adopt it and the List<T>.RemoveAt replacement.


You might also want to pursue more parallelization by refactoring your code to separate thread-safe from non thread-safe operations. 

 

For example, the code below will extract all loops from a Complex in parallel:

 

 

 

public static class ParallelBrepExtensions
{
   /// <summary>
   /// Returns an sequence of Curve3d[] arrays that can be 
   /// used to generate Curve entities. The process has been
   /// refactored to support parallel execution, by moving
   /// the non thread-safe code to the caller (which must
   /// create the Curve entities, and which cannot be done
   /// in parallel). 
   /// 
   /// This method will exploit parallel execution for the
   /// creation of all loops within a complex.
   /// 
   /// Each output element will be an array containing either
   /// a single CompositeCurve3d (polyline), or an array of
   /// Curve3d[] (non-polyline). 
   /// 
   /// The caller decides what to do with multiple non-polyline
   /// curves (e.g., create a single spline, multiple polylines
   /// connected to splines or ellipses, or the default behavior 
   /// of the EXPLODE command). 
   /// </summary>
   /// <param name="complex"></param>
   /// <returns></returns>
   /// <exception cref="ArgumentNullException"></exception>
   /// <exception cref="InvalidOperationException"></exception>

   public static IEnumerable<Curve3d[]> ParallelGetLoops(this AcBr.Complex complex)
   {
      if(complex is null)
         throw new ArgumentNullException(nameof(complex));

      var loops = complex.Shells
         .SelectMany(shell => shell.Faces)
         .SelectMany(face => face.Loops)
         .ToArray();

      var results = new Curve3d[loops.Length][];
      loops.ForEach(1, (loop, i) =>   // run in parallel if > 1 loop
      {
         var geCurves = loop.GetGeCurves();
         if(!geCurves.Any())
            throw new InvalidOperationException("no curves");

         if(geCurves.IsPolyline())   // create polyline from multiple GeCurves
            results[i] = new[] { new CompositeCurve3d(geCurves.Normalize()) };
         else  // return a single curve or create a spline
            results[i] = geCurves.ToArray();
      });

      return results;
   }

   public static IEnumerable<Curve3d> GetGeCurves(this BoundaryLoop loop)
   {
      if(loop is null)
         throw new ArgumentNullException(nameof(loop));
      return loop.Edges.Select(edge =>
      {
         if(edge.Curve is ExternalCurve3d crv && crv.IsNativeCurve)
            return crv.NativeCurve;
         else
            throw new NotSupportedException();
      });
   }

   /// <summary>
   /// Indicates if the contents of the input sequence
   /// can be converted to a Polyline.
   /// 
   /// If the input sequence contains only a single
   /// curve element, the result is false, regardless
   /// of the curve type.
   /// 
   /// Requires C# 10.0 or later.
   /// </summary>
   /// <param name="curves"></param>
   /// <returns></returns>

   public static bool IsPolyline(this IEnumerable<Curve3d> curves, bool parallel = false)
   {
      if(curves is null)
         throw new ArgumentNullException(nameof(curves));
      if(!curves.Any() || !curves.Skip(1).Any())
         return false;
      if(parallel)
         return curves.AsParallel().All(static isPolyline);
      else
         return curves.All(static isPolyline);
   }

   static Func<Curve3d, bool> isPolyline = 
     c => c is LineSegment3d or CircularArc3d && !c.IsClosed()
   
   /// <summary>
   /// Ensures that the result is enumerated in order of
   /// traversal, with coincident start/endpoints. This
   /// method rearranges the order of, and reverses the 
   /// direction of input curves as needed.
   /// </summary>
   /// <param name="curves">The unordered set of curves</param>
   /// <param name="validate">True = validate each curve
   /// (should only be used for user-selected curves,
   /// but not for curves coming from a BRep)</param>
   /// <returns>The input curves in order of traversal</returns>
   /// <exception cref="ArgumentNullException"></exception>

   public static Curve3d[] Normalize(this IEnumerable<Curve3d> curves,
      bool validate = false,
      Tolerance tol = default(Tolerance))
   {
      if(curves is null)
         throw new ArgumentNullException(nameof(curves));
      var input = curves as Curve3d[] ?? curves.ToArray();
      if(input.Length < 2)
         return input;
      if(tol.Equals(default(Tolerance)))
         tol = Tolerance.Global;
      int count = input.Length;
      var joined = new bool[count];
      var output = new Curve3d[count];
      if(validate)
         input[0].Validate();
      output[0] = input[0];
      joined[0] = true;
      // TODO:
      // Add optional test to exit loop with
      // current chain if the endpoint of the
      // last added segment is coincident with
      // the startpoint of the first segment.
      Point3d startPoint = input[0].StartPoint;
      var spInput = input.AsSpan();
      var spOutput = output.AsSpan();
      var spJoined = joined.AsSpan();
      int idx = 1;
      while(idx < count)
      {
         Point3d endPoint = spOutput[idx - 1].EndPoint;
         bool found = false;
         for(int i = 0; i < count; i++)
         {
            if(spJoined[i])
               continue;
            var next = spInput[i];
            if(validate)
               next.Validate();
            if(endPoint.IsEqualTo(next.StartPoint, tol))
            {
               spOutput[idx] = next;
               spJoined[i] = true;
               found = true;
               break;
            }
            else if(endPoint.IsEqualTo(next.EndPoint, tol))
            {
               spOutput[idx] = next.GetReverseParameterCurve();
               spJoined[i] = true;
               found = true;
               break;
            }
         }
         if(!found)
            throw new InvalidOperationException("Disjoint curves");
         idx++;
      }
      return output;
   }
}

public static class ParallelArrayExtensions
{
   /// <summary>
   /// Conditional parallel execution based on array size:
   /// 
   /// The ParallelizationThreshold property determines the 
   /// point at which the operation is done in parallel. If 
   /// the array length is > ParallelizationThreshold, the 
   /// operation is done in parallel.
   /// 
   /// The threshold can also be passed as an argument.
   /// 
   /// If the operation is not done in parallel, it uses a
   /// Span<T> to access the array elements.
   /// </summary>

   /// User-tunable threshold

   public static int ParallelizationThreshold
   {
      get;set;
   }

   public static void ForEach<T>(this T[] array, Action<T> action)
   {
      ForEach<T>(array, ParallelizationThreshold, action);
   }

   public static void ForEach<T>(this T[] array, int threshold, Action<T> action)
   {
      if(array is null)
         throw new ArgumentNullException(nameof(array));
      if(action is null)
         throw new ArgumentNullException(nameof(action));
      threshold = threshold < 1 ? ParallelizationThreshold : threshold;
      if(array.Length > threshold)
      {
         var options = new ParallelOptions
         {
            MaxDegreeOfParallelism = Environment.ProcessorCount
         };
         Parallel.For(0, array.Length, options, i => action(array[i]));
      }
      else
      {
         var span = array.AsSpan();
         for(int i = 0; i < span.Length; i++)
            action(span[i]);
      }
   }

   /// <summary>
   /// Same as above except the action also takes the index
   /// of the array element.
   /// </summary>

   public static void ForEach<T>(this T[] array, Action<T, int> action)
   {
      ForEach<T>(array, ParallelizationThreshold, action);
   }

   public static void ForEach<T>(this T[] array, int threshold, Action<T, int> action)
   {
      threshold = threshold < 1 ? ParallelizationThreshold : threshold;
      if(array is null)
         throw new ArgumentNullException(nameof(array));
      if(action is null)
         throw new ArgumentNullException(nameof(action));
      if(array.Length > threshold)
      {
         var options = new ParallelOptions
         {
            MaxDegreeOfParallelism = Environment.ProcessorCount
         };
         Parallel.For(0, array.Length, options, i => action(array[i], i));
      }
      else
      {
         var span = array.AsSpan();
         for(int i = 0; i < span.Length; i++)
            action(span[i], i);
      }

   }
}

 

 

 

0 Likes
Message 15 of 20

ActivistInvestor
Mentor
Mentor

And, here is one more optimization that you might also consider.

 

If you're going to create a polyline when the curves you have can be converted to one, there's no point to iterating the curves and checking their types first, since you can just do that in the process of reordering them.

 

 

/// <summary>
/// This method incorporates the operations performed
/// by IsPolyline() and Normalize(), and returns a
/// CompositeCurve3d representing a Polyline, if the 
/// input curves can be joined to form one, or null 
/// otherwise.
/// 
/// If one unconditionally intends to create a Polyline
/// from the input if possible, this method should be 
/// more efficient as doesn't require iteration of the 
/// input to determione if a Polyline can be created 
/// from it beforehand. Instead it checks each input 
/// curve as they are encountered, and bails out if the 
/// curve isn't a line or arc.
/// </summary>
/// <param name="curves"></param>
/// <param name="validate"></param>
/// <param name="tol"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="InvalidOperationException"></exception>

public static CompositeCurve3d TryConvertToPolyline(this IEnumerable<Curve3d> curves,
   bool validate = false,
   Tolerance tol = default(Tolerance))
{
   if(curves is null)
      throw new ArgumentNullException(nameof(curves));
   var input = curves as Curve3d[] ?? curves.ToArray();
   if(input.Length < 2)
      return null;
   if(tol.Equals(default(Tolerance)))
      tol = Tolerance.Global;
   int count = input.Length;
   var joined = new bool[count];
   Curve3d first = input[0];
   if(!first.IsPolySegment())
      return null;
   if(validate)
      first.Validate();
   var output = new Curve3d[count];
   output[0] = first;
   joined[0] = true;
   var spInput = input.AsSpan();
   var spOutput = output.AsSpan();
   var spJoined = joined.AsSpan();
   int idx = 1;
   while(idx < count)
   {
      Point3d endPoint = spOutput[idx - 1].EndPoint;
      bool found = false;
      for(int i = 0; i < count; i++)
      {
         if(spJoined[i])
            continue;
         var next = spInput[i];
         if(!next.IsPolySegment())
            return null;
         if(validate)
            next.Validate();
         if(endPoint.IsEqualTo(next.StartPoint, tol))
         {
            spOutput[idx] = next;
            spJoined[i] = true;
            found = true;
            break;
         }
         else if(endPoint.IsEqualTo(next.EndPoint, tol))
         {
            spOutput[idx] = next.GetReverseParameterCurve();
            spJoined[i] = true;
            found = true;
            break;
         }
      }
      if(!found)
         throw new InvalidOperationException("Disjoint curves");
      idx++;
   }
   return new CompositeCurve3d(output);
}

 

Message 16 of 20

ActivistInvestor
Mentor
Mentor

I needed to validate the parallel code I posted above, so I decided to write an Overrule that will cause the EXPLODE command to create Polylines, rather than lines/arcs when exploding a Region.

 

Anyone that's interested can find all of the code here.

 

 

using System.Linq;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.BoundaryRepresentation;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.DatabaseServices.Extensions;
using Autodesk.AutoCAD.Runtime;

namespace AcMgdLib.DatabaseServices
{
   public static partial class RegionExtensions
   {
      /// <summary>
      /// An overrule that will explode regions into Polylines
      /// where possible, overriding the default behavior which 
      /// explodes regions to individual line and arc segments.
      /// 
      /// With the overrule running, the EXPLODE command will
      /// create Polylines rather than chains of contiguous
      /// lines and arcs.
      /// 
      /// Currently, the code will only convert loops that are
      /// comprised entirely of lines and arcs into Polylines.
      /// If an ACIS/ASM curve appears in a loop, the behavior
      /// is the same as the default behavior of EXPLODE.
      /// 
      /// Work-in-progress.
      /// 
      /// The goal is to extend the functionality of this code
      /// to allow the following options:
      /// 
      ///   1. Convert loops containing ACIS/ASM curves into 
      ///      a single, closed Spline entity. While this is 
      ///      currently possible, it is not enabled by default
      ///      because it may not produce the desired result in
      ///      every case. The JOIN command can be used on the
      ///      result to create a single, closed Spline if that
      ///      is desired.
      ///      
      ///   2. Convert loops containing ACIS/ASM curves into
      ///      multiple entities with all contiguous line/arc
      ///      sequences converted to Polylines. This is a bit
      ///      more of a challenge but is entirely possible.
      ///      
      /// Usage:
      /// 
      /// Add all files in the repo folder to a project, 
      /// build it, and NETLOAD the compiled assembly into 
      /// AutoCAD.
      /// 
      /// Issue the REGIONEXPLODE command to enable the 
      /// overrule.
      /// 
      /// Issue the EXPLODE command, and select one or more
      /// regions with loops containing no splines or ellptical
      /// arc segments (e.g., loops created from Polylines).
      /// The EXPLODE command will produce Polylines rather 
      /// than lines and arcs.
      /// 
      /// It probably should have worked this way from
      /// the outset, but.....
      /// 
      /// Development notes:
      /// 
      /// The overrule will propagate XData from the Region
      /// being exploded to the resulting entities (this is
      /// purely experimental and may only be useful in very
      /// specialized use-cases).
      /// 
      /// Parallel Execution disabled by default:
      /// 
      /// Due to some unexplained, intermittent crashes, the
      /// parallel switch is disabled by default, until the
      /// cause of the failure can be identified. At this
      /// point, the suspect is extracting curves from breps,
      /// But, this has yet to be confirmed. If that proves
      /// to be the case, the code will need to be refactored
      /// to extract brep curves serially, and then operate
      /// on them in parallel.
      /// 
      /// Disclaimer: This is experimental code that is not
      /// recommended for production AutoCAD use. Any use of
      /// this code is undertaken entirely at your own risk,
      /// and the author is not responsible for damages that
      /// arise out of using this code.
      /// 
      /// If you choose to experiment with this code, please
      /// report bugs, feature enhancement requests, or other 
      /// issues via the repository discussion at:
      /// 
      ///   https://github.com/ActivistInvestor/AcMgdLib/discussions
      /// 
      /// </summary>

      public class RegionExplodeOverrule : TransformOverrule<Region>
      {
         public override void Explode(Entity entity, DBObjectCollection entitites)
         {
            if(entity is Region region && IsExplodeCommand)
            {
               try
               {
                  using(var brep = new Brep(region))
                  {
                     var geCurves = brep.Explode();
                     var xdata = entity.XData;
                     bool hasXData = xdata != null && xdata.Cast<TypedValue>().Any();

                     foreach(var geCurve in geCurves)
                     {
                        Entity curve = Curve.CreateFromGeCurve(geCurve);
                        entitites.Add(curve);
                        if(hasXData)
                           curve.XData = xdata;
                     }
                  }
               }
               catch(System.Exception)
               {
                  base.Explode(entity, entitites);
               }
            }
            else
            {
               base.Explode(entity, entitites);
            }
         }

         static RegionExplodeOverrule instance = null;

         static bool parallel = false;

         /// <summary>
         /// A command that enables/disables parallel
         /// execution of work done by this overrule.
         /// 
         /// Note: parallel execution is disabled by
         /// default, and must be enabled using this
         /// command. Unless you are dealing with very
         /// complex or dense regions comprised of many
         /// curves or vertices, you don't really need
         /// parallel execution, and probably will not
         /// benefit from it. Also remember that because
         /// this code is experimental and has not been
         /// thoroughly tested in parallel, there is a
         /// possiblity of failure in that mode.
         /// </summary>
         
         [CommandMethod("REGIONEXPLODEPARALLEL")]
         public static void ToggleParallel()
         {
            parallel ^= true;
            string what = parallel ? "en" : "dis";
            Application.DocumentManager.MdiActiveDocument
               .Editor.WriteMessage($"\nRegion Explode parallelization {what}abled");
         }

         [CommandMethod("REGIONEXPLODE")]
         public static void StopStart()
         {
            if(instance == null)
               instance = new RegionExplodeOverrule();
            else
            {
               instance.IsOverruling ^= true;
            }
            string what = instance.IsOverruling ? "en" : "dis";
            Application.DocumentManager.MdiActiveDocument
               .Editor.WriteMessage($"\nExplode Regions to Polylines {what}abled");
         }

         static bool IsExplodeCommand
         {
            get
            {
               return Application.DocumentManager.MdiActiveDocument?
                  .CommandInProgress == "EXPLODE";
            }
         }
      }

   }
}

 

0 Likes
Message 17 of 20

_gile
Consultant
Consultant

@ActivistInvestor  a écrit :

And, here is one more optimization that you might also consider.

 

If you're going to create a polyline when the curves you have can be converted to one, there's no point to iterating the curves and checking their types first, since you can just do that in the process of reordering them.


This is the one I was considering (parallelization is not a priority for me).

Instead of limiting this to converting into Polyline (which should require to check if the arcs and lines to lie on the same plane), I tried something more generic by just trying to convert into a CompositeCurve3d and using a predicate to filter the input Curve3d collection.

/// <summary>
/// Tries to convert the Curve3d sequence into a CompositeCurve3d.
/// </summary>
/// <param name="source">Collection this method applies to.</param>
/// <param name="compositeCurve">Output composite curve.</param>
/// <param name="tolerance">Tolerance used to compare end points.</param>
/// <param name="predicate">Predicate used to filter input curves 3d.</param>
/// <returns>true, if the composite curve could be created; false otherwise.</returns>
/// <exception cref="ArgumentNullException">ArgumentNullException is thrown if <paramref name="source"/> is null.</exception>
/// <exception cref="InvalidOperationException">InvalidOperationException is thrown if non-contiguous segments are found.</exception>
public static bool TryConvertToCompositeCurve(
    this IEnumerable<Curve3d> source,
    out CompositeCurve3d compositeCurve,
    Tolerance tolerance = default,
    Predicate<Curve3d> predicate = null)
{
    Assert.IsNotNull(source, nameof(source));

    var isValid = predicate ?? (c => true);

    if (tolerance.Equals(default(Tolerance)))
        tolerance = Tolerance.Global;

    compositeCurve = default;

    var input = source as Curve3d[] ?? source.ToArray();

    if (!isValid(input[0]))
        return false;

    int length = input.Length;
    if (length < 2)
    {
        compositeCurve = new CompositeCurve3d(new[] { input[0] });
        return true;
    }

    var output = new Curve3d[length];
    var done = new bool[length];

    output[0] = input[0];
    done[0] = true;
    int count = 1;
    var endPoint = output[0].EndPoint;

    while (count < length)
    {
        bool found = false;

        for (int i = 0; i < length; i++)
        {
            if (done[i])
                continue;

            var current = input[i];
            if (!isValid(current))
                return false;

            if (endPoint.IsEqualTo(current.StartPoint, tolerance))
            {
                output[count] = current;
                endPoint = current.EndPoint;
                found = done[i] = true;
                break;
            }
            else if (endPoint.IsEqualTo(current.EndPoint, tolerance))
            {
                output[count] = current.GetReverseParameterCurve();
                endPoint = current.StartPoint;
                found = done[i] = true;
                break;
            }
        }

        if (!found)
            return false;

        count++;
    }
    compositeCurve = new CompositeCurve3d(output);
    return true;
}

 

The GetCurvesByLoop becomes:

public static IEnumerable<(LoopType, Curve[])> GetCurvesByLoop(this Region region, Tolerance tolerance)
{
    Assert.IsNotNull(region, nameof(region));
    using (var brep = new Brep(region))
    {
        foreach (var loop in brep.Faces.SelectMany(f => f.Loops))
        {
            var curves3d = loop.Edges.Select(edge => ((ExternalCurve3d)edge.Curve).NativeCurve);
            if (!curves3d.Skip(1).Any())
            {
                yield return (loop.LoopType, new[] { Curve.CreateFromGeCurve(curves3d.First()) });
            }
            else if (curves3d.TryConvertToCompositeCurve(out CompositeCurve3d compositeCurve, tolerance, c => c is LineSegment3d || c is CircularArc3d))
            {
                yield return (loop.LoopType, new[] { (Polyline)Curve.CreateFromGeCurve(compositeCurve) });
            }
            else
            {
                yield return (loop.LoopType, curves3d.Select(c => Curve.CreateFromGeCurve(c)).ToArray());
            }
        }
    }
}


Gilles Chanteau
Programmation AutoCAD LISP/.NET
GileCAD
GitHub

0 Likes
Message 18 of 20

ActivistInvestor
Mentor
Mentor

Hi @_gile .  Right now I'm looking at how to extend the code to convert all contiguous lines/arcs to polylines, even those in loops that have ACIS/ASM curve segments (splines or elliptical arcs). The basic problem there is that the conversion will produce fewer resulting curves than input edges. So for example, instead of a spline followed by three lines, and then another spline, the conversion will produce a spline followed by a polyline, followed by another spline. That's going to involve a major refactoring of the code as I see no other way to easily adapt either of our solutions to do that.

 

The reason I need to do the more-complete conversion from lines/arcs to polylines is because I need it for a text-to-geometry conversion tool (similar to the Express Tools TXTEXP, but better) I've been working on, and it is very Region-intensive. There are fonts in which a single character produces splines with hundreds of control points, or polylines with hundreds of vertices, so I need to explode regions without ending up with hundreds of interconnected lines/arcs. Similar to the RegionExplodeOverrule, the code will also use an Overrule to enable exploding of TEXT entities into AutoCAD curves, regions, or 3D solids, composed largely of splines and polylines,

 

ActivistInvestor_1-1733080915669.png

 

An example DWG containing the output can be found here

 

0 Likes
Message 19 of 20

ActivistInvestor
Mentor
Mentor

Above, I described what I wanted to achieve with loops containing both line/arc segments and ACIS/ASM curves.

 

It may not be as difficult as it seemed at first, and this is completely untested, but should do what needs to be done:

 

 

/// <summary>
/// Given a sequence of contiguous Curve3d, this will 
/// replace sub-sequences consisting of two or more line 
/// or arc segments with a polyline.
/// 
/// The input sequence must form a contiguous chain of
/// inter-connected curves.
/// </summary>
/// <param name="curves"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>

public static IEnumerable<Curve3d> Optimize(this IEnumerable<Curve3d> curves)
{
   if(curves is null)
      throw new ArgumentNullException(nameof(curves));
   if(!curves.Any())
      yield break;
   if(!curves.Skip(1).Any())
      yield return curves.First();
   List<Curve3d> segments = new List<Curve3d>();
   foreach(Curve3d curve in curves)
   {
      if(curve is LineSegment3d or CircularArc3d)
      {
         segments.Add(curve);
         continue;
      }
      if(segments.Count == 0)
      {
         yield return curve;
         continue;
      }
      if(segments.Count == 1)
      {
         yield return segments[0];
         yield return curve;
         segments.Clear();
         continue;
      }
      foreach(var segment in segments.TryConvert())
         yield return segment;
      yield return curve;
      segments.Clear();
   }
   if(segments.Count > 0)
   {
      foreach(var segment in segments.TryConvert())
         yield return segment;
   }
}

static IEnumerable<Curve3d> TryConvert(this IEnumerable<Curve3d> segments)
{
   var pline = segments.TryCreatePolyline();
   if(pline != null)
   {
      return new Curve3d[] { pline };
   }
   else
   {
      return segments;
   }
}

 

 

Update: Tested and working.

 

In the following video, a region was created from 2 splines and 3 polylines. Exploding the region using the EXPLODE command produces 2 splines and 3 polylines - IOW, exactly what was used to create the Region.

 

 

The updated RegionExplodeOverrule used in the video above can be found here

0 Likes
Message 20 of 20

18348401357
Enthusiast
Enthusiast

get all curves ,then find the largest area one 

    public static IEnumerable<Curve> ToCurves(this Region region)
    {
        if (region.IsNull)
            yield break;
        using var brep = new Brep(region);
        var loops = brep.Complexes.SelectMany(complex => complex.Shells)
            .SelectMany(shell => shell.Faces)
            .SelectMany(face => face.Loops);
        foreach (var loop in loops)
        {
            var curves3d = loop.Edges.Select(edge => ((ExternalCurve3d)edge.Curve).NativeCurve)
                .ToList();
            var cur = Curve.CreateFromGeCurve(1 < curves3d.Count
                ? new CompositeCurve3d(curves3d.ToOrderedArray())
                : curves3d.First());

            foreach (var curve3d in curves3d)
            {
                curve3d.Dispose();
            }

            cur.SetPropertiesFrom(region);
            yield return cur;
        }
    }

  

0 Likes