fix: recalculate remnants after each fill to prevent overlaps
The consolidation pass was iterating stale remnant lists after placing parts, causing overlapping placements. Now recalculates remnants from the plate after each fill operation. Also added plate options to the real nest file integration test. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -248,19 +248,25 @@ namespace OpenNest
|
|||||||
if (token.IsCancellationRequested)
|
if (token.IsCancellationRequested)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// Repeatedly find the largest remnant and pack into it.
|
||||||
|
// Recalculate after each fill to avoid overlapping stale remnants.
|
||||||
|
var anyPlacedOnThis = true;
|
||||||
|
while (anyPlacedOnThis && !token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
anyPlacedOnThis = false;
|
||||||
|
|
||||||
var remaining = leftovers.Where(i => i.Quantity > 0).ToList();
|
var remaining = leftovers.Where(i => i.Quantity > 0).ToList();
|
||||||
if (remaining.Count == 0)
|
if (remaining.Count == 0)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var finder = RemnantFinder.FromPlate(pr.Plate);
|
var finder = RemnantFinder.FromPlate(pr.Plate);
|
||||||
var remnants = finder.FindRemnants();
|
var remnants = finder.FindRemnants();
|
||||||
|
if (remnants.Count == 0)
|
||||||
foreach (var remnant in remnants)
|
|
||||||
{
|
|
||||||
remaining = remaining.Where(i => i.Quantity > 0).ToList();
|
|
||||||
if (remaining.Count == 0)
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// Try the largest remnant.
|
||||||
|
var remnant = remnants[0];
|
||||||
|
|
||||||
var engine = NestEngineRegistry.Create(pr.Plate);
|
var engine = NestEngineRegistry.Create(pr.Plate);
|
||||||
var cloned = remaining.Select(CloneItem).ToList();
|
var cloned = remaining.Select(CloneItem).ToList();
|
||||||
var parts = engine.PackArea(remnant, cloned, progress, token);
|
var parts = engine.PackArea(remnant, cloned, progress, token);
|
||||||
@@ -269,8 +275,8 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
pr.Plate.Parts.AddRange(parts);
|
pr.Plate.Parts.AddRange(parts);
|
||||||
pr.Parts.AddRange(parts);
|
pr.Parts.AddRange(parts);
|
||||||
|
anyPlacedOnThis = true;
|
||||||
|
|
||||||
// Deduct placed quantities from originals.
|
|
||||||
foreach (var item in remaining)
|
foreach (var item in remaining)
|
||||||
{
|
{
|
||||||
var placed = parts.Count(p => p.BaseDrawing.Name == item.Drawing.Name);
|
var placed = parts.Count(p => p.BaseDrawing.Name == item.Drawing.Name);
|
||||||
@@ -292,22 +298,23 @@ namespace OpenNest
|
|||||||
var anyPlacedOnPlate = false;
|
var anyPlacedOnPlate = false;
|
||||||
|
|
||||||
// Fill each leftover drawing onto this plate.
|
// Fill each leftover drawing onto this plate.
|
||||||
|
// Recalculate remnants after each fill to avoid overlaps.
|
||||||
foreach (var item in leftovers)
|
foreach (var item in leftovers)
|
||||||
{
|
{
|
||||||
if (item.Quantity <= 0 || token.IsCancellationRequested)
|
if (item.Quantity <= 0 || token.IsCancellationRequested)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Find remaining space on the plate.
|
// Find remaining space on the plate (recalculated each item).
|
||||||
var finder = RemnantFinder.FromPlate(plate);
|
|
||||||
var remnants = allParts.Count == 0
|
var remnants = allParts.Count == 0
|
||||||
? new List<Box> { plate.WorkArea() }
|
? new List<Box> { plate.WorkArea() }
|
||||||
: finder.FindRemnants();
|
: RemnantFinder.FromPlate(plate).FindRemnants();
|
||||||
|
|
||||||
foreach (var remnant in remnants)
|
if (remnants.Count == 0)
|
||||||
{
|
|
||||||
if (item.Quantity <= 0)
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// Use only the largest remnant to avoid stale overlap issues.
|
||||||
|
var remnant = remnants[0];
|
||||||
|
|
||||||
var engine = NestEngineRegistry.Create(plate);
|
var engine = NestEngineRegistry.Create(plate);
|
||||||
var clonedItem = CloneItem(item);
|
var clonedItem = CloneItem(item);
|
||||||
var parts = engine.Fill(clonedItem, remnant, progress, token);
|
var parts = engine.Fill(clonedItem, remnant, progress, token);
|
||||||
@@ -320,7 +327,6 @@ namespace OpenNest
|
|||||||
anyPlacedOnPlate = true;
|
anyPlacedOnPlate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!anyPlacedOnPlate)
|
if (!anyPlacedOnPlate)
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -386,11 +386,26 @@ public class MultiPlateNesterTests
|
|||||||
|
|
||||||
_output.WriteLine("---");
|
_output.WriteLine("---");
|
||||||
_output.WriteLine($"Total: {items.Count} drawings, {items.Sum(i => i.Quantity)} parts");
|
_output.WriteLine($"Total: {items.Count} drawings, {items.Sum(i => i.Quantity)} parts");
|
||||||
|
|
||||||
|
var plateOptions = new List<PlateOption>
|
||||||
|
{
|
||||||
|
new() { Width = 48, Length = 96, Cost = 0 },
|
||||||
|
new() { Width = 48, Length = 120, Cost = 0 },
|
||||||
|
new() { Width = 48, Length = 144, Cost = 0 },
|
||||||
|
new() { Width = 60, Length = 96, Cost = 0 },
|
||||||
|
new() { Width = 60, Length = 120, Cost = 0 },
|
||||||
|
new() { Width = 60, Length = 144, Cost = 0 },
|
||||||
|
new() { Width = 72, Length = 96, Cost = 0 },
|
||||||
|
new() { Width = 72, Length = 120, Cost = 0 },
|
||||||
|
new() { Width = 72, Length = 144, Cost = 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
_output.WriteLine($"Plate options: {string.Join(", ", plateOptions.Select(o => $"{o.Width}x{o.Length}"))}");
|
||||||
_output.WriteLine("");
|
_output.WriteLine("");
|
||||||
|
|
||||||
var result = MultiPlateNester.Nest(
|
var result = MultiPlateNester.Nest(
|
||||||
items, template,
|
items, template,
|
||||||
plateOptions: null,
|
plateOptions: plateOptions,
|
||||||
salvageRate: 0.5,
|
salvageRate: 0.5,
|
||||||
sortOrder: PartSortOrder.BoundingBoxArea,
|
sortOrder: PartSortOrder.BoundingBoxArea,
|
||||||
minRemnantSize: 12.0,
|
minRemnantSize: 12.0,
|
||||||
|
|||||||
Reference in New Issue
Block a user