perf: use perimeter-only drawing in best fit pair evaluation

PairEvaluator was cloning the full CNC program (including all internal
cutouts) for every candidate. For parts with many holes (e.g. 952),
this caused O(n²) overlap checks and thousands of unnecessary polygon
tessellations per candidate.

Now extracts the perimeter shape once, builds a lightweight drawing
from it, and uses that for all Part.CreateAtOrigin calls. Cutouts are
irrelevant for best fit — only the outer boundary matters for pairing.

75x speedup on a 952-hole rectangle (30s → 0.4s).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 12:14:02 -04:00
parent 640814fdf6
commit 3481764416
+35 -33
View File
@@ -15,11 +15,18 @@ namespace OpenNest.Engine.BestFit
public List<BestFitResult> EvaluateAll(List<PairCandidate> candidates) public List<BestFitResult> EvaluateAll(List<PairCandidate> candidates)
{ {
if (candidates.Count == 0)
return new List<BestFitResult>();
// Build a perimeter-only drawing once — all candidates share the same drawing.
// This avoids cloning the full program (with all cutouts) for every candidate.
var perimeterDrawing = CreatePerimeterDrawing(candidates[0].Drawing);
var resultBag = new ConcurrentBag<BestFitResult>(); var resultBag = new ConcurrentBag<BestFitResult>();
Parallel.ForEach(candidates, c => Parallel.ForEach(candidates, c =>
{ {
resultBag.Add(Evaluate(c)); resultBag.Add(Evaluate(c, perimeterDrawing));
}); });
return resultBag.ToList(); return resultBag.ToList();
@@ -27,18 +34,24 @@ namespace OpenNest.Engine.BestFit
public BestFitResult Evaluate(PairCandidate candidate) public BestFitResult Evaluate(PairCandidate candidate)
{ {
var drawing = candidate.Drawing; var perimeterDrawing = CreatePerimeterDrawing(candidate.Drawing);
return Evaluate(candidate, perimeterDrawing);
}
var part1 = Part.CreateAtOrigin(drawing); private BestFitResult Evaluate(PairCandidate candidate, Drawing perimeterDrawing)
{
var part1 = Part.CreateAtOrigin(perimeterDrawing);
var part2 = Part.CreateAtOrigin(drawing, candidate.Part2Rotation); var part2 = Part.CreateAtOrigin(perimeterDrawing, candidate.Part2Rotation);
part2.Location = candidate.Part2Offset; part2.Location = candidate.Part2Offset;
part2.UpdateBounds(); part2.UpdateBounds();
// Check overlap via shape intersection // Overlap check — perimeter vs perimeter
var overlaps = CheckOverlap(part1, part2); var shape1 = GetPerimeterShape(part1);
var shape2 = GetPerimeterShape(part2);
var overlaps = shape1 != null && shape2 != null && shape1.Intersects(shape2, out _);
// Collect all polygon vertices for convex hull / optimal rotation // Convex hull vertices from perimeter polygons only
var allPoints = GetPartVertices(part1); var allPoints = GetPartVertices(part1);
allPoints.AddRange(GetPartVertices(part2)); allPoints.AddRange(GetPartVertices(part2));
@@ -66,7 +79,7 @@ namespace OpenNest.Engine.BestFit
hullAngles = new List<double> { 0 }; hullAngles = new List<double> { 0 };
} }
var trueArea = drawing.Area * 2; var trueArea = candidate.Drawing.Area * 2;
// Normalize to landscape (width >= height) for consistent display. // Normalize to landscape (width >= height) for consistent display.
if (bestHeight > bestWidth) if (bestHeight > bestWidth)
@@ -91,38 +104,29 @@ namespace OpenNest.Engine.BestFit
}; };
} }
private bool CheckOverlap(Part part1, Part part2) private static Drawing CreatePerimeterDrawing(Drawing source)
{ {
var shapes1 = GetPartShapes(part1); var entities = ConvertProgram.ToGeometry(source.Program)
var shapes2 = GetPartShapes(part2); .Where(e => e.Layer != SpecialLayers.Rapid).ToList();
var profile = new ShapeProfile(entities);
for (var i = 0; i < shapes1.Count; i++) var program = ConvertGeometry.ToProgram(profile.Perimeter);
{ return new Drawing(source.Name, program);
for (var j = 0; j < shapes2.Count; j++)
{
List<Vector> pts;
if (shapes1[i].Intersects(shapes2[j], out pts))
return true;
}
}
return false;
} }
private List<Shape> GetPartShapes(Part part) private static Shape GetPerimeterShape(Part part)
{ {
var entities = ConvertProgram.ToGeometry(part.Program) var entities = ConvertProgram.ToGeometry(part.Program)
.Where(e => e.Layer != SpecialLayers.Rapid); .Where(e => e.Layer != SpecialLayers.Rapid).ToList();
var shapes = ShapeBuilder.GetShapes(entities); var shapes = ShapeBuilder.GetShapes(entities);
shapes.ForEach(s => s.Offset(part.Location)); if (shapes.Count == 0) return null;
return shapes; shapes[0].Offset(part.Location);
return shapes[0];
} }
private List<Vector> GetPartVertices(Part part) private static List<Vector> GetPartVertices(Part part)
{ {
var entities = ConvertProgram.ToGeometry(part.Program) var entities = ConvertProgram.ToGeometry(part.Program)
.Where(e => e.Layer != SpecialLayers.Rapid); .Where(e => e.Layer != SpecialLayers.Rapid).ToList();
var shapes = ShapeBuilder.GetShapes(entities); var shapes = ShapeBuilder.GetShapes(entities);
var points = new List<Vector>(); var points = new List<Vector>();
@@ -130,9 +134,7 @@ namespace OpenNest.Engine.BestFit
{ {
var polygon = shape.ToPolygonWithTolerance(ChordTolerance); var polygon = shape.ToPolygonWithTolerance(ChordTolerance);
polygon.Offset(part.Location); polygon.Offset(part.Location);
points.AddRange(polygon.Vertices);
foreach (var vertex in polygon.Vertices)
points.Add(vertex);
} }
return points; return points;