perf: optimize best fit computation and plate optimizer

- Try all valid best fit pairs instead of only the first when qty=2,
  picking the best via IsBetterFill comparer (fixes suboptimal plate
  selection during auto-nesting)
- Pre-compute best fits across all plate sizes once via
  BestFitCache.ComputeForSizes instead of per-size GPU evaluation
- Early exit plate optimizer when all items fit (salvage < 100%)
- Trim slide offset sweep range to 50% overlap to reduce candidates
- Use actual geometry (ray-arc/ray-circle intersection) instead of
  tessellated polygons for slide distance computation — eliminates
  the massive line count from circle/arc tessellation
- Add RayArcDistance and RayCircleDistance to SpatialQuery
- Add PartGeometry.GetOffsetPerimeterEntities for non-tessellated
  perimeter extraction
- Disable GPU slide computer (slower than CPU currently)
- Remove dead SelectBestFitPair virtual method and overrides

Reduces best fit computation from 7+ minutes to ~4 seconds for a
73x25" part with 30+ holes on a 48x96 plate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 10:21:44 -04:00
parent 3bdbf21881
commit e93523d7a2
13 changed files with 410 additions and 100 deletions

View File

@@ -139,24 +139,42 @@ namespace OpenNest
var bestFits = BestFitCache.GetOrCompute(
drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing);
var best = SelectBestFitPair(bestFits);
if (best == null)
return null;
List<Part> bestPlacement = null;
// BuildParts produces landscape orientation (Width >= Height).
// Try both landscape and portrait (90° rotated) and let the
// engine's comparer pick the better orientation.
var landscape = best.BuildParts(drawing);
var portrait = RotatePair90(landscape);
foreach (var fit in bestFits)
{
if (!fit.Keep)
continue;
var lFits = TryOffsetToWorkArea(landscape, workArea);
var pFits = TryOffsetToWorkArea(portrait, workArea);
// Skip pairs that can't possibly fit the work area in either orientation.
if (fit.ShortestSide > System.Math.Min(workArea.Width, workArea.Length) + Tolerance.Epsilon)
continue;
if (fit.LongestSide > System.Math.Max(workArea.Width, workArea.Length) + Tolerance.Epsilon)
continue;
if (!lFits && !pFits)
return null;
if (lFits && pFits)
return IsBetterFill(portrait, landscape, workArea) ? portrait : landscape;
return lFits ? landscape : portrait;
var landscape = fit.BuildParts(drawing);
var portrait = RotatePair90(landscape);
var lFits = TryOffsetToWorkArea(landscape, workArea);
var pFits = TryOffsetToWorkArea(portrait, workArea);
// Pick the better orientation for this pair.
List<Part> candidate = null;
if (lFits && pFits)
candidate = IsBetterFill(portrait, landscape, workArea) ? portrait : landscape;
else if (lFits)
candidate = landscape;
else if (pFits)
candidate = portrait;
if (candidate == null)
continue;
if (bestPlacement == null || IsBetterFill(candidate, bestPlacement, workArea))
bestPlacement = candidate;
}
return bestPlacement;
}
private static List<Part> RotatePair90(List<Part> parts)