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) <noreply@anthropic.com>
This commit is contained in:
2026-03-16 13:50:14 -04:00
parent a41a08c9af
commit 00ccf82196

View File

@@ -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;
}
/// <summary>
/// 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.
/// </summary>
private List<Part> ShrinkFill(NestItem item, Box box,
IProgress<NestProgress> 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<IBoundable>().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<IBoundable>().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<IBoundable>().GetBoundingBox();
shrunkHeight = trialPlacedBox.Top - box.Y;
}
return bestParts;
}
/// <summary>
/// Wraps an IProgress to prepend previously placed parts to each report,
/// so the UI shows the full picture (strip + remnant) during remnant fills.