diff --git a/OpenNest.Engine/MultiPlateNester.cs b/OpenNest.Engine/MultiPlateNester.cs index d297900..b5819b1 100644 --- a/OpenNest.Engine/MultiPlateNester.cs +++ b/OpenNest.Engine/MultiPlateNester.cs @@ -248,19 +248,25 @@ namespace OpenNest if (token.IsCancellationRequested) break; - var remaining = leftovers.Where(i => i.Quantity > 0).ToList(); - if (remaining.Count == 0) - break; - - var finder = RemnantFinder.FromPlate(pr.Plate); - var remnants = finder.FindRemnants(); - - foreach (var remnant in remnants) + // 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) { - remaining = remaining.Where(i => i.Quantity > 0).ToList(); + anyPlacedOnThis = false; + + var remaining = leftovers.Where(i => i.Quantity > 0).ToList(); if (remaining.Count == 0) break; + var finder = RemnantFinder.FromPlate(pr.Plate); + var remnants = finder.FindRemnants(); + if (remnants.Count == 0) + break; + + // Try the largest remnant. + var remnant = remnants[0]; + var engine = NestEngineRegistry.Create(pr.Plate); var cloned = remaining.Select(CloneItem).ToList(); var parts = engine.PackArea(remnant, cloned, progress, token); @@ -269,8 +275,8 @@ namespace OpenNest { pr.Plate.Parts.AddRange(parts); pr.Parts.AddRange(parts); + anyPlacedOnThis = true; - // Deduct placed quantities from originals. foreach (var item in remaining) { var placed = parts.Count(p => p.BaseDrawing.Name == item.Drawing.Name); @@ -292,33 +298,33 @@ namespace OpenNest var anyPlacedOnPlate = false; // Fill each leftover drawing onto this plate. + // Recalculate remnants after each fill to avoid overlaps. foreach (var item in leftovers) { if (item.Quantity <= 0 || token.IsCancellationRequested) continue; - // Find remaining space on the plate. - var finder = RemnantFinder.FromPlate(plate); + // Find remaining space on the plate (recalculated each item). var remnants = allParts.Count == 0 ? new List { plate.WorkArea() } - : finder.FindRemnants(); + : RemnantFinder.FromPlate(plate).FindRemnants(); - foreach (var remnant in remnants) + if (remnants.Count == 0) + break; + + // Use only the largest remnant to avoid stale overlap issues. + var remnant = remnants[0]; + + var engine = NestEngineRegistry.Create(plate); + var clonedItem = CloneItem(item); + var parts = engine.Fill(clonedItem, remnant, progress, token); + + if (parts.Count > 0) { - if (item.Quantity <= 0) - break; - - var engine = NestEngineRegistry.Create(plate); - var clonedItem = CloneItem(item); - var parts = engine.Fill(clonedItem, remnant, progress, token); - - if (parts.Count > 0) - { - plate.Parts.AddRange(parts); - allParts.AddRange(parts); - item.Quantity = System.Math.Max(0, item.Quantity - parts.Count); - anyPlacedOnPlate = true; - } + plate.Parts.AddRange(parts); + allParts.AddRange(parts); + item.Quantity = System.Math.Max(0, item.Quantity - parts.Count); + anyPlacedOnPlate = true; } } diff --git a/OpenNest.Tests/Engine/MultiPlateNesterTests.cs b/OpenNest.Tests/Engine/MultiPlateNesterTests.cs index 945d787..2216771 100644 --- a/OpenNest.Tests/Engine/MultiPlateNesterTests.cs +++ b/OpenNest.Tests/Engine/MultiPlateNesterTests.cs @@ -386,11 +386,26 @@ public class MultiPlateNesterTests _output.WriteLine("---"); _output.WriteLine($"Total: {items.Count} drawings, {items.Sum(i => i.Quantity)} parts"); + + var plateOptions = new List + { + 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(""); var result = MultiPlateNester.Nest( items, template, - plateOptions: null, + plateOptions: plateOptions, salvageRate: 0.5, sortOrder: PartSortOrder.BoundingBoxArea, minRemnantSize: 12.0,