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 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-11-10 10:59:21 -05:00
1 parent 6894fcfade
commit 7dc471bbf8
3 files changed
+44 -5

No files matched your search

@@ -51,6 +51,36 @@ public class FixedStrategyNestingEngineTests
Assert.Single(result.Plates);
}
public static TheoryData<string> 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()
{
@@ -34,7 +34,8 @@ namespace OpenNest.Engine.Fill
CancellationToken token = default,
IProgress<NestProgress> progress = null,
int plateNumber = 0,
Func<NestItem, Box, List<Part>> widthFillFunc = null
Func<NestItem, Box, List<Part>> widthFillFunc = null,
IEnumerable<Box> 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.
@@ -515,6 +515,9 @@ internal class PlateFiller
return new List<Part>();
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);