From 0472c12113d2ad6673afd60dac34ee96c43fb1af Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 22:48:12 -0400 Subject: [PATCH] refactor(fill): extract constants and EvaluateCandidate in PairFiller Extract magic numbers into named constants (MaxTopCandidates, EarlyExitMinTried, etc.), extract candidate evaluation into EvaluateCandidate method, and expose BestFits property so PairsFillStrategy can reuse without redundant BestFitCache call. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Engine/Fill/PairFiller.cs | 52 ++++++++++++------- .../Strategies/PairsFillStrategy.cs | 7 +-- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/OpenNest.Engine/Fill/PairFiller.cs b/OpenNest.Engine/Fill/PairFiller.cs index 0d34f68..1a2b2fe 100644 --- a/OpenNest.Engine/Fill/PairFiller.cs +++ b/OpenNest.Engine/Fill/PairFiller.cs @@ -12,13 +12,24 @@ namespace OpenNest.Engine.Fill { /// /// Fills a work area using interlocking part pairs from BestFitCache. - /// Extracted from DefaultNestEngine.FillWithPairs. /// public class PairFiller { + private const int MaxTopCandidates = 50; + private const int MaxStripCandidates = 100; + private const double MinStripUtilization = 0.3; + private const int EarlyExitMinTried = 10; + private const int EarlyExitStaleLimit = 10; + private readonly Size plateSize; private readonly double partSpacing; + /// + /// The best-fit results computed during the last Fill call. + /// Available after Fill returns so callers can reuse without recomputing. + /// + public List BestFits { get; private set; } + public PairFiller(Size plateSize, double partSpacing) { this.plateSize = plateSize; @@ -30,11 +41,11 @@ namespace OpenNest.Engine.Fill CancellationToken token = default, IProgress progress = null) { - var bestFits = BestFitCache.GetOrCompute( + BestFits = BestFitCache.GetOrCompute( item.Drawing, plateSize.Length, plateSize.Width, partSpacing); - var candidates = SelectPairCandidates(bestFits, workArea); - Debug.WriteLine($"[PairFiller] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}"); + var candidates = SelectPairCandidates(BestFits, workArea); + Debug.WriteLine($"[PairFiller] Total: {BestFits.Count}, Kept: {BestFits.Count(r => r.Keep)}, Trying: {candidates.Count}"); Debug.WriteLine($"[PairFiller] Plate: {plateSize.Length:F2}x{plateSize.Width:F2}, WorkArea: {workArea.Width:F2}x{workArea.Length:F2}"); List best = null; @@ -47,17 +58,7 @@ namespace OpenNest.Engine.Fill { token.ThrowIfCancellationRequested(); - var result = candidates[i]; - var pairParts = result.BuildParts(item.Drawing); - var angles = result.HullAngles; - var engine = new FillLinear(workArea, partSpacing); - - // Let the remainder strip try pair-based filling too. - var p0 = FillHelpers.BuildRotatedPattern(pairParts, 0); - var p90 = FillHelpers.BuildRotatedPattern(pairParts, Angle.HalfPI); - engine.RemainderPatterns = new List { p0, p90 }; - - var filled = FillHelpers.FillPattern(engine, pairParts, angles, workArea); + var filled = EvaluateCandidate(candidates[i], item.Drawing, workArea); if (filled != null && filled.Count > 0) { @@ -81,8 +82,7 @@ namespace OpenNest.Engine.Fill NestEngineBase.ReportProgress(progress, NestPhase.Pairs, plateNumber, best, workArea, $"Pairs: {i + 1}/{candidates.Count} candidates, best = {bestScore.Count} parts"); - // Early exit: stop if we've tried enough candidates without improvement. - if (i >= 9 && sinceImproved >= 10) + if (i + 1 >= EarlyExitMinTried && sinceImproved >= EarlyExitStaleLimit) { Debug.WriteLine($"[PairFiller] Early exit at {i + 1}/{candidates.Count} — no improvement in last {sinceImproved} candidates"); break; @@ -98,10 +98,22 @@ namespace OpenNest.Engine.Fill return best ?? new List(); } + private List EvaluateCandidate(BestFitResult candidate, Drawing drawing, Box workArea) + { + var pairParts = candidate.BuildParts(drawing); + var engine = new FillLinear(workArea, partSpacing); + + var p0 = FillHelpers.BuildRotatedPattern(pairParts, 0); + var p90 = FillHelpers.BuildRotatedPattern(pairParts, Angle.HalfPI); + engine.RemainderPatterns = new List { p0, p90 }; + + return FillHelpers.FillPattern(engine, pairParts, candidate.HullAngles, workArea); + } + private List SelectPairCandidates(List bestFits, Box workArea) { var kept = bestFits.Where(r => r.Keep).ToList(); - var top = kept.Take(50).ToList(); + var top = kept.Take(MaxTopCandidates).ToList(); var workShortSide = System.Math.Min(workArea.Width, workArea.Length); var plateShortSide = System.Math.Min(plateSize.Width, plateSize.Length); @@ -110,14 +122,14 @@ namespace OpenNest.Engine.Fill { var stripCandidates = bestFits .Where(r => r.ShortestSide <= workShortSide + Tolerance.Epsilon - && r.Utilization >= 0.3) + && r.Utilization >= MinStripUtilization) .OrderByDescending(r => r.Utilization); var existing = new HashSet(top); foreach (var r in stripCandidates) { - if (top.Count >= 100) + if (top.Count >= MaxStripCandidates) break; if (existing.Add(r)) diff --git a/OpenNest.Engine/Strategies/PairsFillStrategy.cs b/OpenNest.Engine/Strategies/PairsFillStrategy.cs index 79c0428..118c732 100644 --- a/OpenNest.Engine/Strategies/PairsFillStrategy.cs +++ b/OpenNest.Engine/Strategies/PairsFillStrategy.cs @@ -1,4 +1,3 @@ -using OpenNest.Engine.BestFit; using OpenNest.Engine.Fill; using System.Collections.Generic; @@ -16,11 +15,7 @@ namespace OpenNest.Engine.Strategies var result = filler.Fill(context.Item, context.WorkArea, context.PlateNumber, context.Token, context.Progress); - // Cache hit — PairFiller already called GetOrCompute internally. - var bestFits = BestFitCache.GetOrCompute( - context.Item.Drawing, context.Plate.Size.Length, - context.Plate.Size.Width, context.Plate.PartSpacing); - context.SharedState["BestFits"] = bestFits; + context.SharedState["BestFits"] = filler.BestFits; return result; }