Files
OpenNest/OpenNest.Engine.Tests/Jobs/FixedStrategyNestingEngineTests.cs
T
ajandClaude Opus 5.5 7dc471bbf8 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>
2026-11-10 10:59:21 -05:00

138 lines
5.0 KiB
C#

using OpenNest.CNC;
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Tests.NestingEngines;
using OpenNest.Geometry;
using Xunit;
namespace OpenNest.Engine.Tests.Jobs;
public class FixedStrategyNestingEngineTests
{
[Fact]
public void ForcesConfiguredStrategyRegardlessOfJobOptions()
{
var engine = new FixedStrategyNestingEngine("Strip");
// The job itself declares an unknown strategy; if FixedStrategyNestingEngine
// didn't override it, PlateNesterFactory would reject it with NotSupportedException.
var job = FiniteStockJobTests.Job(1, new NestJobOptions("Not A Real Strategy"));
var result = engine.Solve(job);
Assert.Equal(NestJobStatus.Complete, result.Status);
}
[Fact]
public void PreservesJobMaxPlates()
{
var engine = new FixedStrategyNestingEngine("Fill");
var part = new NestJobPart(
"part",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(100, 100)),
6
);
var stock = new NestPlateStock(
"sheet",
new Size(220, 220),
quantity: null,
partSpacing: 2.0,
edgeSpacing: new Spacing(5.0, 5.0, 5.0, 5.0),
quadrant: 1
);
var job = new NestJob(
new[] { part },
new[] { stock },
new NestJobOptions("Fill", maxPlates: 1)
);
var result = engine.Solve(job);
Assert.Equal(NestJobStatus.Incomplete, result.Status);
Assert.Equal(NestJobStopReason.PlateLimitReached, result.StopReason);
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()
{
Assert.Throws<ArgumentException>(() => new FixedStrategyNestingEngine(null));
Assert.Throws<ArgumentException>(() => new FixedStrategyNestingEngine(" "));
}
public static TheoryData<string, string, double, int> LockedRotationCases =>
new()
{
{ "Fill", "rect-6x4", 0, 20 },
{ "Strip", "rect-6x4", 0, 20 },
{ "Strip", "rect-20x3", 0, 10 },
{ "Strip", "rect-3x20", 0, 10 },
{ "Strip", "rect-6x4", System.Math.PI / 2, 20 },
{ "Strip", "rect-6x4", 0.3, 10 },
{ "Strip", "lshape", 0.3, 10 },
{ "Vertical Remnant", "lshape", 0.3, 10 },
{ "Horizontal Remnant", "lshape", 0.3, 10 },
};
[Theory]
[MemberData(nameof(LockedRotationCases))]
public void LockedRotation_CompletesAtTheLockedAngle(
string strategy,
string shape,
double angle,
int quantity
)
{
var program = shape switch
{
"rect-6x4" => TestDrawingFactory.Rectangle(6, 4),
"rect-20x3" => TestDrawingFactory.Rectangle(20, 3),
"rect-3x20" => TestDrawingFactory.Rectangle(3, 20),
_ => TestDrawingFactory.LShape(),
};
var part = new NestJobPart(
shape,
PartGeometrySnapshot.FromProgram(program),
quantity,
rotation: RotationPolicy.Fixed(angle)
);
var stock = new NestPlateStock("stock", new Size(30, 50), 3, 1, new Spacing(1, 1, 1, 1));
var job = new NestJob(new[] { part }, new[] { stock }, new NestJobOptions(strategy));
var result = new FixedStrategyNestingEngine(strategy).Solve(job);
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.Equal(quantity, result.Plates.Sum(p => p.Placements.Count));
// Checks NestLayoutCheck and that every pose is an angle the policy allows.
LayoutAssert.Valid(job, result);
}
}