From 00ccf82196022902e9ec5f30eea77617d0036299 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 16 Mar 2026 13:50:14 -0400 Subject: [PATCH] fix(engine): apply shrink loop to remnant fills in StripNestEngine Remainder items were being filled into the full remnant box without compaction. Added ShrinkFill helper that fills then shrinks the box horizontally and vertically while maintaining the same part count. This matches the strip item's shrink behavior and produces tighter layouts that leave more usable space for subsequent items. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Engine/StripNestEngine.cs | 70 +++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/OpenNest.Engine/StripNestEngine.cs b/OpenNest.Engine/StripNestEngine.cs index 6ad04ca..283a145 100644 --- a/OpenNest.Engine/StripNestEngine.cs +++ b/OpenNest.Engine/StripNestEngine.cs @@ -292,8 +292,7 @@ namespace OpenNest if (System.Math.Min(box.Width, box.Length) < minItemDim) continue; - var remnantInner = new DefaultNestEngine(Plate); - var remnantParts = remnantInner.Fill( + var remnantParts = ShrinkFill( new NestItem { Drawing = item.Drawing, Quantity = qty }, box, remnantProgress, token); @@ -326,6 +325,73 @@ 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++) + { + if (token.IsCancellationRequested) + break; + + var trialWidth = shrunkWidth - Plate.PartSpacing; + if (trialWidth <= 0) + break; + + 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; + } + /// /// Wraps an IProgress to prepend previously placed parts to each report, /// so the UI shows the full picture (strip + remnant) during remnant fills.