I created a version of SortCurveLoops that converts the XYZ points to UV points, then works on planar loops. I only tested it with a few cases where ExporterIFCUtils.SortCurveLoops fails, and it works well. I will start using it and see if it breaks in the next weeks.
My function takes in input a Face instead of a list of CurveLoops, because all the curves are tessellated and converted to UV. The Face is used for both finding the CurveLoops and converting to UV.
If there are outer loops contained in other outer loops (like in the third snapshot), the innermost loops are first in the resulting list.



This is the function:
public static List<List<CurveLoop>> SortCurveLoops(Face face)
{
var allLoops = face.GetEdgesAsCurveLoops().Select(loop => new CurveLoopUV(loop, face)).ToList();
var outerLoops = allLoops.Where(loop => loop.IsCounterclockwise).ToList();
var innerLoops = allLoops.Where(loop => !outerLoops.Contains(loop)).ToList();
// sort outerLoops putting last the ones that are outside all the preceding loops
bool somethingHasChanged;
do
{
somethingHasChanged = false;
for (var i = 1; i < outerLoops.Count(); i++)
{
var point = outerLoops[i].StartPointUV;
var loop = outerLoops[i - 1];
if (loop.IsPointInside(point) is CurveLoopUV.PointLocation.Inside)
{
var tmp = outerLoops[i];
outerLoops[i] = outerLoops[i - 1];
outerLoops[i - 1] = tmp;
somethingHasChanged = true;
}
}
} while (somethingHasChanged);
var result = new List<List<CurveLoop>>();
foreach (var outerLoop in outerLoops)
{
var list = new List<CurveLoop> {outerLoop.Loop3d};
for (var i = innerLoops.Count - 1; i >= 0; i--)
{
var innerLoop = innerLoops[i];
if (outerLoops.Count() == 1 // skip testing whether the inner loop is inside the outer loop
|| outerLoop.IsPointInside(innerLoop.StartPointUV) == CurveLoopUV.PointLocation.Inside)
{
list.Add(innerLoop.Loop3d);
innerLoops.RemoveAt(i);
}
}
result.Add(list);
}
return result;
}
This is the class CurveLoopUV that converts the curves from 3D XYZ to UV, then to planar XYZ.
class CurveLoopUV : IEnumerable<Curve>
{
public enum PointLocation
{
Outside,
OnTheEdge,
Inside,
}
public CurveLoop Loop3d { get; }
private readonly CurveLoop _loop2d;
public readonly double MinX, MaxX, MinY, MaxY;
public CurveLoopUV(CurveLoop curveLoop, Face face)
{
Loop3d = curveLoop;
_loop2d = new CurveLoop();
var points3d = Loop3d.SelectMany(curve => curve.Tessellate().Skip(1));
var pointsUv = points3d.Select(point3d => face.Project(point3d).UVPoint);
var points2d = pointsUv.Select(pointUv => new XYZ(pointUv.U, pointUv.V, 0)).ToList();
MinX = MinY = 1.0e100;
MaxX = MaxY = -1.0e100;
var nPoints = points2d.Count();
for (var i = 0; i < nPoints; i++)
{
var p1 = points2d[i];
var p2 = points2d[(i + 1) % nPoints];
_loop2d.Append(Line.CreateBound(p1, p2));
if (p1.X < MinX) MinX = p1.X;
if (p1.Y < MinY) MinY = p1.Y;
if (p1.X > MaxX) MaxX = p1.X;
if (p1.Y > MaxY) MaxY = p1.Y;
}
}
public PointLocation IsPointInside(XYZ point)
{
if (point.Y + Eps < MinY || point.Y + Eps > MaxY)
return PointLocation.Outside;
if (_loop2d.Any(curve => curve.Distance(point) < Eps))
return PointLocation.OnTheEdge;
var line = Line.CreateBound(point, new XYZ(1.0e100, point.Y, 0));
var nIntersections = _loop2d.Count(edge => edge.Intersect(line) == SetComparisonResult.Overlap);
return nIntersections % 2 == 1 ? PointLocation.Inside : PointLocation.Outside;
}
public bool IsCounterclockwise => _loop2d.IsCounterclockwise(XYZ.BasisZ);
public XYZ StartPointUV => _loop2d.First().GetEndPoint(0);
public IEnumerator<Curve> GetEnumerator() => _loop2d.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}