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);
}
}
}