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:
@@ -248,48 +248,64 @@ 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;
|
||||||
|
|
||||||
foreach (var item in effectiveRemainder)
|
var obstacles = allParts.Select(p => p.BoundingBox.Offset(spacing)).ToList();
|
||||||
|
var finder = new RemnantFinder(workArea, obstacles);
|
||||||
|
var madeProgress = true;
|
||||||
|
|
||||||
|
while (madeProgress && !token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
if (token.IsCancellationRequested || freeBoxes.Count == 0)
|
madeProgress = false;
|
||||||
|
var freeBoxes = finder.FindRemnants(spacing);
|
||||||
|
|
||||||
|
if (freeBoxes.Count == 0)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var itemBbox = item.Drawing.Program.BoundingBox();
|
foreach (var item in effectiveRemainder)
|
||||||
var minItemDim = System.Math.Min(itemBbox.Width, itemBbox.Length);
|
|
||||||
|
|
||||||
// Try free boxes from largest to smallest.
|
|
||||||
freeBoxes.Sort((a, b) => b.Area().CompareTo(a.Area()));
|
|
||||||
|
|
||||||
for (var i = 0; i < freeBoxes.Count; i++)
|
|
||||||
{
|
{
|
||||||
var box = freeBoxes[i];
|
if (token.IsCancellationRequested)
|
||||||
|
break;
|
||||||
|
|
||||||
if (System.Math.Min(box.Width, box.Length) < minItemDim)
|
if (item.Quantity == 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var remnantInner = new DefaultNestEngine(Plate);
|
var itemBbox = item.Drawing.Program.BoundingBox();
|
||||||
var remnantParts = remnantInner.Fill(
|
var minItemDim = System.Math.Min(itemBbox.Width, itemBbox.Length);
|
||||||
new NestItem { Drawing = item.Drawing, Quantity = item.Quantity },
|
|
||||||
box, remnantProgress, token);
|
|
||||||
|
|
||||||
if (remnantParts != null && remnantParts.Count > 0)
|
foreach (var box in freeBoxes)
|
||||||
{
|
{
|
||||||
allParts.AddRange(remnantParts);
|
if (System.Math.Min(box.Width, box.Length) < minItemDim)
|
||||||
freeBoxes.RemoveAt(i);
|
continue;
|
||||||
|
|
||||||
var usedBox = remnantParts.Cast<IBoundable>().GetBoundingBox();
|
var remnantInner = new DefaultNestEngine(Plate);
|
||||||
SplitFreeBox(box, usedBox, spacing, freeBoxes);
|
var remnantParts = remnantInner.Fill(
|
||||||
break;
|
new NestItem { Drawing = item.Drawing, Quantity = item.Quantity },
|
||||||
|
box, remnantProgress, token);
|
||||||
|
|
||||||
|
if (remnantParts != null && remnantParts.Count > 0)
|
||||||
|
{
|
||||||
|
allParts.AddRange(remnantParts);
|
||||||
|
item.Quantity = System.Math.Max(0, item.Quantity - 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user