feat: wire IFillComparer through PairFiller and StripeFiller

PairFiller now accepts an optional IFillComparer (defaulting to
DefaultFillComparer) and uses it in EvaluateCandidates and
EvaluateCandidate/FillPattern instead of raw FillScore comparisons.
PairsFillStrategy passes context.Policy?.Comparer through.
StripeFiller derives _comparer from FillContext.Policy in its
constructor and uses it in Fill() instead of FillScore comparisons.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 12:53:28 -04:00
parent 24beb8ada1
commit 83124eb38d
3 changed files with 15 additions and 14 deletions
+8 -8
View File
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using OpenNest.Engine;
namespace OpenNest.Engine.Fill namespace OpenNest.Engine.Fill
{ {
@@ -29,11 +30,13 @@ namespace OpenNest.Engine.Fill
private readonly Size plateSize; private readonly Size plateSize;
private readonly double partSpacing; private readonly double partSpacing;
private readonly IFillComparer comparer;
public PairFiller(Size plateSize, double partSpacing) public PairFiller(Size plateSize, double partSpacing, IFillComparer comparer = null)
{ {
this.plateSize = plateSize; this.plateSize = plateSize;
this.partSpacing = partSpacing; this.partSpacing = partSpacing;
this.comparer = comparer ?? new DefaultFillComparer();
} }
public PairFillResult Fill(NestItem item, Box workArea, public PairFillResult Fill(NestItem item, Box workArea,
@@ -61,7 +64,6 @@ namespace OpenNest.Engine.Fill
int plateNumber, CancellationToken token, IProgress<NestProgress> progress) int plateNumber, CancellationToken token, IProgress<NestProgress> progress)
{ {
List<Part> best = null; List<Part> best = null;
var bestScore = default(FillScore);
var sinceImproved = 0; var sinceImproved = 0;
var effectiveWorkArea = workArea; var effectiveWorkArea = workArea;
@@ -72,12 +74,10 @@ namespace OpenNest.Engine.Fill
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
var filled = EvaluateCandidate(candidates[i], drawing, effectiveWorkArea); var filled = EvaluateCandidate(candidates[i], drawing, effectiveWorkArea);
var score = FillScore.Compute(filled, effectiveWorkArea);
if (score > bestScore) if (comparer.IsBetter(filled, best, effectiveWorkArea))
{ {
best = filled; best = filled;
bestScore = score;
sinceImproved = 0; sinceImproved = 0;
effectiveWorkArea = TryReduceWorkArea(filled, targetCount, workArea, effectiveWorkArea); effectiveWorkArea = TryReduceWorkArea(filled, targetCount, workArea, effectiveWorkArea);
} }
@@ -87,7 +87,7 @@ namespace OpenNest.Engine.Fill
} }
NestEngineBase.ReportProgress(progress, NestPhase.Pairs, plateNumber, best, workArea, NestEngineBase.ReportProgress(progress, NestPhase.Pairs, plateNumber, best, workArea,
$"Pairs: {i + 1}/{candidates.Count} candidates, best = {bestScore.Count} parts"); $"Pairs: {i + 1}/{candidates.Count} candidates, best = {best?.Count ?? 0} parts");
if (i + 1 >= EarlyExitMinTried && sinceImproved >= EarlyExitStaleLimit) if (i + 1 >= EarlyExitMinTried && sinceImproved >= EarlyExitStaleLimit)
{ {
@@ -101,7 +101,7 @@ namespace OpenNest.Engine.Fill
Debug.WriteLine("[PairFiller] Cancelled mid-phase, using results so far"); Debug.WriteLine("[PairFiller] Cancelled mid-phase, using results so far");
} }
Debug.WriteLine($"[PairFiller] Best pair result: {bestScore.Count} parts, density={bestScore.Density:P1}"); Debug.WriteLine($"[PairFiller] Best pair result: {best?.Count ?? 0} parts");
return best ?? new List<Part>(); return best ?? new List<Part>();
} }
@@ -147,7 +147,7 @@ namespace OpenNest.Engine.Fill
var pairParts = candidate.BuildParts(drawing); var pairParts = candidate.BuildParts(drawing);
var engine = new FillLinear(workArea, partSpacing); var engine = new FillLinear(workArea, partSpacing);
var angles = BuildTilingAngles(candidate); var angles = BuildTilingAngles(candidate);
return FillHelpers.FillPattern(engine, pairParts, angles, workArea); return FillHelpers.FillPattern(engine, pairParts, angles, workArea, comparer);
} }
private static List<double> BuildTilingAngles(BestFitResult candidate) private static List<double> BuildTilingAngles(BestFitResult candidate)
+5 -5
View File
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using OpenNest.Engine;
using OpenNest.Engine.BestFit; using OpenNest.Engine.BestFit;
using OpenNest.Engine.Strategies; using OpenNest.Engine.Strategies;
using OpenNest.Geometry; using OpenNest.Geometry;
@@ -18,6 +19,7 @@ public class StripeFiller
private readonly FillContext _context; private readonly FillContext _context;
private readonly NestDirection _primaryAxis; private readonly NestDirection _primaryAxis;
private readonly IFillComparer _comparer;
/// <summary> /// <summary>
/// When true, only complete stripes are placed — no partial rows/columns. /// When true, only complete stripes are placed — no partial rows/columns.
@@ -35,6 +37,7 @@ public class StripeFiller
{ {
_context = context; _context = context;
_primaryAxis = primaryAxis; _primaryAxis = primaryAxis;
_comparer = context.Policy?.Comparer ?? new DefaultFillComparer();
} }
public List<Part> Fill() public List<Part> Fill()
@@ -49,7 +52,6 @@ public class StripeFiller
var strategyName = _primaryAxis == NestDirection.Horizontal ? "Row" : "Column"; var strategyName = _primaryAxis == NestDirection.Horizontal ? "Row" : "Column";
List<Part> bestParts = null; List<Part> bestParts = null;
var bestScore = default(FillScore);
for (var i = 0; i < bestFits.Count; i++) for (var i = 0; i < bestFits.Count; i++)
{ {
@@ -82,22 +84,20 @@ public class StripeFiller
if (result == null || result.Count == 0) if (result == null || result.Count == 0)
continue; continue;
var score = FillScore.Compute(result, workArea);
Debug.WriteLine($"[StripeFiller] {strategyName} candidate {i} {dirLabel}: " + Debug.WriteLine($"[StripeFiller] {strategyName} candidate {i} {dirLabel}: " +
$"angle={Angle.ToDegrees(angle):F1}°, N={count}, waste={waste:F2}, " + $"angle={Angle.ToDegrees(angle):F1}°, N={count}, waste={waste:F2}, " +
$"grid={result.Count} parts"); $"grid={result.Count} parts");
if (bestParts == null || score > bestScore) if (_comparer.IsBetter(result, bestParts, workArea))
{ {
bestParts = result; bestParts = result;
bestScore = score;
} }
} }
} }
NestEngineBase.ReportProgress(_context.Progress, NestPhase.Custom, NestEngineBase.ReportProgress(_context.Progress, NestPhase.Custom,
_context.PlateNumber, bestParts, workArea, _context.PlateNumber, bestParts, workArea,
$"{strategyName}: {i + 1}/{bestFits.Count} pairs, best = {bestScore.Count} parts"); $"{strategyName}: {i + 1}/{bestFits.Count} pairs, best = {bestParts?.Count ?? 0} parts");
} }
return bestParts ?? new List<Part>(); return bestParts ?? new List<Part>();
@@ -11,7 +11,8 @@ namespace OpenNest.Engine.Strategies
public List<Part> Fill(FillContext context) public List<Part> Fill(FillContext context)
{ {
var filler = new PairFiller(context.Plate.Size, context.Plate.PartSpacing); var comparer = context.Policy?.Comparer;
var filler = new PairFiller(context.Plate.Size, context.Plate.PartSpacing, comparer);
var result = filler.Fill(context.Item, context.WorkArea, var result = filler.Fill(context.Item, context.WorkArea,
context.PlateNumber, context.Token, context.Progress); context.PlateNumber, context.Token, context.Progress);