From 630d514b0ec261168ed54c329f7daa0136b1935a Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 21 Sep 2026 11:07:45 -0400 Subject: [PATCH] fix(engine): don't drop the topmost part when no other drawing is waiting RemnantFiller removes the topmost placed part to keep a clean rectangular obstacle for the next drawing, but the envelope then walls that slot off, so the part was lost for nothing (4 squares on a 9x9 plate became 3). Only remove it while another drawing still has demand. Fixes the mixed-stock NestRunner tests. Co-Authored-By: Claude Sonnet 5 --- OpenNest.Engine/Fill/RemnantFiller.cs | 22 +++++++++++++++++- OpenNest.Tests/Fill/RemnantFillerTests2.cs | 26 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/OpenNest.Engine/Fill/RemnantFiller.cs b/OpenNest.Engine/Fill/RemnantFiller.cs index ddf9dee..22b18aa 100644 --- a/OpenNest.Engine/Fill/RemnantFiller.cs +++ b/OpenNest.Engine/Fill/RemnantFiller.cs @@ -114,7 +114,10 @@ namespace OpenNest.Engine.Fill // rectangular obstacle boundary. Without this, gaps between // individual bounding boxes cause the next drawing to fill // into inter-row spaces, producing an interleaved layout. - if (placed.Count > 2) + // Only worthwhile while another drawing is still waiting for + // space; otherwise the removed part's slot is walled off by the + // envelope below and the part is lost for nothing. + if (placed.Count > 2 && HasOtherDemand(items, item, localQty)) RemoveTopmostPart(placed); allParts.AddRange(placed); @@ -132,6 +135,23 @@ namespace OpenNest.Engine.Fill return false; } + private static bool HasOtherDemand( + List items, + NestItem current, + Dictionary localQty + ) + { + foreach (var other in items) + { + if (ReferenceEquals(other.Drawing, current.Drawing)) + continue; + if (localQty[other.Drawing] > 0) + return true; + } + + return false; + } + private static void RemoveTopmostPart(List parts) { var topIdx = 0; diff --git a/OpenNest.Tests/Fill/RemnantFillerTests2.cs b/OpenNest.Tests/Fill/RemnantFillerTests2.cs index 2e37714..498570e 100644 --- a/OpenNest.Tests/Fill/RemnantFillerTests2.cs +++ b/OpenNest.Tests/Fill/RemnantFillerTests2.cs @@ -103,4 +103,30 @@ public class RemnantFillerTests2 // Should not throw, returns whatever was placed Assert.NotNull(result); } + + [Fact] + public void FillItems_SingleDrawing_KeepsEveryPlacedPart() + { + // With no other drawing waiting for space there is nothing to keep clear, so a full + // grid fill must not lose its topmost part. + var workArea = new Box(0, 0, 100, 100); + var filler = new RemnantFiller(workArea, 0); + var items = new List + { + new NestItem { Drawing = MakeSquareDrawing(10), Quantity = 4 }, + }; + + Func> fillFunc = (ni, b) => + new List + { + TestHelpers.MakePartAt(0, 0, 10), + TestHelpers.MakePartAt(10, 0, 10), + TestHelpers.MakePartAt(0, 10, 10), + TestHelpers.MakePartAt(10, 10, 10), + }; + + var placed = filler.FillItems(items, fillFunc); + + Assert.Equal(4, placed.Count); + } }