feat: add FillRectangleBestFit strategy and remove false overlap rejection

- Remove IsBetterValidFill overlap gate for FillLinear results; the
  geometry-aware spacing in FillLinear is sufficient and the overlap
  check produced false positives on parts with arcs/curves, causing
  valid grid layouts to be rejected in favor of inferior pair fills.
- Add FillRectangleBestFit strategy that uses BestCombination to mix
  normal and rotated orientations, filling remnant strips for higher
  part counts on rectangular parts.
- All Fill overloads now compare linear, rectangle best-fit, and
  pair-based strategies, picking whichever yields the most parts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 20:00:38 -05:00
parent 0c14d7f854
commit 90b89a5dfa

View File

@@ -42,26 +42,36 @@ namespace OpenNest
engine.Fill(item.Drawing, bestRotation + Angle.HalfPI, NestDirection.Vertical)
};
// Pick the best valid linear configuration.
// Pick the best linear configuration. FillLinear already ensures
// geometry-aware spacing, so skip the redundant overlap check that
// can produce false positives on arcs/curves.
List<Part> linearBest = null;
foreach (var config in configs)
{
if (IsBetterValidFill(config, linearBest))
if (IsBetterFill(config, linearBest))
linearBest = config;
}
var linearMs = sw.ElapsedMilliseconds;
// Try rectangle best-fit (mixes orientations to fill remnant strips).
var rectResult = FillRectangleBestFit(item, workArea);
var rectMs = sw.ElapsedMilliseconds - linearMs;
// Try pair-based approach.
var pairResult = FillWithPairs(item);
var pairMs = sw.ElapsedMilliseconds - linearMs;
var pairMs = sw.ElapsedMilliseconds - linearMs - rectMs;
// Pick whichever is the better fill.
Debug.WriteLine($"[NestEngine.Fill] Linear: {linearBest?.Count ?? 0} parts ({linearMs}ms) | Pair: {pairResult.Count} parts ({pairMs}ms) | WorkArea: {workArea.Width:F1}x{workArea.Height:F1}");
Debug.WriteLine($"[NestEngine.Fill] Linear: {linearBest?.Count ?? 0} parts ({linearMs}ms) | Rect: {rectResult?.Count ?? 0} parts ({rectMs}ms) | Pair: {pairResult.Count} parts ({pairMs}ms) | WorkArea: {workArea.Width:F1}x{workArea.Height:F1}");
var best = linearBest;
if (IsBetterFill(rectResult, best))
best = rectResult;
if (IsBetterFill(pairResult, best))
best = pairResult;
@@ -86,10 +96,16 @@ namespace OpenNest
var angles = FindHullEdgeAngles(groupParts);
var best = FillPattern(engine, groupParts, angles);
// For single-part groups, also try pair-based filling.
// For single-part groups, also try rectangle best-fit and pair-based filling.
if (groupParts.Count == 1)
{
var pairResult = FillWithPairs(new NestItem { Drawing = groupParts[0].BaseDrawing });
var nestItem = new NestItem { Drawing = groupParts[0].BaseDrawing };
var rectResult = FillRectangleBestFit(nestItem, workArea);
if (IsBetterFill(rectResult, best))
best = rectResult;
var pairResult = FillWithPairs(nestItem);
if (IsBetterFill(pairResult, best))
best = pairResult;
@@ -120,12 +136,20 @@ namespace OpenNest
foreach (var config in configs)
{
if (IsBetterValidFill(config, best))
if (IsBetterFill(config, best))
best = config;
}
Debug.WriteLine($"[Fill(NestItem,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Height:F1}");
// Try rectangle best-fit (mixes orientations to fill remnant strips).
var rectResult = FillRectangleBestFit(item, workArea);
Debug.WriteLine($"[Fill(NestItem,Box)] RectBestFit: {rectResult?.Count ?? 0} parts");
if (IsBetterFill(rectResult, best))
best = rectResult;
// Try pair-based approach.
var pairResult = FillWithPairs(item, workArea);
@@ -157,7 +181,15 @@ namespace OpenNest
if (groupParts.Count == 1)
{
var pairResult = FillWithPairs(new NestItem { Drawing = groupParts[0].BaseDrawing }, workArea);
var nestItem = new NestItem { Drawing = groupParts[0].BaseDrawing };
var rectResult = FillRectangleBestFit(nestItem, workArea);
Debug.WriteLine($"[Fill(groupParts,Box)] RectBestFit: {rectResult?.Count ?? 0} parts");
if (IsBetterFill(rectResult, best))
best = rectResult;
var pairResult = FillWithPairs(nestItem, workArea);
Debug.WriteLine($"[Fill(groupParts,Box)] Pair: {pairResult.Count} parts | Winner: {(IsBetterFill(pairResult, best) ? "Pair" : "Linear")}");
@@ -262,6 +294,26 @@ namespace OpenNest
return parts.Count > 0;
}
private List<Part> FillRectangleBestFit(NestItem item, Box workArea)
{
var binItem = ConvertToRectangleItem(item);
var bin = new Bin
{
Location = workArea.Location,
Size = workArea.Size
};
bin.Width += Plate.PartSpacing;
bin.Height += Plate.PartSpacing;
var engine = new FillBestFit(bin);
engine.Fill(binItem);
var nestItems = new List<NestItem> { item };
return ConvertToParts(bin, nestItems);
}
private List<Part> FillWithPairs(NestItem item)
{
return FillWithPairs(item, Plate.WorkArea());