From 7dc471bbf8b3486994d63684de1ef2c4e8df3275 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Tue, 10 Nov 2026 10:59:21 -0500 Subject: [PATCH] fix(engine): keep Strip layouts clear of clamp keep-outs The Strip whole-job engine laid its strips, settled them and picked its pack remnant from the bare work area, never adding the plate's clamp keep-outs the Lattice orchestration uses. On a clamped sheet it put parts over the clamp, the runner's layout check rejected the candidate, and Strip produced nothing (N26-2A5 rev 5: door stop at (21.09,1.50) over clamp-1). NestStrips now treats InteractiveClamps.ClearanceRectangles as occupied space for the iterative shrink strips (through RemnantFiller), settles with the clamp obstacles when there are zones, and finds the pack remnant around them even before any strip is placed. Unclamped plates take the old path unchanged. A FixedStrategyNestingEngine theory runs all four fixed strategies on a clamped 26 x 26 sheet; only Strip failed it before. Co-Authored-By: Claude Opus 5.5 --- .../Jobs/FixedStrategyNestingEngineTests.cs | 30 +++++++++++++++++++ OpenNest.Engine/Fill/IterativeShrinkFiller.cs | 5 ++-- .../Jobs/Placement/Fillers/PlateFiller.cs | 14 +++++++-- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/OpenNest.Engine.Tests/Jobs/FixedStrategyNestingEngineTests.cs b/OpenNest.Engine.Tests/Jobs/FixedStrategyNestingEngineTests.cs index 2c4483b0..47a107ca 100644 --- a/OpenNest.Engine.Tests/Jobs/FixedStrategyNestingEngineTests.cs +++ b/OpenNest.Engine.Tests/Jobs/FixedStrategyNestingEngineTests.cs @@ -51,6 +51,36 @@ public class FixedStrategyNestingEngineTests Assert.Single(result.Plates); } + public static TheoryData Strategies => + new() { "Lattice", "Strip", "Vertical Remnant", "Horizontal Remnant" }; + + [Theory] + [MemberData(nameof(Strategies))] + public void ClampedSheet_KeepsTheClampClearance(string strategy) + { + // N26-2A5 rev 5: a 26 x 26 sheet with one rail clamp on its front edge. Strip used to + // lay its strips and pack its leftovers across the clamp, so the runner's validation + // threw every layout away. + var clamp = new ClampZone("clamp-1", new Box(19.25, 0, 1.5, 0.625)); + var stock = new NestPlateStock("sheet", new Size(26, 26), 1, 0.25, + new Spacing(0.25, 0.25, 0.25, 0.25), 1, null, [clamp]); + var parts = new[] + { + new NestJobPart("stop", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(3, 1.5)), 14), + new NestJobPart("plate", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 2.5)), 14), + }; + var job = new NestJob(parts, new[] { stock }, new NestJobOptions(strategy)); + + var result = new FixedStrategyNestingEngine(strategy).Solve(job); + + Assert.Equal(NestJobStatus.Complete, result.Status); + Assert.Equal(28, result.Plates.Sum(p => p.Placements.Count)); + // NestLayoutCheck holds every part the clamp clearance away from the zone. + LayoutAssert.Valid(job, result); + } + [Fact] public void RejectsNullOrWhitespaceStrategyAtConstruction() { diff --git a/OpenNest.Engine/Fill/IterativeShrinkFiller.cs b/OpenNest.Engine/Fill/IterativeShrinkFiller.cs index 9918e584..c97b4903 100644 --- a/OpenNest.Engine/Fill/IterativeShrinkFiller.cs +++ b/OpenNest.Engine/Fill/IterativeShrinkFiller.cs @@ -34,7 +34,8 @@ namespace OpenNest.Engine.Fill CancellationToken token = default, IProgress progress = null, int plateNumber = 0, - Func> widthFillFunc = null + Func> widthFillFunc = null, + IEnumerable keepOuts = null ) { if (items == null || items.Count == 0) @@ -71,7 +72,7 @@ namespace OpenNest.Engine.Fill } } - var filler = new RemnantFiller(workArea, spacing); + var filler = new RemnantFiller(workArea, spacing, keepOuts); // Track parts placed by previous items so ShrinkFiller can // include them in progress reports. diff --git a/OpenNest.Engine/Jobs/Placement/Fillers/PlateFiller.cs b/OpenNest.Engine/Jobs/Placement/Fillers/PlateFiller.cs index 060292d7..9bf9c30e 100644 --- a/OpenNest.Engine/Jobs/Placement/Fillers/PlateFiller.cs +++ b/OpenNest.Engine/Jobs/Placement/Fillers/PlateFiller.cs @@ -515,6 +515,9 @@ internal class PlateFiller return new List(); var workArea = Plate.WorkArea(); + // Clamp keep-outs (zones grown by the clamp clearance) are occupied space for the + // strips, the settle and the pack remnant alike, as in the Lattice orchestration. + var clampKeepOuts = InteractiveClamps.ClearanceRectangles(Plate); var fillItems = items .Where(item => item.Quantity != 1) .OrderByDescending(item => item.Priority) @@ -546,11 +549,15 @@ internal class PlateFiller token, progress, PlateNumber, - widthFillFunc + widthFillFunc, + clampKeepOuts ); allParts.AddRange(shrinkResult.Parts); - Compactor.Settle(allParts, workArea, Plate.PartSpacing); + if (clampKeepOuts.Count > 0) + Compactor.Settle(allParts, Plate, workArea); + else + Compactor.Settle(allParts, workArea, Plate.PartSpacing); packItems.AddRange(shrinkResult.Leftovers); } @@ -558,11 +565,12 @@ internal class PlateFiller if (packItems.Count > 0 && !token.IsCancellationRequested) { var packArea = workArea; - if (allParts.Count > 0) + if (allParts.Count > 0 || clampKeepOuts.Count > 0) { var obstacles = allParts .Select(part => part.BoundingBox.Offset(Plate.PartSpacing)) .ToList(); + obstacles.AddRange(clampKeepOuts); var finder = new RemnantFinder(workArea, obstacles); var remnants = finder.FindRemnants(); packArea = remnants.Count > 0 ? remnants[0] : new Box(0, 0, 0, 0);