feat: use FillScore for fill result comparisons in NestEngine

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 23:11:38 -04:00
parent a5b7049ecc
commit 4d250e3990
+32 -34
View File
@@ -36,7 +36,7 @@ namespace OpenNest
// Try improving by filling the remainder strip separately. // Try improving by filling the remainder strip separately.
var improved = TryRemainderImprovement(item, workArea, best); var improved = TryRemainderImprovement(item, workArea, best);
if (IsBetterFill(improved, best)) if (IsBetterFill(improved, best, workArea))
{ {
Debug.WriteLine($"[Fill] Remainder improvement: {improved.Count} parts (was {best?.Count ?? 0})"); Debug.WriteLine($"[Fill] Remainder improvement: {improved.Count} parts (was {best?.Count ?? 0})");
best = improved; best = improved;
@@ -92,29 +92,30 @@ namespace OpenNest
var h = engine.Fill(item.Drawing, angle, NestDirection.Horizontal); var h = engine.Fill(item.Drawing, angle, NestDirection.Horizontal);
var v = engine.Fill(item.Drawing, angle, NestDirection.Vertical); var v = engine.Fill(item.Drawing, angle, NestDirection.Vertical);
if (IsBetterFill(h, best)) if (IsBetterFill(h, best, workArea))
best = h; best = h;
if (IsBetterFill(v, best)) if (IsBetterFill(v, best, workArea))
best = v; best = v;
} }
Debug.WriteLine($"[FindBestFill] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Height:F1} | Angles: {angles.Count}"); var bestLinearScore = best != null ? FillScore.Compute(best, workArea) : default;
Debug.WriteLine($"[FindBestFill] Linear: {bestLinearScore.Count} parts, density={bestLinearScore.Density:P1} | WorkArea: {workArea.Width:F1}x{workArea.Height:F1} | Angles: {angles.Count}");
// Try rectangle best-fit (mixes orientations to fill remnant strips). // Try rectangle best-fit (mixes orientations to fill remnant strips).
var rectResult = FillRectangleBestFit(item, workArea); var rectResult = FillRectangleBestFit(item, workArea);
Debug.WriteLine($"[FindBestFill] RectBestFit: {rectResult?.Count ?? 0} parts"); Debug.WriteLine($"[FindBestFill] RectBestFit: {rectResult?.Count ?? 0} parts");
if (IsBetterFill(rectResult, best)) if (IsBetterFill(rectResult, best, workArea))
best = rectResult; best = rectResult;
// Try pair-based approach. // Try pair-based approach.
var pairResult = FillWithPairs(item, workArea); var pairResult = FillWithPairs(item, workArea);
Debug.WriteLine($"[FindBestFill] Pair: {pairResult.Count} parts | Winner: {(IsBetterFill(pairResult, best) ? "Pair" : "Linear")}"); Debug.WriteLine($"[FindBestFill] Pair: {pairResult.Count} parts | Winner: {(IsBetterFill(pairResult, best, workArea) ? "Pair" : "Linear")}");
if (IsBetterFill(pairResult, best)) if (IsBetterFill(pairResult, best, workArea))
best = pairResult; best = pairResult;
return best; return best;
@@ -127,7 +128,7 @@ namespace OpenNest
var engine = new FillLinear(workArea, Plate.PartSpacing); var engine = new FillLinear(workArea, Plate.PartSpacing);
var angles = RotationAnalysis.FindHullEdgeAngles(groupParts); var angles = RotationAnalysis.FindHullEdgeAngles(groupParts);
var best = FillPattern(engine, groupParts, angles); var best = FillPattern(engine, groupParts, angles, workArea);
Debug.WriteLine($"[Fill(groupParts,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Height:F1}"); Debug.WriteLine($"[Fill(groupParts,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Height:F1}");
@@ -138,20 +139,20 @@ namespace OpenNest
Debug.WriteLine($"[Fill(groupParts,Box)] RectBestFit: {rectResult?.Count ?? 0} parts"); Debug.WriteLine($"[Fill(groupParts,Box)] RectBestFit: {rectResult?.Count ?? 0} parts");
if (IsBetterFill(rectResult, best)) if (IsBetterFill(rectResult, best, workArea))
best = rectResult; best = rectResult;
var pairResult = FillWithPairs(nestItem, workArea); var pairResult = FillWithPairs(nestItem, workArea);
Debug.WriteLine($"[Fill(groupParts,Box)] Pair: {pairResult.Count} parts | Winner: {(IsBetterFill(pairResult, best) ? "Pair" : "Linear")}"); Debug.WriteLine($"[Fill(groupParts,Box)] Pair: {pairResult.Count} parts | Winner: {(IsBetterFill(pairResult, best, workArea) ? "Pair" : "Linear")}");
if (IsBetterFill(pairResult, best)) if (IsBetterFill(pairResult, best, workArea))
best = pairResult; best = pairResult;
// Try improving by filling the remainder strip separately. // Try improving by filling the remainder strip separately.
var improved = TryRemainderImprovement(nestItem, workArea, best); var improved = TryRemainderImprovement(nestItem, workArea, best);
if (IsBetterFill(improved, best)) if (IsBetterFill(improved, best, workArea))
{ {
Debug.WriteLine($"[Fill(groupParts,Box)] Remainder improvement: {improved.Count} parts (was {best?.Count ?? 0})"); Debug.WriteLine($"[Fill(groupParts,Box)] Remainder improvement: {improved.Count} parts (was {best?.Count ?? 0})");
best = improved; best = improved;
@@ -205,7 +206,7 @@ namespace OpenNest
var candidates = SelectPairCandidates(bestFits, workArea); var candidates = SelectPairCandidates(bestFits, workArea);
Debug.WriteLine($"[FillWithPairs] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}"); Debug.WriteLine($"[FillWithPairs] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}");
var resultBag = new System.Collections.Concurrent.ConcurrentBag<(int count, List<Part> parts)>(); var resultBag = new System.Collections.Concurrent.ConcurrentBag<(FillScore score, List<Part> parts)>();
System.Threading.Tasks.Parallel.For(0, candidates.Count, i => System.Threading.Tasks.Parallel.For(0, candidates.Count, i =>
{ {
@@ -213,21 +214,25 @@ namespace OpenNest
var pairParts = result.BuildParts(item.Drawing); var pairParts = result.BuildParts(item.Drawing);
var angles = RotationAnalysis.FindHullEdgeAngles(pairParts); var angles = RotationAnalysis.FindHullEdgeAngles(pairParts);
var engine = new FillLinear(workArea, Plate.PartSpacing); var engine = new FillLinear(workArea, Plate.PartSpacing);
var filled = FillPattern(engine, pairParts, angles); var filled = FillPattern(engine, pairParts, angles, workArea);
if (filled != null && filled.Count > 0) if (filled != null && filled.Count > 0)
resultBag.Add((filled.Count, filled)); resultBag.Add((FillScore.Compute(filled, workArea), filled));
}); });
List<Part> best = null; List<Part> best = null;
var bestScore = default(FillScore);
foreach (var (count, parts) in resultBag) foreach (var (score, parts) in resultBag)
{
if (best == null || score > bestScore)
{ {
if (best == null || count > best.Count)
best = parts; best = parts;
bestScore = score;
}
} }
Debug.WriteLine($"[FillWithPairs] Best pair result: {best?.Count ?? 0} parts"); Debug.WriteLine($"[FillWithPairs] Best pair result: {bestScore.Count} parts, remnant={bestScore.UsableRemnantArea:F1}, density={bestScore.Density:P1}");
return best ?? new List<Part>(); return best ?? new List<Part>();
} }
@@ -296,7 +301,7 @@ namespace OpenNest
return false; return false;
} }
private bool IsBetterFill(List<Part> candidate, List<Part> current) private bool IsBetterFill(List<Part> candidate, List<Part> current, Box workArea)
{ {
if (candidate == null || candidate.Count == 0) if (candidate == null || candidate.Count == 0)
return false; return false;
@@ -304,22 +309,15 @@ namespace OpenNest
if (current == null || current.Count == 0) if (current == null || current.Count == 0)
return true; return true;
if (candidate.Count != current.Count) return FillScore.Compute(candidate, workArea) > FillScore.Compute(current, workArea);
return candidate.Count > current.Count;
// Same count: prefer smaller bounding box (more compact).
var candidateBox = ((IEnumerable<IBoundable>)candidate).GetBoundingBox();
var currentBox = ((IEnumerable<IBoundable>)current).GetBoundingBox();
return candidateBox.Area() < currentBox.Area();
} }
private bool IsBetterValidFill(List<Part> candidate, List<Part> current) private bool IsBetterValidFill(List<Part> candidate, List<Part> current, Box workArea)
{ {
if (candidate != null && candidate.Count > 0 && HasOverlaps(candidate, Plate.PartSpacing)) if (candidate != null && candidate.Count > 0 && HasOverlaps(candidate, Plate.PartSpacing))
return false; return false;
return IsBetterFill(candidate, current); return IsBetterFill(candidate, current, workArea);
} }
/// <summary> /// <summary>
@@ -444,12 +442,12 @@ namespace OpenNest
var hResult = TryStripRefill(item, workArea, currentBest, horizontal: true); var hResult = TryStripRefill(item, workArea, currentBest, horizontal: true);
if (IsBetterFill(hResult, best)) if (IsBetterFill(hResult, best, workArea))
best = hResult; best = hResult;
var vResult = TryStripRefill(item, workArea, currentBest, horizontal: false); var vResult = TryStripRefill(item, workArea, currentBest, horizontal: false);
if (IsBetterFill(vResult, best)) if (IsBetterFill(vResult, best, workArea))
best = vResult; best = vResult;
return best; return best;
@@ -475,7 +473,7 @@ namespace OpenNest
return pattern; return pattern;
} }
private List<Part> FillPattern(FillLinear engine, List<Part> groupParts, List<double> angles) private List<Part> FillPattern(FillLinear engine, List<Part> groupParts, List<double> angles, Box workArea)
{ {
List<Part> best = null; List<Part> best = null;
@@ -489,10 +487,10 @@ namespace OpenNest
var h = engine.Fill(pattern, NestDirection.Horizontal); var h = engine.Fill(pattern, NestDirection.Horizontal);
var v = engine.Fill(pattern, NestDirection.Vertical); var v = engine.Fill(pattern, NestDirection.Vertical);
if (IsBetterValidFill(h, best)) if (IsBetterValidFill(h, best, workArea))
best = h; best = h;
if (IsBetterValidFill(v, best)) if (IsBetterValidFill(v, best, workArea))
best = v; best = v;
} }