From 75cb6b2bac7193c13edddd6890da595dad9ed6e8 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 16 Mar 2026 22:37:59 -0400 Subject: [PATCH] refactor(engine): rewire StripNestEngine to use extracted helpers Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Engine/StripNestEngine.cs | 241 ++++------------------------- 1 file changed, 32 insertions(+), 209 deletions(-) diff --git a/OpenNest.Engine/StripNestEngine.cs b/OpenNest.Engine/StripNestEngine.cs index 6e21279..6d8b336 100644 --- a/OpenNest.Engine/StripNestEngine.cs +++ b/OpenNest.Engine/StripNestEngine.cs @@ -9,8 +9,6 @@ namespace OpenNest { public class StripNestEngine : NestEngineBase { - private const int MaxShrinkIterations = 20; - public StripNestEngine(Plate plate) : base(plate) { } @@ -165,54 +163,25 @@ namespace OpenNest ? new Box(workArea.X, workArea.Y, workArea.Width, estimatedDim) : new Box(workArea.X, workArea.Y, estimatedDim, workArea.Length); - // Initial fill using DefaultNestEngine (composition, not inheritance). - var inner = new DefaultNestEngine(Plate); - var stripParts = inner.Fill( - new NestItem { Drawing = stripItem.Drawing, Quantity = stripItem.Quantity }, - stripBox, progress, token); + // Shrink to tightest strip. + var shrinkAxis = direction == StripDirection.Bottom + ? ShrinkAxis.Height : ShrinkAxis.Width; - if (stripParts == null || stripParts.Count == 0) + Func> stripFill = (ni, b) => + { + var trialInner = new DefaultNestEngine(Plate); + return trialInner.Fill(ni, b, progress, token); + }; + + var shrinkResult = ShrinkFiller.Shrink(stripFill, + new NestItem { Drawing = stripItem.Drawing, Quantity = stripItem.Quantity }, + stripBox, Plate.PartSpacing, shrinkAxis, token); + + if (shrinkResult.Parts == null || shrinkResult.Parts.Count == 0) return result; - // Measure actual strip dimension from placed parts. - var placedBox = stripParts.Cast().GetBoundingBox(); - var actualDim = direction == StripDirection.Bottom - ? placedBox.Top - workArea.Y - : placedBox.Right - workArea.X; - - var bestParts = stripParts; - var bestDim = actualDim; - var targetCount = stripParts.Count; - - // Shrink loop: reduce strip dimension by PartSpacing until count drops. - for (var i = 0; i < MaxShrinkIterations; i++) - { - if (token.IsCancellationRequested) - break; - - var trialDim = bestDim - Plate.PartSpacing; - if (trialDim <= 0) - break; - - var trialBox = direction == StripDirection.Bottom - ? new Box(workArea.X, workArea.Y, workArea.Width, trialDim) - : new Box(workArea.X, workArea.Y, trialDim, workArea.Length); - - var trialInner = new DefaultNestEngine(Plate); - var trialParts = trialInner.Fill( - new NestItem { Drawing = stripItem.Drawing, Quantity = stripItem.Quantity }, - trialBox, progress, token); - - if (trialParts == null || trialParts.Count < targetCount) - break; - - // Same count in a tighter strip — keep going. - bestParts = trialParts; - var trialPlacedBox = trialParts.Cast().GetBoundingBox(); - bestDim = direction == StripDirection.Bottom - ? trialPlacedBox.Top - workArea.Y - : trialPlacedBox.Right - workArea.X; - } + var bestParts = shrinkResult.Parts; + var bestDim = shrinkResult.Dimension; // TODO: Compact strip parts individually to close geometry-based gaps. // Disabled pending investigation — remnant finder picks up gaps created @@ -258,88 +227,23 @@ namespace OpenNest }) .ToList(); - // Fill remnant areas iteratively using RemnantFinder. - // After each fill, re-discover all free rectangles and try again - // until no more items can be placed. + // Fill remnants if (remnantBox.Width > 0 && remnantBox.Length > 0) { var remnantProgress = progress != null ? new AccumulatingProgress(progress, allParts) - : null; + : (IProgress)null; - var obstacles = allParts.Select(p => p.BoundingBox.Offset(spacing)).ToList(); - var finder = new RemnantFinder(workArea, obstacles); - var madeProgress = true; + var remnantFiller = new RemnantFiller(workArea, spacing); + remnantFiller.AddObstacles(allParts); - // Track quantities locally so we don't mutate the shared NestItem objects. - // TryOrientation is called twice (bottom, left) with the same items. - var localQty = new Dictionary(); - foreach (var item in effectiveRemainder) - localQty[item.Drawing.Name] = item.Quantity; + Func> remnantFillFunc = (ni, b) => + ShrinkFill(ni, b, remnantProgress, token); - while (madeProgress && !token.IsCancellationRequested) - { - madeProgress = false; + var additional = remnantFiller.FillItems(effectiveRemainder, + remnantFillFunc, token, remnantProgress); - // Minimum remnant size = smallest remaining part dimension - var minRemnantDim = double.MaxValue; - foreach (var item in effectiveRemainder) - { - if (localQty[item.Drawing.Name] <= 0) - continue; - var bb = item.Drawing.Program.BoundingBox(); - var dim = System.Math.Min(bb.Width, bb.Length); - if (dim < minRemnantDim) - minRemnantDim = dim; - } - - if (minRemnantDim == double.MaxValue) - break; // No items with remaining quantity - - var freeBoxes = finder.FindRemnants(minRemnantDim); - - if (freeBoxes.Count == 0) - break; - - foreach (var item in effectiveRemainder) - { - if (token.IsCancellationRequested) - break; - - var qty = localQty[item.Drawing.Name]; - if (qty == 0) - continue; - - var itemBbox = item.Drawing.Program.BoundingBox(); - var minItemDim = System.Math.Min(itemBbox.Width, itemBbox.Length); - - foreach (var box in freeBoxes) - { - if (System.Math.Min(box.Width, box.Length) < minItemDim) - continue; - - var remnantParts = ShrinkFill( - new NestItem { Drawing = item.Drawing, Quantity = qty }, - box, remnantProgress, token); - - if (remnantParts != null && remnantParts.Count > 0) - { - allParts.AddRange(remnantParts); - localQty[item.Drawing.Name] = System.Math.Max(0, qty - remnantParts.Count); - - // Update obstacles and re-discover remnants - foreach (var p in remnantParts) - finder.AddObstacle(p.BoundingBox.Offset(spacing)); - - madeProgress = true; - break; // Re-discover free boxes with updated obstacles - } - } - - if (madeProgress) - break; // Restart the outer loop to re-discover remnants - } - } + allParts.AddRange(additional); } result.Parts = allParts; @@ -351,101 +255,20 @@ namespace OpenNest return result; } - /// - /// Fill a box and then shrink it to the tightest area that still fits - /// the same number of parts. This maximizes leftover space for subsequent fills. - /// private List ShrinkFill(NestItem item, Box box, IProgress progress, CancellationToken token) { - var inner = new DefaultNestEngine(Plate); - var parts = inner.Fill(item, box, progress, token); - - if (parts == null || parts.Count < 2) - return parts; - - var targetCount = parts.Count; - var placedBox = parts.Cast().GetBoundingBox(); - - // Try shrinking horizontally - var bestParts = parts; - var shrunkWidth = placedBox.Right - box.X; - var shrunkHeight = placedBox.Top - box.Y; - - for (var i = 0; i < MaxShrinkIterations; i++) + Func> fillFunc = (ni, b) => { - if (token.IsCancellationRequested) - break; + var inner = new DefaultNestEngine(Plate); + return inner.Fill(ni, b, null, token); + }; - var trialWidth = shrunkWidth - Plate.PartSpacing; - if (trialWidth <= 0) - break; + var heightResult = ShrinkFiller.Shrink(fillFunc, item, box, + Plate.PartSpacing, ShrinkAxis.Height, token); - var trialBox = new Box(box.X, box.Y, trialWidth, box.Length); - var trialInner = new DefaultNestEngine(Plate); - var trialParts = trialInner.Fill(item, trialBox, null, token); - - if (trialParts == null || trialParts.Count < targetCount) - break; - - bestParts = trialParts; - var trialPlacedBox = trialParts.Cast().GetBoundingBox(); - shrunkWidth = trialPlacedBox.Right - box.X; - } - - // Try shrinking vertically - for (var i = 0; i < MaxShrinkIterations; i++) - { - if (token.IsCancellationRequested) - break; - - var trialHeight = shrunkHeight - Plate.PartSpacing; - if (trialHeight <= 0) - break; - - var trialBox = new Box(box.X, box.Y, box.Width, trialHeight); - var trialInner = new DefaultNestEngine(Plate); - var trialParts = trialInner.Fill(item, trialBox, null, token); - - if (trialParts == null || trialParts.Count < targetCount) - break; - - bestParts = trialParts; - var trialPlacedBox = trialParts.Cast().GetBoundingBox(); - shrunkHeight = trialPlacedBox.Top - box.Y; - } - - return bestParts; + return heightResult.Parts; } - /// - /// Wraps an IProgress to prepend previously placed parts to each report, - /// so the UI shows the full picture (strip + remnant) during remnant fills. - /// - private class AccumulatingProgress : IProgress - { - private readonly IProgress inner; - private readonly List previousParts; - - public AccumulatingProgress(IProgress inner, List previousParts) - { - this.inner = inner; - this.previousParts = previousParts; - } - - public void Report(NestProgress value) - { - if (value.BestParts != null && previousParts.Count > 0) - { - var combined = new List(previousParts.Count + value.BestParts.Count); - combined.AddRange(previousParts); - combined.AddRange(value.BestParts); - value.BestParts = combined; - value.BestPartCount = combined.Count; - } - - inner.Report(value); - } - } } }