fix(engine): use RemnantFinder for iterative remnant filling in StripNestEngine

Replace the single-pass guillotine split approach with RemnantFinder-based
iteration. After each fill, re-discover all free rectangles and try all
remaining items again until no more progress is made. This fills gaps that
were previously left empty after the initial strip + remainder layout.

Also change ActiveWorkArea border color from orange to red.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-16 13:11:28 -04:00
parent 51b482aefb
commit 3d23943b69
2 changed files with 43 additions and 65 deletions

View File

@@ -248,31 +248,40 @@ namespace OpenNest
}) })
.ToList(); .ToList();
// Fill remnant with remainder items using free-rectangle tracking. // Fill remnant areas iteratively using RemnantFinder.
// After each fill, the consumed box is split into two non-overlapping // After each fill, re-discover all free rectangles and try again
// sub-rectangles (guillotine cut) so no usable area is lost. // until no more items can be placed.
if (remnantBox.Width > 0 && remnantBox.Length > 0) if (remnantBox.Width > 0 && remnantBox.Length > 0)
{ {
var freeBoxes = new List<Box> { remnantBox };
var remnantProgress = progress != null var remnantProgress = progress != null
? new AccumulatingProgress(progress, allParts) ? new AccumulatingProgress(progress, allParts)
: null; : null;
var obstacles = allParts.Select(p => p.BoundingBox.Offset(spacing)).ToList();
var finder = new RemnantFinder(workArea, obstacles);
var madeProgress = true;
while (madeProgress && !token.IsCancellationRequested)
{
madeProgress = false;
var freeBoxes = finder.FindRemnants(spacing);
if (freeBoxes.Count == 0)
break;
foreach (var item in effectiveRemainder) foreach (var item in effectiveRemainder)
{ {
if (token.IsCancellationRequested || freeBoxes.Count == 0) if (token.IsCancellationRequested)
break; break;
if (item.Quantity == 0)
continue;
var itemBbox = item.Drawing.Program.BoundingBox(); var itemBbox = item.Drawing.Program.BoundingBox();
var minItemDim = System.Math.Min(itemBbox.Width, itemBbox.Length); var minItemDim = System.Math.Min(itemBbox.Width, itemBbox.Length);
// Try free boxes from largest to smallest. foreach (var box in freeBoxes)
freeBoxes.Sort((a, b) => b.Area().CompareTo(a.Area()));
for (var i = 0; i < freeBoxes.Count; i++)
{ {
var box = freeBoxes[i];
if (System.Math.Min(box.Width, box.Length) < minItemDim) if (System.Math.Min(box.Width, box.Length) < minItemDim)
continue; continue;
@@ -284,13 +293,20 @@ namespace OpenNest
if (remnantParts != null && remnantParts.Count > 0) if (remnantParts != null && remnantParts.Count > 0)
{ {
allParts.AddRange(remnantParts); allParts.AddRange(remnantParts);
freeBoxes.RemoveAt(i); item.Quantity = System.Math.Max(0, item.Quantity - remnantParts.Count);
var usedBox = remnantParts.Cast<IBoundable>().GetBoundingBox(); // Update obstacles and re-discover remnants
SplitFreeBox(box, usedBox, spacing, freeBoxes); foreach (var p in remnantParts)
break; 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
}
} }
} }
@@ -303,44 +319,6 @@ namespace OpenNest
return result; return result;
} }
private static void SplitFreeBox(Box parent, Box used, double spacing, List<Box> freeBoxes)
{
var hWidth = parent.Right - used.Right - spacing;
var vHeight = parent.Top - used.Top - spacing;
if (hWidth > spacing && vHeight > spacing)
{
// Guillotine split: give the overlapping corner to the larger strip.
var hFullArea = hWidth * parent.Length;
var vFullArea = parent.Width * vHeight;
if (hFullArea >= vFullArea)
{
// hStrip gets full height; vStrip truncated to left of split line.
freeBoxes.Add(new Box(used.Right + spacing, parent.Y, hWidth, parent.Length));
var vWidth = used.Right + spacing - parent.X;
if (vWidth > spacing)
freeBoxes.Add(new Box(parent.X, used.Top + spacing, vWidth, vHeight));
}
else
{
// vStrip gets full width; hStrip truncated below split line.
freeBoxes.Add(new Box(parent.X, used.Top + spacing, parent.Width, vHeight));
var hHeight = used.Top + spacing - parent.Y;
if (hHeight > spacing)
freeBoxes.Add(new Box(used.Right + spacing, parent.Y, hWidth, hHeight));
}
}
else if (hWidth > spacing)
{
freeBoxes.Add(new Box(used.Right + spacing, parent.Y, hWidth, parent.Length));
}
else if (vHeight > spacing)
{
freeBoxes.Add(new Box(parent.X, used.Top + spacing, parent.Width, vHeight));
}
}
/// <summary> /// <summary>
/// Wraps an IProgress to prepend previously placed parts to each report, /// Wraps an IProgress to prepend previously placed parts to each report,
/// so the UI shows the full picture (strip + remnant) during remnant fills. /// so the UI shows the full picture (strip + remnant) during remnant fills.

View File

@@ -625,9 +625,9 @@ namespace OpenNest.Controls
}; };
rect.Y -= rect.Height; rect.Y -= rect.Height;
using var pen = new Pen(Color.Orange, 2f) using var pen = new Pen(Color.Red, 1f)
{ {
DashStyle = DashStyle.Dash DashStyle = DashStyle.Dot
}; };
g.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height); g.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height);
} }