From 1f88453d4c96834394465248e23a255d40cb4a28 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 6 Apr 2026 15:55:48 -0400 Subject: [PATCH] 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) --- OpenNest.Engine/MultiPlateNester.cs | 62 ++++++++++--------- .../Engine/MultiPlateNesterTests.cs | 17 ++++- 2 files changed, 50 insertions(+), 29 deletions(-) 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,