style: apply CSharpier formatting to all C# sources

Repo-wide sweep with the pinned CSharpier 1.3.0 tool. Whitespace and
line-wrapping only; OpenNest.Engine.Tests (109) and OpenNest.IO.Tests
pass after reformat, full solution builds 0 errors.

Added .csharpierignore so csproj/config XML keeps its existing layout
(CSharpier's XML wrapping churns attributes with zero benefit).

Formatting is now enforceable: dotnet csharpier check . passes.
This commit is contained in:
aj
2026-09-20 16:41:50 -04:00
parent 8e6fa677fb
commit aec0523062
476 changed files with 16592 additions and 7800 deletions
@@ -25,7 +25,7 @@ public class SortStripsTests
// shortest, then medium). The tallest column's original position leaves a
// 5-unit gap to its neighbor; that single sampled gap must not get replayed
// as the spacing for the whole staircase once it's no longer the leading pair.
var tall = MakeRectPart(0, 0, 10, 30); // Left 0-10, gap of 5 to next
var tall = MakeRectPart(0, 0, 10, 30); // Left 0-10, gap of 5 to next
var shortCol = MakeRectPart(15, 0, 5, 5); // Left 15-20, gap of 1 to next
var medium = MakeRectPart(21, 0, 20, 15); // Left 21-41
@@ -40,7 +40,9 @@ public class SortStripsTests
var newLeft = parts.Min(p => p.BoundingBox.Left);
var newSpan = newRight - newLeft;
Assert.True(newSpan <= originalSpan + 1e-9,
$"Resequenced columns must not exceed the original footprint: original span {originalSpan}, new span {newSpan}");
Assert.True(
newSpan <= originalSpan + 1e-9,
$"Resequenced columns must not exceed the original footprint: original span {originalSpan}, new span {newSpan}"
);
}
}
+158 -44
View File
@@ -4,36 +4,66 @@ namespace OpenNest.Engine.Tests.Jobs;
public class FiniteStockJobTests
{
internal static NestJob Job(int? stock = 3, NestJobOptions? options = null) => new(
new[] { new NestJobPart("p", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle()), 3) },
new[] { new NestPlateStock("s", new Size(100, 200), stock) }, options);
internal static NestJob Job(int? stock = 3, NestJobOptions? options = null) =>
new(
new[]
{
new NestJobPart(
"p",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle()),
3
),
},
new[] { new NestPlateStock("s", new Size(100, 200), stock) },
options
);
internal sealed class Nester(Func<PlatePlacementRequest, PlateCandidate> place) : IPlateNester
{
public int Calls { get; private set; }
public PlateCandidate Place(PlatePlacementRequest request, IProgress<NestJobProgress>? progress = null,
CancellationToken token = default) { Calls++; return place(request); }
public PlateCandidate Place(
PlatePlacementRequest request,
IProgress<NestJobProgress>? progress = null,
CancellationToken token = default
)
{
Calls++;
return place(request);
}
}
internal static PlateCandidate One(PlatePlacementRequest request) => new(new[]
{ new NestJobPlacement(request.Parts[0].Id, 99, 0, 0, 0) });
internal static PlateCandidate One(PlatePlacementRequest request) =>
new(new[] { new NestJobPlacement(request.Parts[0].Id, 99, 0, 0, 0) });
[Theory]
[InlineData(3, 3, 0, NestJobStatus.Complete, NestJobStopReason.Completed)]
[InlineData(2, 2, 1, NestJobStatus.Incomplete, NestJobStopReason.StockExhausted)]
[InlineData(0, 0, 3, NestJobStatus.Incomplete, NestJobStopReason.StockExhausted)]
public void DemandAndPhysicalStockAreAccountedFromPlacements(int stock, int placed, int left,
NestJobStatus status, NestJobStopReason reason)
public void DemandAndPhysicalStockAreAccountedFromPlacements(
int stock,
int placed,
int left,
NestJobStatus status,
NestJobStopReason reason
)
{
var requests = new List<int>();
var nester = new Nester(r => { requests.Add(r.Parts[0].Quantity); return One(r); });
var nester = new Nester(r =>
{
requests.Add(r.Parts[0].Quantity);
return One(r);
});
var job = Job(stock);
var result = new NestJobRunner(_ => nester).Solve(job);
Assert.Equal(status, result.Status);
Assert.Equal(reason, result.StopReason);
Assert.Equal(placed, result.Plates.Count);
Assert.All(result.Plates, p => Assert.Single(p.Placements));
Assert.Equal(Enumerable.Range(0, placed), result.Plates.SelectMany(p => p.Placements).Select(p => p.InstanceIndex));
Assert.Equal(
Enumerable.Range(0, placed),
result.Plates.SelectMany(p => p.Placements).Select(p => p.InstanceIndex)
);
Assert.Equal(Enumerable.Range(0, placed).Select(i => 3 - i), requests);
Assert.Equal(new PartFulfillment("p", 3, placed, left), Assert.Single(result.Fulfillment));
Assert.Equal(new StockUsage("s", placed, stock - placed), Assert.Single(result.StockUsage));
@@ -58,7 +88,9 @@ public class FiniteStockJobTests
[Fact]
public void PlateLimitStopsUnlimitedStock()
{
var result = new NestJobRunner(_ => new Nester(One)).Solve(Job(null, new NestJobOptions(maxPlates: 2)));
var result = new NestJobRunner(_ => new Nester(One)).Solve(
Job(null, new NestJobOptions(maxPlates: 2))
);
Assert.Equal(2, result.Plates.Count);
Assert.Equal(NestJobStopReason.PlateLimitReached, result.StopReason);
Assert.Equal(new StockUsage("s", 2, null), Assert.Single(result.StockUsage));
@@ -69,9 +101,14 @@ public class FiniteStockJobTests
{
using var cts = new CancellationTokenSource();
var commits = new List<NestJobProgress>();
var nester = new Nester(r => { cts.Cancel(); return One(r); });
Assert.Throws<OperationCanceledException>(() => new NestJobRunner(_ => nester)
.Solve(Job(), new InlineProgress(commits.Add), cts.Token));
var nester = new Nester(r =>
{
cts.Cancel();
return One(r);
});
Assert.Throws<OperationCanceledException>(() =>
new NestJobRunner(_ => nester).Solve(Job(), new InlineProgress(commits.Add), cts.Token)
);
Assert.DoesNotContain(commits, p => p.Stage == NestJobStage.PlateCommitted);
}
@@ -80,8 +117,9 @@ public class FiniteStockJobTests
{
using var cts = new CancellationTokenSource();
cts.Cancel();
Assert.Throws<OperationCanceledException>(() => new NestJobRunner(_ => throw new Exception("called"))
.Solve(Job(), token: cts.Token));
Assert.Throws<OperationCanceledException>(() =>
new NestJobRunner(_ => throw new Exception("called")).Solve(Job(), token: cts.Token)
);
}
[Theory]
@@ -92,23 +130,31 @@ public class FiniteStockJobTests
[InlineData("p", 0, 0, 0, 4)]
public void InvalidCandidateThrows(string id, double x, double y, double rotation, int count)
{
var nester = new Nester(_ => new PlateCandidate(Enumerable.Range(0, count)
.Select(i => new NestJobPlacement(id, i, x, y, rotation))));
var nester = new Nester(_ => new PlateCandidate(
Enumerable.Range(0, count).Select(i => new NestJobPlacement(id, i, x, y, rotation))
));
Assert.Throws<InvalidOperationException>(() => new NestJobRunner(_ => nester).Solve(Job()));
}
[Fact]
public void NullCandidateAndUnknownStrategyAreExplicitErrors()
{
Assert.Throws<InvalidOperationException>(() => new NestJobRunner(_ => new Nester(_ => null!)).Solve(Job()));
Assert.Throws<NotSupportedException>(() => new NestJobRunner(_ => null!).Solve(Job(options: new NestJobOptions("missing"))));
Assert.Throws<InvalidOperationException>(() =>
new NestJobRunner(_ => new Nester(_ => null!)).Solve(Job())
);
Assert.Throws<NotSupportedException>(() =>
new NestJobRunner(_ => null!).Solve(Job(options: new NestJobOptions("missing")))
);
}
[Fact]
public void MixedStockCanBeEvaluated()
{
var job = Job();
var mixed = new NestJob(job.Parts, job.Plates.Concat(new[] { new NestPlateStock("other", new Size(20, 10), 2) }));
var mixed = new NestJob(
job.Parts,
job.Plates.Concat(new[] { new NestPlateStock("other", new Size(20, 10), 2) })
);
var result = new NestJobRunner(_ => new Nester(One)).Solve(mixed);
Assert.Equal(NestJobStatus.Complete, result.Status);
}
@@ -119,9 +165,20 @@ public class FiniteStockJobTests
[InlineData(10, 20, -1, 1)]
[InlineData(10, 20, double.PositiveInfinity, 1)]
[InlineData(10, 20, 0, 5)]
public void InvalidStockSettingsRejected(double width, double length, double spacing, int quadrant)
public void InvalidStockSettingsRejected(
double width,
double length,
double spacing,
int quadrant
)
{
var job = new NestJob(Job().Parts, new[] { new NestPlateStock("s", new Size(width, length), 1, spacing, quadrant: quadrant) });
var job = new NestJob(
Job().Parts,
new[]
{
new NestPlateStock("s", new Size(width, length), 1, spacing, quadrant: quadrant),
}
);
Assert.Throws<ArgumentException>(() => new NestJobRunner(_ => new Nester(One)).Solve(job));
}
@@ -129,25 +186,56 @@ public class FiniteStockJobTests
public void InvalidEdgesAndGeometryRejected()
{
var runner = new NestJobRunner(_ => new Nester(One));
foreach (var edges in new[] { new Spacing(-1, 0, 0, 0), new Spacing(0, double.NaN, 0, 0), new Spacing(1000, 1000, 1000, 1000) })
Assert.Throws<ArgumentException>(() => runner.Solve(new NestJob(Job().Parts,
new[] { new NestPlateStock("s", new Size(100, 200), edgeSpacing: edges) })));
foreach (
var edges in new[]
{
new Spacing(-1, 0, 0, 0),
new Spacing(0, double.NaN, 0, 0),
new Spacing(1000, 1000, 1000, 1000),
}
)
Assert.Throws<ArgumentException>(() =>
runner.Solve(
new NestJob(
Job().Parts,
new[] { new NestPlateStock("s", new Size(100, 200), edgeSpacing: edges) }
)
)
);
var program = TestDrawingFactory.Rectangle();
program.LineTo(double.NaN, 0);
Assert.Throws<ArgumentException>(() => runner.Solve(new NestJob(new[]
{ new NestJobPart("p", PartGeometrySnapshot.FromProgram(program), 1) }, Job().Plates)));
Assert.Throws<ArgumentException>(() =>
runner.Solve(
new NestJob(
new[] { new NestJobPart("p", PartGeometrySnapshot.FromProgram(program), 1) },
Job().Plates
)
)
);
}
[Fact]
public void InvalidContractInputsAreRejected()
{
var job = Job();
Assert.Throws<ArgumentNullException>(() => new NestJobRunner(_ => new Nester(One)).Solve(null!));
Assert.Throws<ArgumentException>(() => new NestJob(new NestJobPart[] { null! }, job.Plates));
Assert.Throws<ArgumentException>(() => new NestJob(job.Parts.Concat(job.Parts), job.Plates));
Assert.Throws<ArgumentException>(() => new NestJob(job.Parts, job.Plates.Concat(job.Plates)));
Assert.Throws<ArgumentOutOfRangeException>(() => new NestJobPart("p", job.Parts[0].Geometry, 0));
Assert.Throws<ArgumentOutOfRangeException>(() => new NestPlateStock("s", new Size(1, 1), -1));
Assert.Throws<ArgumentNullException>(() =>
new NestJobRunner(_ => new Nester(One)).Solve(null!)
);
Assert.Throws<ArgumentException>(() =>
new NestJob(new NestJobPart[] { null! }, job.Plates)
);
Assert.Throws<ArgumentException>(() =>
new NestJob(job.Parts.Concat(job.Parts), job.Plates)
);
Assert.Throws<ArgumentException>(() =>
new NestJob(job.Parts, job.Plates.Concat(job.Plates))
);
Assert.Throws<ArgumentOutOfRangeException>(() =>
new NestJobPart("p", job.Parts[0].Geometry, 0)
);
Assert.Throws<ArgumentOutOfRangeException>(() =>
new NestPlateStock("s", new Size(1, 1), -1)
);
Assert.Throws<ArgumentOutOfRangeException>(() => new NestJobOptions(maxPlates: 0));
}
@@ -155,11 +243,28 @@ public class FiniteStockJobTests
public void RunnerFactoriesAreInstanceScopedAndReceiveExactStrategyKeys()
{
var keys = new List<string>();
var first = new NestJobRunner(key => { keys.Add(key); return new Nester(One); });
var second = new NestJobRunner(key => { keys.Add(key); return new Nester(_ => new PlateCandidate(Array.Empty<NestJobPlacement>())); });
Assert.Equal(NestJobStatus.Complete, first.Solve(Job(options: new NestJobOptions("custom-A"))).Status);
Assert.Equal(NestJobStopReason.NoPlacementFound, second.Solve(Job(options: new NestJobOptions("custom-B"))).StopReason);
Assert.Equal(NestJobStatus.Complete, first.Solve(Job(options: new NestJobOptions("custom-A"))).Status);
var first = new NestJobRunner(key =>
{
keys.Add(key);
return new Nester(One);
});
var second = new NestJobRunner(key =>
{
keys.Add(key);
return new Nester(_ => new PlateCandidate(Array.Empty<NestJobPlacement>()));
});
Assert.Equal(
NestJobStatus.Complete,
first.Solve(Job(options: new NestJobOptions("custom-A"))).Status
);
Assert.Equal(
NestJobStopReason.NoPlacementFound,
second.Solve(Job(options: new NestJobOptions("custom-B"))).StopReason
);
Assert.Equal(
NestJobStatus.Complete,
first.Solve(Job(options: new NestJobOptions("custom-A"))).Status
);
Assert.Equal(new[] { "custom-A", "custom-B", "custom-A" }, keys);
}
@@ -169,10 +274,19 @@ public class FiniteStockJobTests
using var cts = new CancellationTokenSource();
var calls = 0;
var commits = new List<NestJobProgress>();
var nester = new Nester(r => { if (++calls == 2) cts.Cancel(); return One(r); });
Assert.Throws<OperationCanceledException>(() => new NestJobRunner(_ => nester)
.Solve(Job(), new InlineProgress(commits.Add), cts.Token));
Assert.Equal(1, Assert.Single(commits.Where(p => p.Stage == NestJobStage.PlateCommitted)).CommittedParts);
var nester = new Nester(r =>
{
if (++calls == 2)
cts.Cancel();
return One(r);
});
Assert.Throws<OperationCanceledException>(() =>
new NestJobRunner(_ => nester).Solve(Job(), new InlineProgress(commits.Add), cts.Token)
);
Assert.Equal(
1,
Assert.Single(commits.Where(p => p.Stage == NestJobStage.PlateCommitted)).CommittedParts
);
Assert.Equal(2, calls);
}
@@ -22,10 +22,24 @@ public class FixedStrategyNestingEngineTests
public void PreservesJobMaxPlates()
{
var engine = new FixedStrategyNestingEngine("Default");
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("Default", maxPlates: 1));
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("Default", maxPlates: 1)
);
var result = engine.Solve(job);
+106 -47
View File
@@ -10,33 +10,54 @@ public class JobAdapterTests
{
var drawing = new Drawing("same name", TestDrawingFactory.Rectangle());
drawing.Quantity.Required = 9;
var item = new NestItem { Drawing = drawing, Quantity = 3, Priority = 7, StepAngle = 0 };
var sourcePlate = new Plate(100, 200) { Quantity = 3, PartSpacing = 2 };
var job = new NestJob(new[] { DrawingJobMapper.FromItem("requirement", item) },
new[] { DrawingJobMapper.FromPlate("stock", sourcePlate, 3) });
var quantities = new List<int>();
var adapter = new LegacyPlateNesterAdapter(p => new MutatingEngine(p, items =>
var item = new NestItem
{
var privateItem = Assert.Single(items);
quantities.Add(privateItem.Quantity);
Assert.NotSame(drawing, privateItem.Drawing);
Assert.Equal(0, privateItem.StepAngle);
Assert.Equal(7, privateItem.Priority);
var part = new Part(privateItem.Drawing);
privateItem.Quantity = 0;
privateItem.Drawing.Quantity.Required = 0;
return new List<Part> { part };
}));
Drawing = drawing,
Quantity = 3,
Priority = 7,
StepAngle = 0,
};
var sourcePlate = new Plate(100, 200) { Quantity = 3, PartSpacing = 2 };
var job = new NestJob(
new[] { DrawingJobMapper.FromItem("requirement", item) },
new[] { DrawingJobMapper.FromPlate("stock", sourcePlate, 3) }
);
var quantities = new List<int>();
var adapter = new LegacyPlateNesterAdapter(p => new MutatingEngine(
p,
items =>
{
var privateItem = Assert.Single(items);
quantities.Add(privateItem.Quantity);
Assert.NotSame(drawing, privateItem.Drawing);
Assert.Equal(0, privateItem.StepAngle);
Assert.Equal(7, privateItem.Priority);
var part = new Part(privateItem.Drawing);
privateItem.Quantity = 0;
privateItem.Drawing.Quantity.Required = 0;
return new List<Part> { part };
}
));
var result = new NestJobRunner(_ => adapter).Solve(job);
var materialized = NestResultMaterializer.Materialize(job, result);
Assert.Equal(new[] { 3, 2, 1 }, quantities);
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.Equal(3, materialized.Nest.Plates.Count);
Assert.All(materialized.Nest.Plates, p => { Assert.Equal(1, p.Quantity); Assert.Single(p.Parts); });
Assert.All(
materialized.Nest.Plates,
p =>
{
Assert.Equal(1, p.Quantity);
Assert.Single(p.Parts);
}
);
var outputDrawing = materialized.DrawingsByPartId["requirement"];
Assert.Equal(3, outputDrawing.Quantity.Required);
Assert.Equal(3, outputDrawing.Quantity.Nested);
Assert.All(materialized.Nest.Plates, p => Assert.Same(outputDrawing, p.Parts[0].BaseDrawing));
Assert.All(
materialized.Nest.Plates,
p => Assert.Same(outputDrawing, p.Parts[0].BaseDrawing)
);
Assert.NotSame(drawing, outputDrawing);
Assert.Equal(9, drawing.Quantity.Required);
Assert.Equal(0, drawing.Quantity.Nested);
@@ -44,26 +65,38 @@ public class JobAdapterTests
Assert.Equal(3, sourcePlate.Quantity);
Assert.Empty(sourcePlate.Parts);
Assert.Equal(2, sourcePlate.PartSpacing);
Assert.Equal(PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle()).Motions,
PartGeometrySnapshot.FromProgram(drawing.Program).Motions);
Assert.Equal(
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle()).Motions,
PartGeometrySnapshot.FromProgram(drawing.Program).Motions
);
}
[Fact]
public void ReferenceIdentityNotNamesControlsLegacyPlacements()
{
var drawing = new Drawing("duplicate", TestDrawingFactory.Rectangle());
var job = new NestJob(new[] { DrawingJobMapper.FromDrawing("a", drawing, 1),
DrawingJobMapper.FromDrawing("b", drawing, 1) }, FiniteStockJobTests.Job(1).Plates);
var adapter = new LegacyPlateNesterAdapter(p => new MutatingEngine(p, items =>
{
Assert.NotSame(items[0].Drawing, items[1].Drawing);
foreach (var item in items) item.Drawing.Name = "identical";
return new List<Part>
var job = new NestJob(
new[]
{
new Part(items[0].Drawing, new Vector(0, 0)),
new Part(items[1].Drawing, new Vector(10, 0))
};
}));
DrawingJobMapper.FromDrawing("a", drawing, 1),
DrawingJobMapper.FromDrawing("b", drawing, 1),
},
FiniteStockJobTests.Job(1).Plates
);
var adapter = new LegacyPlateNesterAdapter(p => new MutatingEngine(
p,
items =>
{
Assert.NotSame(items[0].Drawing, items[1].Drawing);
foreach (var item in items)
item.Drawing.Name = "identical";
return new List<Part>
{
new Part(items[0].Drawing, new Vector(0, 0)),
new Part(items[1].Drawing, new Vector(10, 0)),
};
}
));
var result = new NestJobRunner(_ => adapter).Solve(job);
Assert.Equal(new[] { "a", "b" }, result.Plates[0].Placements.Select(p => p.PartId));
var output = NestResultMaterializer.Materialize(job, result);
@@ -74,10 +107,19 @@ public class JobAdapterTests
[Fact]
public void UnknownPrivateDrawingIsRejectedEvenWithMatchingName()
{
var adapter = new LegacyPlateNesterAdapter(p => new MutatingEngine(p, items => new List<Part>
{ new(new Drawing(items[0].Drawing.Name, TestDrawingFactory.Rectangle())) }));
Assert.Throws<InvalidOperationException>(() => new NestJobRunner(_ => adapter).Solve(FiniteStockJobTests.Job()));
Assert.Throws<NotSupportedException>(() => LegacyPlateNesterAdapter.Create("not registered"));
var adapter = new LegacyPlateNesterAdapter(p => new MutatingEngine(
p,
items => new List<Part>
{
new(new Drawing(items[0].Drawing.Name, TestDrawingFactory.Rectangle())),
}
));
Assert.Throws<InvalidOperationException>(() =>
new NestJobRunner(_ => adapter).Solve(FiniteStockJobTests.Job())
);
Assert.Throws<NotSupportedException>(() =>
LegacyPlateNesterAdapter.Create("not registered")
);
}
[Fact]
@@ -103,17 +145,26 @@ public class JobAdapterTests
incremental.LineTo(7, 4);
var incrementalSnapshot = PartGeometrySnapshot.FromProgram(incremental);
Assert.Equal(Mode.Incremental, DrawingJobMapper.ToProgram(incrementalSnapshot).Mode);
Assert.Equal(incrementalSnapshot.Motions,
PartGeometrySnapshot.FromProgram(DrawingJobMapper.ToProgram(incrementalSnapshot)).Motions);
Assert.Equal(
incrementalSnapshot.Motions,
PartGeometrySnapshot
.FromProgram(DrawingJobMapper.ToProgram(incrementalSnapshot))
.Motions
);
}
[Fact]
public void RealDefaultEngineRunsFromDrawingThroughMaterialization()
{
var drawing = new Drawing("generated asymmetric rectangle", TestDrawingFactory.Rectangle(13, 7));
var drawing = new Drawing(
"generated asymmetric rectangle",
TestDrawingFactory.Rectangle(13, 7)
);
drawing.Quantity.Required = 1;
var job = new NestJob(new[] { DrawingJobMapper.FromDrawing("rectangle", drawing, 1) },
new[] { new NestPlateStock("sheet", new Size(40, 60), 1, 1, new Spacing(2, 2, 2, 2)) });
var job = new NestJob(
new[] { DrawingJobMapper.FromDrawing("rectangle", drawing, 1) },
new[] { new NestPlateStock("sheet", new Size(40, 60), 1, 1, new Spacing(2, 2, 2, 2)) }
);
var result = new NestJobRunner(LegacyPlateNesterAdapter.Create).Solve(job);
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.Equal(new StockUsage("sheet", 1, 0), Assert.Single(result.StockUsage));
@@ -140,10 +191,13 @@ public class JobAdapterTests
{
var program = TestDrawingFactory.Rectangle();
program.Offset(-5, 3);
var job = new NestJob(new[] { new NestJobPart("p", PartGeometrySnapshot.FromProgram(program), 1) },
FiniteStockJobTests.Job(1).Plates);
var result = new NestJobRunner(_ => new FiniteStockJobTests.Nester(_ => new PlateCandidate(new[]
{ new NestJobPlacement("p", 0, 23, 31, 0.7) }))).Solve(job);
var job = new NestJob(
new[] { new NestJobPart("p", PartGeometrySnapshot.FromProgram(program), 1) },
FiniteStockJobTests.Job(1).Plates
);
var result = new NestJobRunner(_ => new FiniteStockJobTests.Nester(_ => new PlateCandidate(
new[] { new NestJobPlacement("p", 0, 23, 31, 0.7) }
))).Solve(job);
var output = NestResultMaterializer.Materialize(job, result);
var part = output.Nest.Plates[0].Parts[0];
var expected = new Vector(-5, 3).Rotate(0.7);
@@ -152,11 +206,16 @@ public class JobAdapterTests
Assert.Equal(new Vector(23, 31), part.Location);
}
private sealed class MutatingEngine(Plate plate, Func<List<NestItem>, List<Part>> nest) : NestEngineBase(plate)
private sealed class MutatingEngine(Plate plate, Func<List<NestItem>, List<Part>> nest)
: NestEngineBase(plate)
{
public override string Name => "test";
public override string Description => "mutates private demand";
public override List<Part> Nest(List<NestItem> items, IProgress<NestProgress> progress, CancellationToken token)
=> nest(items);
public override List<Part> Nest(
List<NestItem> items,
IProgress<NestProgress> progress,
CancellationToken token
) => nest(items);
}
}
@@ -10,13 +10,21 @@ public class NestJobCancellationTests
{
using var cancellation = new CancellationTokenSource();
cancellation.Cancel();
var nester = new CancellableNester(_ => new PlateCandidate(Array.Empty<NestJobPlacement>()));
var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), 1);
var nester = new CancellableNester(_ => new PlateCandidate(
Array.Empty<NestJobPlacement>()
));
var part = new NestJobPart(
"part",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)),
1
);
var sourceGeometry = part.Geometry.Motions.ToArray();
var stock = new NestPlateStock("stock", new Size(20, 30), 1);
var job = new NestJob(new[] { part }, new[] { stock });
Assert.Throws<OperationCanceledException>(() => new NestJobRunner(_ => nester).Solve(job, token: cancellation.Token));
Assert.Throws<OperationCanceledException>(() =>
new NestJobRunner(_ => nester).Solve(job, token: cancellation.Token)
);
Assert.Equal(0, nester.Calls);
Assert.Equal(sourceGeometry, job.Parts[0].Geometry.Motions);
@@ -29,19 +37,30 @@ public class NestJobCancellationTests
{
using var cancellation = new CancellationTokenSource();
var reports = new List<NestJobProgress>();
var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), 1);
var part = new NestJobPart(
"part",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)),
1
);
var sourceGeometry = part.Geometry.Motions.ToArray();
var stock = new NestPlateStock("stock", new Size(20, 30), 1);
var job = new NestJob(new[] { part }, new[] { stock });
var nester = new CancellableNester((_, token) =>
{
cancellation.Cancel();
token.ThrowIfCancellationRequested();
return new PlateCandidate(Array.Empty<NestJobPlacement>());
});
var nester = new CancellableNester(
(_, token) =>
{
cancellation.Cancel();
token.ThrowIfCancellationRequested();
return new PlateCandidate(Array.Empty<NestJobPlacement>());
}
);
Assert.Throws<OperationCanceledException>(() => new NestJobRunner(_ => nester)
.Solve(job, new InlineProgress(reports.Add), cancellation.Token));
Assert.Throws<OperationCanceledException>(() =>
new NestJobRunner(_ => nester).Solve(
job,
new InlineProgress(reports.Add),
cancellation.Token
)
);
Assert.Equal(1, nester.Calls);
Assert.DoesNotContain(reports, report => report.Stage == NestJobStage.PlateCommitted);
@@ -54,9 +73,18 @@ public class NestJobCancellationTests
public void LegacyProgressIsWrappedWithCurrentCandidateContext()
{
var reports = new List<NestJobProgress>();
var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), 1);
var job = new NestJob(new[] { part }, new[] { new NestPlateStock("stock", new Size(20, 30), 1) });
var runner = new NestJobRunner(_ => new LegacyPlateNesterAdapter(plate => new ReportingEngine(plate)));
var part = new NestJobPart(
"part",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)),
1
);
var job = new NestJob(
new[] { part },
new[] { new NestPlateStock("stock", new Size(20, 30), 1) }
);
var runner = new NestJobRunner(_ => new LegacyPlateNesterAdapter(
plate => new ReportingEngine(plate)
));
var result = runner.Solve(job, new InlineProgress(reports.Add));
@@ -84,15 +112,20 @@ public class NestJobCancellationTests
this.place = (request, _) => place(request);
}
public CancellableNester(Func<PlatePlacementRequest, CancellationToken, PlateCandidate> place)
public CancellableNester(
Func<PlatePlacementRequest, CancellationToken, PlateCandidate> place
)
{
this.place = place;
}
public int Calls { get; private set; }
public PlateCandidate Place(PlatePlacementRequest request, IProgress<NestJobProgress>? progress = null,
CancellationToken token = default)
public PlateCandidate Place(
PlatePlacementRequest request,
IProgress<NestJobProgress>? progress = null,
CancellationToken token = default
)
{
Calls++;
return place(request, token);
@@ -104,8 +137,11 @@ public class NestJobCancellationTests
public override string Name => "reporting";
public override string Description => "reports progress";
public override List<Part> Nest(List<NestItem> items, IProgress<NestProgress>? progress,
CancellationToken token)
public override List<Part> Nest(
List<NestItem> items,
IProgress<NestProgress>? progress,
CancellationToken token
)
{
progress?.Report(new NestProgress { Description = "legacy detail" });
return new List<Part>();
@@ -18,8 +18,9 @@ public class NestJobEngineSelectionTests
var defaultResult = new NestJobRunner(PlateNesterFactory.Create).Solve(job);
Assert.Equal(NestJobStatus.Complete, defaultResult.Status);
var stripResult = new NestJobRunner(PlateNesterFactory.Create)
.Solve(new NestJob(job.Parts, job.Plates, new NestJobOptions("Strip")));
var stripResult = new NestJobRunner(PlateNesterFactory.Create).Solve(
new NestJob(job.Parts, job.Plates, new NestJobOptions("Strip"))
);
Assert.Equal(NestJobStatus.Complete, stripResult.Status);
Assert.Equal(original, NestEngineRegistry.ActiveEngineName);
@@ -48,8 +49,10 @@ public class NestJobEngineSelectionTests
Assert.Throws<NotSupportedException>(() => PlateNesterFactory.Create("Not A Real Engine"));
var job = FiniteStockJobTests.Job(1);
Assert.Throws<NotSupportedException>(() =>
new NestJobRunner(key => throw new NotSupportedException($"Unknown placement strategy: {key}"))
.Solve(new NestJob(job.Parts, job.Plates, new NestJobOptions("Bogus"))));
new NestJobRunner(key =>
throw new NotSupportedException($"Unknown placement strategy: {key}")
).Solve(new NestJob(job.Parts, job.Plates, new NestJobOptions("Bogus")))
);
}
[Fact]
@@ -57,7 +60,11 @@ public class NestJobEngineSelectionTests
{
// A plugin engine registered through the legacy registry must not become selectable
// through the job factory; the new boundary is independent of registry state.
NestEngineRegistry.Register("ProbePlugin", "test plugin", plate => new PluginShapeEngine(plate));
NestEngineRegistry.Register(
"ProbePlugin",
"test plugin",
plate => new PluginShapeEngine(plate)
);
Assert.Contains(NestEngineRegistry.AvailableEngines, e => e.Name == "ProbePlugin");
Assert.Throws<NotSupportedException>(() => PlateNesterFactory.Create("ProbePlugin"));
@@ -68,10 +75,13 @@ public class NestJobEngineSelectionTests
public void StripEngineEndToEndPlacesAndAccounts()
{
var drawing = new Drawing("strip part", TestDrawingFactory.Rectangle(30, 30));
var job = new NestJob(new[] { DrawingJobMapper.FromDrawing("part", drawing, 2) },
new[] { new NestPlateStock("s", new Size(90, 90), 1) });
var result = new NestJobRunner(PlateNesterFactory.Create)
.Solve(new NestJob(job.Parts, job.Plates, new NestJobOptions("Strip")));
var job = new NestJob(
new[] { DrawingJobMapper.FromDrawing("part", drawing, 2) },
new[] { new NestPlateStock("s", new Size(90, 90), 1) }
);
var result = new NestJobRunner(PlateNesterFactory.Create).Solve(
new NestJob(job.Parts, job.Plates, new NestJobOptions("Strip"))
);
Assert.True(result.Plates.SelectMany(p => p.Placements).Count() >= 1);
foreach (var f in result.Fulfillment)
@@ -24,11 +24,24 @@ public class NestJobExampleTests
// Mixed inventory: five large sheets and unlimited small sheets.
new[]
{
new NestPlateStock("large", new Size(600.0, 400.0), quantity: 5, partSpacing: 2.0,
edgeSpacing: new Spacing(5.0, 5.0, 5.0, 5.0), quadrant: 1),
new NestPlateStock("small", new Size(300.0, 300.0), quantity: null, partSpacing: 2.0,
edgeSpacing: new Spacing(5.0, 5.0, 5.0, 5.0), quadrant: 1),
});
new NestPlateStock(
"large",
new Size(600.0, 400.0),
quantity: 5,
partSpacing: 2.0,
edgeSpacing: new Spacing(5.0, 5.0, 5.0, 5.0),
quadrant: 1
),
new NestPlateStock(
"small",
new Size(300.0, 300.0),
quantity: null,
partSpacing: 2.0,
edgeSpacing: new Spacing(5.0, 5.0, 5.0, 5.0),
quadrant: 1
),
}
);
var result = new NestJobRunner(PlateNesterFactory.Create).Solve(job);
@@ -36,22 +49,30 @@ public class NestJobExampleTests
Console.WriteLine($"Status: {result.Status}, stop reason: {result.StopReason}.");
foreach (var plate in result.Plates)
{
Console.WriteLine($"Plate {plate.PlateIndex} from stock '{plate.StockId}' " +
$"({plate.Stock.Size.Width} x {plate.Stock.Size.Length}):");
Console.WriteLine(
$"Plate {plate.PlateIndex} from stock '{plate.StockId}' "
+ $"({plate.Stock.Size.Width} x {plate.Stock.Size.Length}):"
);
foreach (var placement in plate.Placements)
Console.WriteLine($" {placement.PartId} #{placement.InstanceIndex} at " +
$"({placement.X:F1}, {placement.Y:F1}) rotated {placement.Rotation:F3} rad.");
Console.WriteLine(
$" {placement.PartId} #{placement.InstanceIndex} at "
+ $"({placement.X:F1}, {placement.Y:F1}) rotated {placement.Rotation:F3} rad."
);
}
// -- Every requirement reports exact fulfillment, including leftovers. --
foreach (var fulfillment in result.Fulfillment)
Console.WriteLine($"Requirement '{fulfillment.PartId}': requested {fulfillment.Requested}, " +
$"placed {fulfillment.Placed}, unplaced {fulfillment.Unplaced}.");
Console.WriteLine(
$"Requirement '{fulfillment.PartId}': requested {fulfillment.Requested}, "
+ $"placed {fulfillment.Placed}, unplaced {fulfillment.Unplaced}."
);
// -- Every stock line reports physical sheets used and remaining availability. --
foreach (var usage in result.StockUsage)
Console.WriteLine($"Stock '{usage.StockId}': used {usage.Used}, " +
$"remaining {(usage.Remaining.HasValue ? usage.Remaining.Value.ToString() : "unlimited")}.");
Console.WriteLine(
$"Stock '{usage.StockId}': used {usage.Used}, "
+ $"remaining {(usage.Remaining.HasValue ? usage.Remaining.Value.ToString() : "unlimited")}."
);
// Invariants the enumeration relies on: conservation per requirement and per stock line, no
// empty plates, every plate bound to supplied stock, and per-placement instance accounting.
@@ -64,28 +85,45 @@ public class NestJobExampleTests
{
var stock = job.Plates.First(candidate => candidate.Id == usage.StockId);
Assert.True(usage.Used >= 0);
Assert.Equal(stock.Quantity is int capacity ? capacity - usage.Used : (int?)null, usage.Remaining);
Assert.Equal(
stock.Quantity is int capacity ? capacity - usage.Used : (int?)null,
usage.Remaining
);
}
Assert.All(result.Plates, plate => Assert.NotEmpty(plate.Placements));
var plateCountByStock = result.Plates.GroupBy(plate => plate.StockId)
var plateCountByStock = result
.Plates.GroupBy(plate => plate.StockId)
.ToDictionary(group => group.Key, group => group.Count());
foreach (var usage in result.StockUsage)
Assert.Equal(usage.Used, plateCountByStock.GetValueOrDefault(usage.StockId));
var instanceIndicesByPart = result.Plates
.SelectMany(plate => plate.Placements)
var instanceIndicesByPart = result
.Plates.SelectMany(plate => plate.Placements)
.GroupBy(placement => placement.PartId)
.ToDictionary(group => group.Key, group => group.Select(placement => placement.InstanceIndex));
.ToDictionary(
group => group.Key,
group => group.Select(placement => placement.InstanceIndex)
);
foreach (var fulfillment in result.Fulfillment)
Assert.Equal(Enumerable.Range(0, fulfillment.Placed),
instanceIndicesByPart.GetValueOrDefault(fulfillment.PartId, new List<int>()).OrderBy(index => index));
Assert.Equal(
Enumerable.Range(0, fulfillment.Placed),
instanceIndicesByPart
.GetValueOrDefault(fulfillment.PartId, new List<int>())
.OrderBy(index => index)
);
// The default heuristic completes this synthetic job from the mixed inventory.
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.Equal(NestJobStopReason.Completed, result.StopReason);
Assert.Equal(5, result.Fulfillment.Single(fulfillment => fulfillment.PartId == "bracket").Placed);
Assert.Equal(8, result.Fulfillment.Single(fulfillment => fulfillment.PartId == "plate-clip").Placed);
Assert.Equal(
5,
result.Fulfillment.Single(fulfillment => fulfillment.PartId == "bracket").Placed
);
Assert.Equal(
8,
result.Fulfillment.Single(fulfillment => fulfillment.PartId == "plate-clip").Placed
);
}
[Fact]
@@ -94,9 +132,19 @@ public class NestJobExampleTests
// Same shape of job, but a plate budget forces an explicit partial result.
var job = new NestJob(
new[] { Part("part", 100.0, 100.0, 6, priority: 0) },
new[] { new NestPlateStock("sheet", new Size(220.0, 220.0), quantity: null, partSpacing: 2.0,
edgeSpacing: new Spacing(5.0, 5.0, 5.0, 5.0), quadrant: 1) },
new NestJobOptions("Default", maxPlates: 1));
new[]
{
new NestPlateStock(
"sheet",
new Size(220.0, 220.0),
quantity: null,
partSpacing: 2.0,
edgeSpacing: new Spacing(5.0, 5.0, 5.0, 5.0),
quadrant: 1
),
},
new NestJobOptions("Default", maxPlates: 1)
);
var result = new NestJobRunner(PlateNesterFactory.Create).Solve(job);
@@ -143,6 +191,17 @@ public class NestJobExampleTests
Assert.Same(drawing, placed.BaseDrawing);
}
private static NestJobPart Part(string id, double width, double length, int quantity, int priority) =>
new(id, PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(width, length)), quantity, priority);
private static NestJobPart Part(
string id,
double width,
double length,
int quantity,
int priority
) =>
new(
id,
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(width, length)),
quantity,
priority
);
}
@@ -7,12 +7,22 @@ public class NestJobGeometryTests
[Fact]
public void CandidateOutsideUsableWorkAreaFailsWithoutMutatingInput()
{
var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), 1);
var part = new NestJobPart(
"part",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)),
1
);
var sourceGeometry = part.Geometry.Motions.ToArray();
var stock = new NestPlateStock("stock", new Size(20, 30), 1,
edgeSpacing: new Spacing(2, 1, 3, 4));
var stock = new NestPlateStock(
"stock",
new Size(20, 30),
1,
edgeSpacing: new Spacing(2, 1, 3, 4)
);
var job = new NestJob(new[] { part }, new[] { stock });
var runner = new NestJobRunner(_ => new CandidateNester(new[] { new NestJobPlacement("part", 0, 26, 1, 0) }));
var runner = new NestJobRunner(_ => new CandidateNester(
new[] { new NestJobPlacement("part", 0, 26, 1, 0) }
));
Assert.Throws<InvalidOperationException>(() => runner.Solve(job));
@@ -28,48 +38,88 @@ public class NestJobGeometryTests
[InlineData(4, 0, -7)]
public void UnequalRectanglesFitAtEachQuadrantsUsableOrigin(int quadrant, double x, double y)
{
var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 3)), 1);
var part = new NestJobPart(
"part",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 3)),
1
);
var stock = new NestPlateStock("stock", new Size(7, 11), 1, quadrant: quadrant);
var job = new NestJob(new[] { part }, new[] { stock });
var result = Solve(job, new NestJobPlacement("part", 0, x, y, 0));
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.Equal(new NestJobPlacement("part", 0, x, y, 0), Assert.Single(result.Plates[0].Placements));
Assert.Equal(
new NestJobPlacement("part", 0, x, y, 0),
Assert.Single(result.Plates[0].Placements)
);
}
[Fact]
public void FixedAndBoundedRotationPoliciesRejectDisallowedAngles()
{
var fixedPart = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 2)), 1,
rotation: RotationPolicy.Fixed(System.Math.PI / 2));
var fixedJob = new NestJob(new[] { fixedPart }, new[] { new NestPlateStock("stock", new Size(10, 10), 1) });
Assert.Throws<InvalidOperationException>(() => Solve(fixedJob, new NestJobPlacement("part", 0, 0, 0, 0)));
var fixedPart = new NestJobPart(
"part",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 2)),
1,
rotation: RotationPolicy.Fixed(System.Math.PI / 2)
);
var fixedJob = new NestJob(
new[] { fixedPart },
new[] { new NestPlateStock("stock", new Size(10, 10), 1) }
);
Assert.Throws<InvalidOperationException>(() =>
Solve(fixedJob, new NestJobPlacement("part", 0, 0, 0, 0))
);
var fixedResult = Solve(fixedJob, new NestJobPlacement("part", 0, 2, 0, System.Math.PI / 2));
var fixedResult = Solve(
fixedJob,
new NestJobPlacement("part", 0, 2, 0, System.Math.PI / 2)
);
Assert.Equal(NestJobStatus.Complete, fixedResult.Status);
var boundedPart = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 2)), 1,
rotation: RotationPolicy.BoundedSweep(0, System.Math.PI / 2, System.Math.PI / 4));
var boundedJob = new NestJob(new[] { boundedPart }, new[] { new NestPlateStock("stock", new Size(10, 10), 1) });
Assert.Throws<InvalidOperationException>(() => Solve(boundedJob,
new NestJobPlacement("part", 0, 2, 0, System.Math.PI / 3)));
var boundedPart = new NestJobPart(
"part",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 2)),
1,
rotation: RotationPolicy.BoundedSweep(0, System.Math.PI / 2, System.Math.PI / 4)
);
var boundedJob = new NestJob(
new[] { boundedPart },
new[] { new NestPlateStock("stock", new Size(10, 10), 1) }
);
Assert.Throws<InvalidOperationException>(() =>
Solve(boundedJob, new NestJobPlacement("part", 0, 2, 0, System.Math.PI / 3))
);
var boundedResult = Solve(boundedJob, new NestJobPlacement("part", 0, 2, 0, System.Math.PI / 4));
var boundedResult = Solve(
boundedJob,
new NestJobPlacement("part", 0, 2, 0, System.Math.PI / 4)
);
Assert.Equal(NestJobStatus.Complete, boundedResult.Status);
}
[Fact]
public void EdgeTouchingIsAllowedAtZeroSpacingAndRejectedAtPositiveSpacing()
{
var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)), 2);
var part = new NestJobPart(
"part",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)),
2
);
var touching = new[]
{
new NestJobPlacement("part", 0, 0, 0, 0),
new NestJobPlacement("part", 1, 2, 0, 0)
new NestJobPlacement("part", 1, 2, 0, 0),
};
var zeroSpacing = new NestJob(new[] { part }, new[] { new NestPlateStock("stock", new Size(10, 10), 1) });
var positiveSpacing = new NestJob(new[] { part }, new[] { new NestPlateStock("stock", new Size(10, 10), 1, 0.1) });
var zeroSpacing = new NestJob(
new[] { part },
new[] { new NestPlateStock("stock", new Size(10, 10), 1) }
);
var positiveSpacing = new NestJob(
new[] { part },
new[] { new NestPlateStock("stock", new Size(10, 10), 1, 0.1) }
);
Assert.Equal(NestJobStatus.Complete, Solve(zeroSpacing, touching).Status);
Assert.Throws<InvalidOperationException>(() => Solve(positiveSpacing, touching));
@@ -78,20 +128,39 @@ public class NestJobGeometryTests
[Fact]
public void OverlapAndContainmentAreRejected()
{
var outer = new NestJobPart("outer", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 6)), 1);
var inner = new NestJobPart("inner", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)), 1);
var job = new NestJob(new[] { outer, inner }, new[] { new NestPlateStock("stock", new Size(20, 20), 1) });
var outer = new NestJobPart(
"outer",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 6)),
1
);
var inner = new NestJobPart(
"inner",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)),
1
);
var job = new NestJob(
new[] { outer, inner },
new[] { new NestPlateStock("stock", new Size(20, 20), 1) }
);
Assert.Throws<InvalidOperationException>(() => Solve(job,
new NestJobPlacement("outer", 0, 0, 0, 0),
new NestJobPlacement("inner", 0, 2, 2, 0)));
Assert.Throws<InvalidOperationException>(() =>
Solve(
job,
new NestJobPlacement("outer", 0, 0, 0, 0),
new NestJobPlacement("inner", 0, 2, 2, 0)
)
);
}
[Fact]
public void EmptyStockStopsWithoutCallingCandidateNester()
{
var nester = new CountingNester();
var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)), 1);
var part = new NestJobPart(
"part",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)),
1
);
var job = new NestJob(new[] { part }, Array.Empty<NestPlateStock>());
var result = new NestJobRunner(_ => nester).Solve(job);
@@ -115,25 +184,34 @@ public class NestJobGeometryTests
var materialized = NestResultMaterializer.Materialize(job, result);
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.All(result.Fulfillment, fulfillment => Assert.Equal(fulfillment.Requested,
fulfillment.Placed + fulfillment.Unplaced));
Assert.All(
result.Fulfillment,
fulfillment =>
Assert.Equal(fulfillment.Requested, fulfillment.Placed + fulfillment.Unplaced)
);
Assert.Equal(sourceGeometry, job.Parts[0].Geometry.Motions);
Assert.Equal(3, job.Parts[0].Quantity);
Assert.Equal(1, job.Plates[0].Quantity);
Assert.All(materialized.Nest.Plates, plate =>
{
var workArea = plate.WorkArea();
Assert.All(plate.Parts, placed =>
Assert.All(
materialized.Nest.Plates,
plate =>
{
Assert.True(placed.BoundingBox.Left >= workArea.Left - 1e-6);
Assert.True(placed.BoundingBox.Right <= workArea.Right + 1e-6);
Assert.True(placed.BoundingBox.Bottom >= workArea.Bottom - 1e-6);
Assert.True(placed.BoundingBox.Top <= workArea.Top + 1e-6);
});
for (var left = 0; left < plate.Parts.Count; left++)
var workArea = plate.WorkArea();
Assert.All(
plate.Parts,
placed =>
{
Assert.True(placed.BoundingBox.Left >= workArea.Left - 1e-6);
Assert.True(placed.BoundingBox.Right <= workArea.Right + 1e-6);
Assert.True(placed.BoundingBox.Bottom >= workArea.Bottom - 1e-6);
Assert.True(placed.BoundingBox.Top <= workArea.Top + 1e-6);
}
);
for (var left = 0; left < plate.Parts.Count; left++)
for (var right = left + 1; right < plate.Parts.Count; right++)
Assert.False(plate.Parts[left].Intersects(plate.Parts[right], out _));
});
}
);
}
private static NestJobResult Solve(NestJob job, params NestJobPlacement[] placements) =>
@@ -141,16 +219,22 @@ public class NestJobGeometryTests
private sealed class CandidateNester(IEnumerable<NestJobPlacement> placements) : IPlateNester
{
public PlateCandidate Place(PlatePlacementRequest request, IProgress<NestJobProgress>? progress = null,
CancellationToken token = default) => new(placements);
public PlateCandidate Place(
PlatePlacementRequest request,
IProgress<NestJobProgress>? progress = null,
CancellationToken token = default
) => new(placements);
}
private sealed class CountingNester : IPlateNester
{
public int Calls { get; private set; }
public PlateCandidate Place(PlatePlacementRequest request, IProgress<NestJobProgress>? progress = null,
CancellationToken token = default)
public PlateCandidate Place(
PlatePlacementRequest request,
IProgress<NestJobProgress>? progress = null,
CancellationToken token = default
)
{
Calls++;
return new PlateCandidate(Array.Empty<NestJobPlacement>());
@@ -16,17 +16,22 @@ public class NestJobIdentityTests
{
var a = new Drawing("identical", TestDrawingFactory.Rectangle(40, 40));
var b = new Drawing("identical", TestDrawingFactory.Rectangle(40, 40));
var job = new NestJob(new[]
{
DrawingJobMapper.FromDrawing("a", a, 2),
DrawingJobMapper.FromDrawing("b", b, 2)
}, new[] { new NestPlateStock("s", new Size(90, 90), 1) });
var job = new NestJob(
new[]
{
DrawingJobMapper.FromDrawing("a", a, 2),
DrawingJobMapper.FromDrawing("b", b, 2),
},
new[] { new NestPlateStock("s", new Size(90, 90), 1) }
);
var result = new NestJobRunner(LegacyPlateNesterAdapter.Create).Solve(job);
// Every placed part maps to a known requirement ID; no part is invented or cross-counted.
Assert.True(result.Plates.SelectMany(p => p.Placements).All(p => p.PartId is "a" or "b"));
var counts = result.Plates.SelectMany(p => p.Placements).GroupBy(p => p.PartId)
var counts = result
.Plates.SelectMany(p => p.Placements)
.GroupBy(p => p.PartId)
.ToDictionary(g => g.Key, g => g.Count());
foreach (var (id, placed) in counts)
Assert.True(placed <= 2, $"Requirement {id} placed {placed} > requested 2");
@@ -40,11 +45,14 @@ public class NestJobIdentityTests
public void TwoRequirementsOnSameSourceDrawingKeepIndependentQuantities()
{
var source = new Drawing("shared", TestDrawingFactory.Rectangle(30, 30));
var job = new NestJob(new[]
{
DrawingJobMapper.FromDrawing("first", source, 2),
DrawingJobMapper.FromDrawing("second", source, 2)
}, new[] { new NestPlateStock("s", new Size(90, 90), 1) });
var job = new NestJob(
new[]
{
DrawingJobMapper.FromDrawing("first", source, 2),
DrawingJobMapper.FromDrawing("second", source, 2),
},
new[] { new NestPlateStock("s", new Size(90, 90), 1) }
);
var result = new NestJobRunner(LegacyPlateNesterAdapter.Create).Solve(job);
@@ -72,7 +80,7 @@ public class NestJobIdentityTests
var items = new List<NestItem>
{
new() { Drawing = a, Quantity = 2 },
new() { Drawing = b, Quantity = 2 }
new() { Drawing = b, Quantity = 2 },
};
// Place exactly 2 parts from item A and none from item B, then run the base-class
// deduction. Deterministic regardless of any fill heuristic.
@@ -99,7 +107,7 @@ public class NestJobIdentityTests
var items = new List<NestItem>
{
new() { Drawing = a, Quantity = 1 },
new() { Drawing = b, Quantity = 1 }
new() { Drawing = b, Quantity = 1 },
};
var placed = new BaseNestEngineProbe(plate).Nest(items, null, default);
Assert.Equal(2, placed.Count);
@@ -110,15 +118,27 @@ public class NestJobIdentityTests
{
public override string Name => "probe";
public override string Description => "probe";
public override List<Part> Fill(NestItem item, Box workArea,
IProgress<NestProgress> progress, CancellationToken token)
=> new DefaultNestEngine(Plate).Fill(item, workArea, progress, token);
public override List<Part> Fill(List<Part> groupParts, Box workArea,
IProgress<NestProgress> progress, CancellationToken token)
=> new DefaultNestEngine(Plate).Fill(groupParts, workArea, progress, token);
public override List<Part> PackArea(Box box, List<NestItem> items,
IProgress<NestProgress> progress, CancellationToken token)
=> new DefaultNestEngine(Plate).PackArea(box, items, progress, token);
public override List<Part> Fill(
NestItem item,
Box workArea,
IProgress<NestProgress> progress,
CancellationToken token
) => new DefaultNestEngine(Plate).Fill(item, workArea, progress, token);
public override List<Part> Fill(
List<Part> groupParts,
Box workArea,
IProgress<NestProgress> progress,
CancellationToken token
) => new DefaultNestEngine(Plate).Fill(groupParts, workArea, progress, token);
public override List<Part> PackArea(
Box box,
List<NestItem> items,
IProgress<NestProgress> progress,
CancellationToken token
) => new DefaultNestEngine(Plate).PackArea(box, items, progress, token);
}
/// <summary>Places exactly 2 parts from the first multi-quantity item and none from the
@@ -128,10 +148,16 @@ public class NestJobIdentityTests
private int _first = -1;
public override string Name => "starving";
public override string Description => "starves all but the first fill item";
public override List<Part> Fill(NestItem item, Box workArea,
IProgress<NestProgress> progress, CancellationToken token)
public override List<Part> Fill(
NestItem item,
Box workArea,
IProgress<NestProgress> progress,
CancellationToken token
)
{
if (_first < 0) _first = 1;
if (_first < 0)
_first = 1;
if (_first++ != 1)
return new List<Part>();
var parts = new List<Part>();
@@ -10,12 +10,19 @@ public class NestJobRunnerTests
{
var fake = new FakePlateNester();
var factoryCalls = 0;
var runner = new NestJobRunner(_ => { factoryCalls++; return fake; });
var job = new NestJob(Array.Empty<NestJobPart>(), new[]
var runner = new NestJobRunner(_ =>
{
new NestPlateStock("finite", new Size(100, 200), 2),
new NestPlateStock("unlimited", new Size(100, 200))
factoryCalls++;
return fake;
});
var job = new NestJob(
Array.Empty<NestJobPart>(),
new[]
{
new NestPlateStock("finite", new Size(100, 200), 2),
new NestPlateStock("unlimited", new Size(100, 200)),
}
);
var result = runner.Solve(job);
@@ -23,9 +30,19 @@ public class NestJobRunnerTests
Assert.Equal(NestJobStopReason.Completed, result.StopReason);
Assert.Empty(result.Plates);
Assert.Empty(result.Fulfillment);
Assert.Collection(result.StockUsage,
usage => { Assert.Equal(0, usage.Used); Assert.Equal(2, usage.Remaining); },
usage => { Assert.Equal(0, usage.Used); Assert.Null(usage.Remaining); });
Assert.Collection(
result.StockUsage,
usage =>
{
Assert.Equal(0, usage.Used);
Assert.Equal(2, usage.Remaining);
},
usage =>
{
Assert.Equal(0, usage.Used);
Assert.Null(usage.Remaining);
}
);
Assert.Equal(0, factoryCalls);
Assert.Equal(0, fake.Calls);
}
@@ -37,13 +54,19 @@ public class NestJobRunnerTests
cancellation.Cancel();
var runner = new NestJobRunner(_ => new FakePlateNester());
var job = new NestJob(Array.Empty<NestJobPart>(), Array.Empty<NestPlateStock>());
Assert.Throws<OperationCanceledException>(() => runner.Solve(job, token: cancellation.Token));
Assert.Throws<OperationCanceledException>(() =>
runner.Solve(job, token: cancellation.Token)
);
}
[Fact]
public void EmptyStockReturnsIncomplete()
{
var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle()), 1);
var part = new NestJobPart(
"part",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle()),
1
);
var job = new NestJob(new[] { part }, Array.Empty<NestPlateStock>());
var runner = new NestJobRunner(_ => new FakePlateNester());
var result = runner.Solve(job);
@@ -64,7 +87,11 @@ public class NestJobRunnerTests
var edges = new Spacing(1, 2, 3, 4);
var stocks = new List<NestPlateStock> { new("s", size, 0, 2, edges, 3) };
var job = new NestJob(parts, stocks);
parts.Clear(); stocks.Clear(); program.Codes.Clear(); size.Width = 0; edges.Left = 999;
parts.Clear();
stocks.Clear();
program.Codes.Clear();
size.Width = 0;
edges.Left = 999;
Assert.Single(job.Parts);
Assert.Equal(3, job.Parts[0].Quantity);
@@ -92,8 +119,12 @@ public class NestJobRunnerTests
private sealed class FakePlateNester : IPlateNester
{
public int Calls { get; private set; }
public PlateCandidate Place(PlatePlacementRequest request, IProgress<NestJobProgress>? progress = null,
CancellationToken token = default)
public PlateCandidate Place(
PlatePlacementRequest request,
IProgress<NestJobProgress>? progress = null,
CancellationToken token = default
)
{
Calls++;
return new PlateCandidate(Array.Empty<NestJobPlacement>());
@@ -7,8 +7,11 @@ public class NestJobStockSelectionTests
[Fact]
public void LaterFittingStockWinsWhenFirstStockCannotPlace()
{
var result = Solve(new[] { Part("p", 1) }, new[] { Stock("small", 10, 10, 1), Stock("large", 20, 20, 1) },
request => request.Stock.Id == "large" ? Candidate(request, "p") : Empty());
var result = Solve(
new[] { Part("p", 1) },
new[] { Stock("small", 10, 10, 1), Stock("large", 20, 20, 1) },
request => request.Stock.Id == "large" ? Candidate(request, "p") : Empty()
);
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.Equal("large", Assert.Single(result.Plates).StockId);
@@ -17,58 +20,103 @@ public class NestJobStockSelectionTests
[Fact]
public void ExhaustedLargeStockIsNotRecreatedWhileSmallerStockServesSmallParts()
{
var result = Solve(new[] { Part("large", 1, 0), Part("small", 2, 1) },
new[] { Stock("large", 20, 20, 1), Stock("small", 10, 10, 2) }, request => request.Stock.Id switch
{
"large" when request.Parts.Any(part => part.Id == "large") => Candidate(request, "large"),
"small" when request.Parts.Any(part => part.Id == "small") => Candidate(request, "small"),
_ => Empty()
});
var result = Solve(
new[] { Part("large", 1, 0), Part("small", 2, 1) },
new[] { Stock("large", 20, 20, 1), Stock("small", 10, 10, 2) },
request =>
request.Stock.Id switch
{
"large" when request.Parts.Any(part => part.Id == "large") => Candidate(
request,
"large"
),
"small" when request.Parts.Any(part => part.Id == "small") => Candidate(
request,
"small"
),
_ => Empty(),
}
);
Assert.Equal(new[] { "large", "small", "small" }, result.Plates.Select(plate => plate.StockId));
Assert.Collection(result.StockUsage,
Assert.Equal(
new[] { "large", "small", "small" },
result.Plates.Select(plate => plate.StockId)
);
Assert.Collection(
result.StockUsage,
usage => Assert.Equal(new StockUsage("large", 1, 0), usage),
usage => Assert.Equal(new StockUsage("small", 2, 0), usage));
usage => Assert.Equal(new StockUsage("small", 2, 0), usage)
);
}
[Fact]
public void EqualDimensionsWithDifferentStockIdsRemainIndependent()
{
var result = Solve(new[] { Part("p", 2) }, new[] { Stock("first", 10, 10, 1), Stock("second", 10, 10, 1) },
request => Candidate(request, "p"));
var result = Solve(
new[] { Part("p", 2) },
new[] { Stock("first", 10, 10, 1), Stock("second", 10, 10, 1) },
request => Candidate(request, "p")
);
Assert.Equal(new[] { "first", "second" }, result.Plates.Select(plate => plate.StockId));
Assert.Equal(new[] { new StockUsage("first", 1, 0), new StockUsage("second", 1, 0) }, result.StockUsage);
Assert.Equal(
new[] { new StockUsage("first", 1, 0), new StockUsage("second", 1, 0) },
result.StockUsage
);
}
[Fact]
public void LosingTrialsDoNotConsumeStockPartsOrDrawingCounters()
{
var calls = new List<(string Stock, int Quantity)>();
var result = Solve(new[] { Part("p", 2) }, new[] { Stock("wide", 20, 20, 2), Stock("narrow", 10, 10, 2) }, request =>
{
calls.Add((request.Stock.Id, request.Parts.Single().Quantity));
return request.Stock.Id == "wide" ? Candidate(request, "p", 0, 100) : Candidate(request, "p", 0, 0);
});
var result = Solve(
new[] { Part("p", 2) },
new[] { Stock("wide", 20, 20, 2), Stock("narrow", 10, 10, 2) },
request =>
{
calls.Add((request.Stock.Id, request.Parts.Single().Quantity));
return request.Stock.Id == "wide"
? Candidate(request, "p", 0, 100)
: Candidate(request, "p", 0, 0);
}
);
Assert.Equal(new[] { "narrow", "narrow" }, result.Plates.Select(plate => plate.StockId));
Assert.Equal(new[] { ("wide", 2), ("narrow", 2), ("wide", 1), ("narrow", 1) }, calls);
Assert.Equal(new StockUsage("wide", 0, 2), result.StockUsage[0]);
Assert.Equal(new StockUsage("narrow", 2, 0), result.StockUsage[1]);
Assert.Equal(new[] { 0, 1 }, result.Plates.SelectMany(plate => plate.Placements).Select(placement => placement.InstanceIndex));
Assert.Equal(
new[] { 0, 1 },
result
.Plates.SelectMany(plate => plate.Placements)
.Select(placement => placement.InstanceIndex)
);
}
[Fact]
public void CandidatePriorityAreaEnvelopeAndInputOrderAreComparedInDocumentedOrder()
{
var priority = Solve(new[] { Part("high", 1, 0), Part("low", 1, 1) }, new[] { Stock("a", 10, 10, 1), Stock("b", 10, 10, 1) },
request => request.Stock.Id == "a" ? Candidate(request, "low") : Candidate(request, "high"));
var area = Solve(new[] { Part("p", 1) }, new[] { Stock("large", 20, 20, 1), Stock("small", 10, 10, 1) },
request => Candidate(request, "p"));
var envelope = Solve(new[] { Part("p", 2) }, new[] { Stock("a", 10, 10, 1), Stock("b", 10, 10, 1) },
request => request.Stock.Id == "a" ? CandidatePair("p", 4, 5) : CandidatePair("p", 4, 0));
var inputOrder = Solve(new[] { Part("p", 1) }, new[] { Stock("first", 10, 10, 1), Stock("second", 10, 10, 1) },
request => Candidate(request, "p"));
var priority = Solve(
new[] { Part("high", 1, 0), Part("low", 1, 1) },
new[] { Stock("a", 10, 10, 1), Stock("b", 10, 10, 1) },
request =>
request.Stock.Id == "a" ? Candidate(request, "low") : Candidate(request, "high")
);
var area = Solve(
new[] { Part("p", 1) },
new[] { Stock("large", 20, 20, 1), Stock("small", 10, 10, 1) },
request => Candidate(request, "p")
);
var envelope = Solve(
new[] { Part("p", 2) },
new[] { Stock("a", 10, 10, 1), Stock("b", 10, 10, 1) },
request => request.Stock.Id == "a" ? CandidatePair("p", 4, 5) : CandidatePair("p", 4, 0)
);
var inputOrder = Solve(
new[] { Part("p", 1) },
new[] { Stock("first", 10, 10, 1), Stock("second", 10, 10, 1) },
request => Candidate(request, "p")
);
Assert.Equal("b", priority.Plates[0].StockId);
Assert.Equal("small", area.Plates[0].StockId);
@@ -79,8 +127,17 @@ public class NestJobStockSelectionTests
[Fact]
public void UnlimitedStockStopsWhenDemandIsFulfilledAndPlateLimitLeavesLeftovers()
{
var unlimited = Solve(new[] { Part("p", 2) }, new[] { Stock("u", 10, 10, null) }, request => Candidate(request, "p"));
var limited = Solve(new[] { Part("p", 3) }, new[] { Stock("u", 10, 10, null) }, request => Candidate(request, "p"), new NestJobOptions(maxPlates: 2));
var unlimited = Solve(
new[] { Part("p", 2) },
new[] { Stock("u", 10, 10, null) },
request => Candidate(request, "p")
);
var limited = Solve(
new[] { Part("p", 3) },
new[] { Stock("u", 10, 10, null) },
request => Candidate(request, "p"),
new NestJobOptions(maxPlates: 2)
);
Assert.Equal(NestJobStopReason.Completed, unlimited.StopReason);
Assert.Equal(2, unlimited.Plates.Count);
@@ -88,32 +145,51 @@ public class NestJobStockSelectionTests
Assert.Equal(new PartFulfillment("p", 3, 2, 1), Assert.Single(limited.Fulfillment));
}
private static NestJobResult Solve(IEnumerable<NestJobPart> parts, IEnumerable<NestPlateStock> stock,
Func<PlatePlacementRequest, PlateCandidate> place, NestJobOptions? options = null) =>
new NestJobRunner(_ => new Nester(place)).Solve(new NestJob(parts, stock, options));
private static NestJobResult Solve(
IEnumerable<NestJobPart> parts,
IEnumerable<NestPlateStock> stock,
Func<PlatePlacementRequest, PlateCandidate> place,
NestJobOptions? options = null
) => new NestJobRunner(_ => new Nester(place)).Solve(new NestJob(parts, stock, options));
private static NestJobPart Part(string id, int quantity, int priority = 0) =>
new(id, PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 5)), quantity, priority);
new(
id,
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 5)),
quantity,
priority
);
private static NestPlateStock Stock(string id, double width, double length, int? quantity) =>
new(id, new Size(width, length), quantity);
private static PlateCandidate Candidate(PlatePlacementRequest request, string id, double firstX = 0, double secondX = 0)
private static PlateCandidate Candidate(
PlatePlacementRequest request,
string id,
double firstX = 0,
double secondX = 0
)
{
return new PlateCandidate(new[] { new NestJobPlacement(id, 0, firstX, 0, 0) });
}
private static PlateCandidate CandidatePair(string id, double secondX, double secondY) => new(new[]
{
new NestJobPlacement(id, 0, 0, 0, 0),
new NestJobPlacement(id, 1, secondX, secondY, 0)
});
private static PlateCandidate CandidatePair(string id, double secondX, double secondY) =>
new(
new[]
{
new NestJobPlacement(id, 0, 0, 0, 0),
new NestJobPlacement(id, 1, secondX, secondY, 0),
}
);
private static PlateCandidate Empty() => new(Array.Empty<NestJobPlacement>());
private sealed class Nester(Func<PlatePlacementRequest, PlateCandidate> place) : IPlateNester
{
public PlateCandidate Place(PlatePlacementRequest request, IProgress<NestJobProgress>? progress = null,
CancellationToken token = default) => place(request);
public PlateCandidate Place(
PlatePlacementRequest request,
IProgress<NestJobProgress>? progress = null,
CancellationToken token = default
) => place(request);
}
}
@@ -14,10 +14,10 @@ public class NestJobValidationTests
program.LineTo(0, 3);
program.LineTo(-4, 0);
program.LineTo(0, -3);
var job = new NestJob(new[]
{
new NestJobPart("part", PartGeometrySnapshot.FromProgram(program), 1)
}, new[] { new NestPlateStock("stock", new Size(10, 10), 1) });
var job = new NestJob(
new[] { new NestJobPart("part", PartGeometrySnapshot.FromProgram(program), 1) },
new[] { new NestPlateStock("stock", new Size(10, 10), 1) }
);
var result = Solve(job, new NestJobPlacement("part", 0, 0, 0, 0));
@@ -28,13 +28,26 @@ public class NestJobValidationTests
[Fact]
public void CandidateInsideAnotherRequirementsHoleDoesNotOverlapMaterial()
{
var outer = new NestJobPart("outer", PartGeometrySnapshot.FromProgram(RectangleWithHole()), 1);
var inner = new NestJobPart("inner", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)), 1);
var job = new NestJob(new[] { outer, inner }, new[] { new NestPlateStock("stock", new Size(20, 20), 1) });
var outer = new NestJobPart(
"outer",
PartGeometrySnapshot.FromProgram(RectangleWithHole()),
1
);
var inner = new NestJobPart(
"inner",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)),
1
);
var job = new NestJob(
new[] { outer, inner },
new[] { new NestPlateStock("stock", new Size(20, 20), 1) }
);
var result = Solve(job,
var result = Solve(
job,
new NestJobPlacement("outer", 0, 0, 0, 0),
new NestJobPlacement("inner", 0, 4, 4, 0));
new NestJobPlacement("inner", 0, 4, 4, 0)
);
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.Single(result.Plates);
@@ -44,12 +57,23 @@ public class NestJobValidationTests
[Fact]
public void SmallCornerOverlapIsRejected()
{
var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(10, 10)), 2);
var job = new NestJob(new[] { part }, new[] { new NestPlateStock("stock", new Size(20, 20), 1) });
var part = new NestJobPart(
"part",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(10, 10)),
2
);
var job = new NestJob(
new[] { part },
new[] { new NestPlateStock("stock", new Size(20, 20), 1) }
);
Assert.Throws<InvalidOperationException>(() => Solve(job,
new NestJobPlacement("part", 0, 0, 0, 0),
new NestJobPlacement("part", 1, 9, 9, 0)));
Assert.Throws<InvalidOperationException>(() =>
Solve(
job,
new NestJobPlacement("part", 0, 0, 0, 0),
new NestJobPlacement("part", 1, 9, 9, 0)
)
);
}
[Theory]
@@ -57,12 +81,21 @@ public class NestJobValidationTests
[InlineData(10.0, 10.0)]
public void BoundaryContactWithZeroSpacingIsAccepted(double x, double y)
{
var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(10, 10)), 2);
var job = new NestJob(new[] { part }, new[] { new NestPlateStock("stock", new Size(20, 20), 1) });
var part = new NestJobPart(
"part",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(10, 10)),
2
);
var job = new NestJob(
new[] { part },
new[] { new NestPlateStock("stock", new Size(20, 20), 1) }
);
var result = Solve(job,
var result = Solve(
job,
new NestJobPlacement("part", 0, 0, 0, 0),
new NestJobPlacement("part", 1, x, y, 0));
new NestJobPlacement("part", 1, x, y, 0)
);
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.Equal(2, Assert.Single(result.Plates).Placements.Count);
@@ -72,16 +105,24 @@ public class NestJobValidationTests
public void UnknownOrOverproducingCandidateFailsBeforeCommitWithoutChangingInput()
{
var reports = new List<NestJobProgress>();
var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)), 1);
var part = new NestJobPart(
"part",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)),
1
);
var stock = new NestPlateStock("stock", new Size(20, 20), 1);
var job = new NestJob(new[] { part }, new[] { stock });
var runner = new NestJobRunner(_ => new CandidateNester(new[]
{
new NestJobPlacement("part", 0, 0, 0, 0),
new NestJobPlacement("unknown", 0, 4, 0, 0)
}));
var runner = new NestJobRunner(_ => new CandidateNester(
new[]
{
new NestJobPlacement("part", 0, 0, 0, 0),
new NestJobPlacement("unknown", 0, 4, 0, 0),
}
));
Assert.Throws<InvalidOperationException>(() => runner.Solve(job, new InlineProgress(reports.Add)));
Assert.Throws<InvalidOperationException>(() =>
runner.Solve(job, new InlineProgress(reports.Add))
);
Assert.Equal(1, job.Parts[0].Quantity);
Assert.Equal(1, job.Plates[0].Quantity);
@@ -91,12 +132,23 @@ public class NestJobValidationTests
[Fact]
public void CandidateThatOverproducesIsRejectedRatherThanClamped()
{
var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)), 1);
var job = new NestJob(new[] { part }, new[] { new NestPlateStock("stock", new Size(20, 20), 1) });
var part = new NestJobPart(
"part",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)),
1
);
var job = new NestJob(
new[] { part },
new[] { new NestPlateStock("stock", new Size(20, 20), 1) }
);
Assert.Throws<InvalidOperationException>(() => Solve(job,
new NestJobPlacement("part", 0, 0, 0, 0),
new NestJobPlacement("part", 1, 4, 0, 0)));
Assert.Throws<InvalidOperationException>(() =>
Solve(
job,
new NestJobPlacement("part", 0, 0, 0, 0),
new NestJobPlacement("part", 1, 4, 0, 0)
)
);
Assert.Equal(1, job.Parts[0].Quantity);
Assert.Equal(1, job.Plates[0].Quantity);
@@ -110,14 +162,20 @@ public class NestJobValidationTests
var program = new Program();
program.MoveTo(0, 0);
program.LineTo(4, 0);
if (zeroLength) program.LineTo(4, 0);
if (zeroLength)
program.LineTo(4, 0);
program.LineTo(4, 3);
program.LineTo(0, 3);
if (zeroLength) program.LineTo(0, 0);
var job = new NestJob(new[] { new NestJobPart("part", PartGeometrySnapshot.FromProgram(program), 1) },
new[] { new NestPlateStock("stock", new Size(20, 20), 1) });
if (zeroLength)
program.LineTo(0, 0);
var job = new NestJob(
new[] { new NestJobPart("part", PartGeometrySnapshot.FromProgram(program), 1) },
new[] { new NestPlateStock("stock", new Size(20, 20), 1) }
);
Assert.Throws<ArgumentException>(() => new NestJobRunner(_ => new CandidateNester(Array.Empty<NestJobPlacement>())).Solve(job));
Assert.Throws<ArgumentException>(() =>
new NestJobRunner(_ => new CandidateNester(Array.Empty<NestJobPlacement>())).Solve(job)
);
}
private static NestJobResult Solve(NestJob job, params NestJobPlacement[] placements) =>
@@ -136,8 +194,11 @@ public class NestJobValidationTests
private sealed class CandidateNester(IEnumerable<NestJobPlacement> placements) : IPlateNester
{
public PlateCandidate Place(PlatePlacementRequest request, IProgress<NestJobProgress>? progress = null,
CancellationToken token = default) => new(placements);
public PlateCandidate Place(
PlatePlacementRequest request,
IProgress<NestJobProgress>? progress = null,
CancellationToken token = default
) => new(placements);
}
private sealed class InlineProgress(Action<NestJobProgress> report) : IProgress<NestJobProgress>
@@ -32,7 +32,11 @@ public class NestingEngineRegistryTests
{
var before = NestingEngineRegistry.AvailableEngines.Count;
NestingEngineRegistry.Register("Default", "duplicate", () => new FixedStrategyNestingEngine("Default"));
NestingEngineRegistry.Register(
"Default",
"duplicate",
() => new FixedStrategyNestingEngine("Default")
);
Assert.Equal(before, NestingEngineRegistry.AvailableEngines.Count);
}
@@ -42,7 +46,9 @@ public class NestingEngineRegistryTests
{
var before = NestingEngineRegistry.AvailableEngines.Count;
NestingEngineRegistry.LoadPlugins(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()));
NestingEngineRegistry.LoadPlugins(
Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())
);
Assert.Equal(before, NestingEngineRegistry.AvailableEngines.Count);
}
@@ -17,8 +17,11 @@ public class PlateNesterParityTests
private static readonly Size PlateSize = new(30, 50);
private static readonly Spacing Edge = new(1, 1, 1, 1);
private static NestJob Job(IReadOnlyList<NestJobPart> parts, int? stockQuantity = 3,
string strategy = "Default")
private static NestJob Job(
IReadOnlyList<NestJobPart> parts,
int? stockQuantity = 3,
string strategy = "Default"
)
{
var stock = new NestPlateStock("stock", PlateSize, stockQuantity, 1, Edge);
return new NestJob(parts, new[] { stock }, new NestJobOptions(strategy));
@@ -45,8 +48,10 @@ public class PlateNesterParityTests
Assert.Equal(lSorted[j].PartId, rSorted[j].PartId);
Assert.Equal(lSorted[j].X, rSorted[j].X, 6);
Assert.Equal(lSorted[j].Y, rSorted[j].Y, 6);
Assert.True(AnglesEqual(lSorted[j].Rotation, rSorted[j].Rotation),
$"rotation differs: {lSorted[j].Rotation} vs {rSorted[j].Rotation}");
Assert.True(
AnglesEqual(lSorted[j].Rotation, rSorted[j].Rotation),
$"rotation differs: {lSorted[j].Rotation} vs {rSorted[j].Rotation}"
);
}
}
}
@@ -54,8 +59,8 @@ public class PlateNesterParityTests
private static bool AnglesEqual(double left, double right)
{
var delta = (left - right) % (System.Math.PI * 2);
return System.Math.Abs(delta) <= Tolerance ||
System.Math.Abs(System.Math.Abs(delta) - System.Math.PI * 2) <= Tolerance;
return System.Math.Abs(delta) <= Tolerance
|| System.Math.Abs(System.Math.Abs(delta) - System.Math.PI * 2) <= Tolerance;
}
[Fact]
@@ -63,18 +68,32 @@ public class PlateNesterParityTests
{
var parts = new[]
{
new NestJobPart("a", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)), 4),
new NestJobPart("b", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), 3)
new NestJobPart(
"a",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)),
4
),
new NestJobPart(
"b",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)),
3
),
};
var legacy = Solve(new LegacyPlateNesterAdapter(plate => new DefaultNestEngine(plate)), Job(parts));
var legacy = Solve(
new LegacyPlateNesterAdapter(plate => new DefaultNestEngine(plate)),
Job(parts)
);
var migrated = Solve(new DefaultPlateNester(), Job(parts));
Assert.Equal(legacy.Status, migrated.Status);
Assert.Equal(NestJobStatus.Complete, migrated.Status);
Assert.Equal(ByPart(legacy), ByPart(migrated));
foreach (var usage in legacy.StockUsage)
Assert.Equal(usage.Used, migrated.StockUsage.First(u => u.StockId == usage.StockId).Used);
Assert.Equal(
usage.Used,
migrated.StockUsage.First(u => u.StockId == usage.StockId).Used
);
// Automatic-rotation rectangles on a single stock size are deterministic: identical layouts.
AssertLayoutsIdentical(legacy, migrated);
}
@@ -84,19 +103,31 @@ public class PlateNesterParityTests
{
var parts = new[]
{
new NestJobPart("a", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)), 4),
new NestJobPart("b", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), 3)
new NestJobPart(
"a",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)),
4
),
new NestJobPart(
"b",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)),
3
),
};
var legacy = Solve(new LegacyPlateNesterAdapter(plate => new StripNestEngine(plate)),
Job(parts, strategy: "Strip"));
var legacy = Solve(
new LegacyPlateNesterAdapter(plate => new StripNestEngine(plate)),
Job(parts, strategy: "Strip")
);
var migrated = Solve(new StripPlateNester(), Job(parts, strategy: "Strip"));
Assert.Equal(legacy.Status, migrated.Status);
Assert.Equal(NestJobStatus.Complete, migrated.Status);
Assert.Equal(ByPart(legacy), ByPart(migrated));
Assert.Equal(legacy.Plates.SelectMany(p => p.Placements).Count(),
migrated.Plates.SelectMany(p => p.Placements).Count());
Assert.Equal(
legacy.Plates.SelectMany(p => p.Placements).Count(),
migrated.Plates.SelectMany(p => p.Placements).Count()
);
// Shrink-fill ordering can differ between engine instances; do not assert identical coordinates.
}
@@ -125,7 +156,11 @@ public class PlateNesterParityTests
var parts = new[]
{
new NestJobPart("l", PartGeometrySnapshot.FromProgram(lshape), 3),
new NestJobPart("sq", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), 2)
new NestJobPart(
"sq",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)),
2
),
};
var result = Solve(new DefaultPlateNester(), Job(parts));
@@ -161,7 +196,7 @@ public class PlateNesterParityTests
var parts = new[]
{
new NestJobPart("holed", PartGeometrySnapshot.FromProgram(holed), 2),
new NestJobPart("arc", PartGeometrySnapshot.FromProgram(arc), 2)
new NestJobPart("arc", PartGeometrySnapshot.FromProgram(arc), 2),
};
var result = Solve(new DefaultPlateNester(), Job(parts));
@@ -173,15 +208,22 @@ public class PlateNesterParityTests
[Fact]
public void FixedRotation_Respected()
{
var part = new NestJobPart("fixed", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)),
2, rotation: RotationPolicy.Fixed(0));
var part = new NestJobPart(
"fixed",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)),
2,
rotation: RotationPolicy.Fixed(0)
);
var result = Solve(new DefaultPlateNester(), Job(new[] { part }));
Assert.Equal(NestJobStatus.Complete, result.Status);
var placements = result.Plates.SelectMany(p => p.Placements).ToList();
Assert.Equal(2, placements.Count);
foreach (var placement in placements)
Assert.True(AnglesEqual(placement.Rotation, 0), $"fixed rotation violated: {placement.Rotation}");
Assert.True(
AnglesEqual(placement.Rotation, 0),
$"fixed rotation violated: {placement.Rotation}"
);
}
[Fact]
@@ -192,7 +234,7 @@ public class PlateNesterParityTests
var parts = new[]
{
new NestJobPart("first", PartGeometrySnapshot.FromProgram(program), 2),
new NestJobPart("second", PartGeometrySnapshot.FromProgram(program), 1)
new NestJobPart("second", PartGeometrySnapshot.FromProgram(program), 1),
};
var result = Solve(new DefaultPlateNester(), Job(parts));
@@ -229,10 +271,16 @@ public class PlateNesterParityTests
// 14x9 parts on 30x20: one sheet holds fewer than five, so the runner runs multiple candidate
// trials through the same nester instance. The run-scoped drawing cache must keep producing
// valid, correctly-attributed placements across trials.
var part = new NestJobPart("p", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(14, 9)), 5);
var part = new NestJobPart(
"p",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(14, 9)),
5
);
var stock = new NestPlateStock("stock", new Size(30, 20), 3);
var nester = new DefaultPlateNester();
var result = new NestJobRunner(_ => nester).Solve(new NestJob(new[] { part }, new[] { stock }));
var result = new NestJobRunner(_ => nester).Solve(
new NestJob(new[] { part }, new[] { stock })
);
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.Equal(5, result.Fulfillment.Single(f => f.PartId == "p").Placed);
@@ -246,10 +294,17 @@ public class PlateNesterParityTests
public void LegacyRemnantStrategies_StillResolveThroughAdapter()
{
// Remnant strategies must keep working through the legacy adapter after the factory change.
var part = new NestJobPart("p", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)), 2);
var part = new NestJobPart(
"p",
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)),
2
);
foreach (var strategy in new[] { "Vertical Remnant", "Horizontal Remnant" })
{
var result = Solve(PlateNesterFactory.Create(strategy), Job(new[] { part }, strategy: strategy));
var result = Solve(
PlateNesterFactory.Create(strategy),
Job(new[] { part }, strategy: strategy)
);
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.Equal(2, result.Fulfillment.Single(f => f.PartId == "p").Placed);
}
+160 -61
View File
@@ -4,19 +4,31 @@ namespace OpenNest.Engine.Tests.Jobs;
public class StockLadderTests
{
private static NestJobPart Rectangle(string id, int quantity, double x = 4, double y = 4,
RotationPolicy? rotation = null) => new(id,
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(x, y)), quantity,
rotation: rotation ?? RotationPolicy.Fixed(0));
private static NestJobPart Rectangle(
string id,
int quantity,
double x = 4,
double y = 4,
RotationPolicy? rotation = null
) =>
new(
id,
PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(x, y)),
quantity,
rotation: rotation ?? RotationPolicy.Fixed(0)
);
[Fact]
public void MergesEquivalentDemandOntoLargerSheetAndReturnsFiniteStock()
{
var job = new NestJob(new[] { Rectangle("a", 5) }, new[]
{
new NestPlateStock("small", new Size(10, 10), 2),
new NestPlateStock("large", new Size(10, 18), 1)
});
var job = new NestJob(
new[] { Rectangle("a", 5) },
new[]
{
new NestPlateStock("small", new Size(10, 10), 2),
new NestPlateStock("large", new Size(10, 18), 1),
}
);
var result = new StockLadderNestingEngine().Solve(job);
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.Equal("large", Assert.Single(result.Plates).StockId);
@@ -36,7 +48,11 @@ public class StockLadderTests
Assert.Equal(NestJobStopReason.StockExhausted, result.StopReason);
Assert.Equal(4, result.Fulfillment[0].Placed);
Verify(job, result);
job = new NestJob(parts, new[] { new NestPlateStock("only", new Size(10, 10)) }, new NestJobOptions(maxPlates: 1));
job = new NestJob(
parts,
new[] { new NestPlateStock("only", new Size(10, 10)) },
new NestJobOptions(maxPlates: 1)
);
result = new StockLadderNestingEngine().Solve(job);
Assert.Equal(NestJobStopReason.PlateLimitReached, result.StopReason);
Assert.Single(result.Plates);
@@ -46,11 +62,14 @@ public class StockLadderTests
[Fact]
public void ConstrainedLargeSinglePrecedesSmallFillers()
{
var job = new NestJob(new[] { Rectangle("small", 12, 2, 2), Rectangle("large", 1, 12, 6) }, new[]
{
new NestPlateStock("small-sheet", new Size(10, 10)),
new NestPlateStock("large-sheet", new Size(10, 18))
});
var job = new NestJob(
new[] { Rectangle("small", 12, 2, 2), Rectangle("large", 1, 12, 6) },
new[]
{
new NestPlateStock("small-sheet", new Size(10, 10)),
new NestPlateStock("large-sheet", new Size(10, 18)),
}
);
var result = new StockLadderNestingEngine().Solve(job);
Assert.Equal("large", result.Plates[0].Placements[0].PartId);
Assert.Contains(result.Plates[0].Placements, p => p.PartId == "small");
@@ -59,12 +78,31 @@ public class StockLadderTests
}
[Theory]
[InlineData(1)] [InlineData(2)] [InlineData(3)] [InlineData(4)]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
public void GeometrySpacingRotationsAndQuadrantsAreValidated(int quadrant)
{
var job = new NestJob(new[] { Rectangle("a", 6, 3, 5, RotationPolicy.Fixed(System.Math.PI / 2)) },
new[] { new NestPlateStock("sheet", new Size(12, 18), partSpacing: 0.25,
edgeSpacing: new Spacing { Left = 0.5, Right = 0.5, Top = 0.5, Bottom = 0.5 }, quadrant: quadrant) });
var job = new NestJob(
new[] { Rectangle("a", 6, 3, 5, RotationPolicy.Fixed(System.Math.PI / 2)) },
new[]
{
new NestPlateStock(
"sheet",
new Size(12, 18),
partSpacing: 0.25,
edgeSpacing: new Spacing
{
Left = 0.5,
Right = 0.5,
Top = 0.5,
Bottom = 0.5,
},
quadrant: quadrant
),
}
);
var result = new StockLadderNestingEngine().Solve(job);
Assert.Equal(NestJobStatus.Complete, result.Status);
Verify(job, result);
@@ -73,8 +111,10 @@ public class StockLadderTests
[Fact]
public void ImpossibleDemandTerminatesWithoutUsingUnlimitedStock()
{
var job = new NestJob(new[] { Rectangle("a", 1, 100, 100) },
new[] { new NestPlateStock("sheet", new Size(10, 10)) });
var job = new NestJob(
new[] { Rectangle("a", 1, 100, 100) },
new[] { new NestPlateStock("sheet", new Size(10, 10)) }
);
var result = new StockLadderNestingEngine().Solve(job);
Assert.Equal(NestJobStopReason.NoPlacementFound, result.StopReason);
Assert.Empty(result.Plates);
@@ -83,31 +123,56 @@ public class StockLadderTests
[Fact]
public void CancellationBeforeAndDuringTrialNeverReturnsPartialSuccess()
{
var job = new NestJob(new[] { Rectangle("a", 1) }, new[] { new NestPlateStock("s", new Size(10, 10)) });
var job = new NestJob(
new[] { Rectangle("a", 1) },
new[] { new NestPlateStock("s", new Size(10, 10)) }
);
using var cts = new CancellationTokenSource();
var engine = new StockLadderNestingEngine(() => new CallbackNester(request =>
{
cts.Cancel();
return new PlateCandidate(Array.Empty<NestJobPlacement>());
}));
var engine = new StockLadderNestingEngine(() =>
new CallbackNester(request =>
{
cts.Cancel();
return new PlateCandidate(Array.Empty<NestJobPlacement>());
})
);
Assert.Throws<OperationCanceledException>(() => engine.Solve(job, token: cts.Token));
Assert.Throws<OperationCanceledException>(() => new StockLadderNestingEngine().Solve(job, token: cts.Token));
Assert.Throws<OperationCanceledException>(() =>
new StockLadderNestingEngine().Solve(job, token: cts.Token)
);
}
[Theory]
[InlineData(false)] [InlineData(true)]
[InlineData(false)]
[InlineData(true)]
public void RejectsOverlappingOrOverproducingNester(bool overproduce)
{
var job = new NestJob(new[] { Rectangle("a", 2) }, new[] { new NestPlateStock("s", new Size(10, 10)) });
var engine = new StockLadderNestingEngine(() => new CallbackNester(request =>
new PlateCandidate(overproduce
? Enumerable.Repeat(new NestJobPlacement("a", 0, 0, 0, 0), 3)
: new[] { new NestJobPlacement("a", 0, 50, 0, 0) })));
var job = new NestJob(
new[] { Rectangle("a", 2) },
new[] { new NestPlateStock("s", new Size(10, 10)) }
);
var engine = new StockLadderNestingEngine(() =>
new CallbackNester(request => new PlateCandidate(
overproduce
? Enumerable.Repeat(new NestJobPlacement("a", 0, 0, 0, 0), 3)
: new[] { new NestJobPlacement("a", 0, 50, 0, 0) }
))
);
Assert.Throws<InvalidOperationException>(() => engine.Solve(job));
// Direct full-demand overlap check, not masked by the single-part feasibility probe limit.
Assert.Throws<InvalidOperationException>(() => NestJobValidator.ValidateCandidate(
new PlateCandidate(new[] { new NestJobPlacement("a", 0, 0, 0, 0), new NestJobPlacement("a", 1, 1, 1, 0) }),
job.Plates[0], new Dictionary<string, int> { ["a"] = 2 }, job.Parts.ToDictionary(p => p.Id)));
Assert.Throws<InvalidOperationException>(() =>
NestJobValidator.ValidateCandidate(
new PlateCandidate(
new[]
{
new NestJobPlacement("a", 0, 0, 0, 0),
new NestJobPlacement("a", 1, 1, 1, 0),
}
),
job.Plates[0],
new Dictionary<string, int> { ["a"] = 2 },
job.Parts.ToDictionary(p => p.Id)
)
);
}
[Fact]
@@ -115,33 +180,52 @@ public class StockLadderTests
{
var part = Rectangle("a", 1);
var stock = new NestPlateStock("s", new Size(10, 10));
var sheet = new NestJobPlateResult(0, stock, new[] { new NestJobPlacement("a", 0, 0, 0, 0) });
NestJob Job(double rate, double min) => new(new[] { part }, new[] { stock },
new NestJobOptions(salvageRate: rate, minimumSalvageDimension: min));
var sheet = new NestJobPlateResult(
0,
stock,
new[] { new NestJobPlacement("a", 0, 0, 0, 0) }
);
NestJob Job(double rate, double min) =>
new(
new[] { part },
new[] { stock },
new NestJobOptions(salvageRate: rate, minimumSalvageDimension: min)
);
Assert.Equal(100, StockLadderNestingEngine.EstimateNetArea(Job(0.5, 0), sheet));
Assert.Equal(100, StockLadderNestingEngine.EstimateNetArea(Job(0.5, 7), sheet));
Assert.Equal(70, StockLadderNestingEngine.EstimateNetArea(Job(0.5, 5), sheet), 6);
Assert.Throws<ArgumentOutOfRangeException>(() => new NestJobOptions(salvageRate: double.NaN));
Assert.Throws<ArgumentOutOfRangeException>(() =>
new NestJobOptions(salvageRate: double.NaN)
);
Assert.Throws<ArgumentOutOfRangeException>(() => new NestJobOptions(salvageRate: 1.1));
}
[Fact]
public void FailedRepackRetainsAllDemandAndFiniteStockAccounting()
{
var job = new NestJob(new[] { Rectangle("a", 5) }, new[]
{
new NestPlateStock("small", new Size(10, 10), 2),
new NestPlateStock("large", new Size(10, 18), 1)
});
var job = new NestJob(
new[] { Rectangle("a", 5) },
new[]
{
new NestPlateStock("small", new Size(10, 10), 2),
new NestPlateStock("large", new Size(10, 18), 1),
}
);
var fullDemandLargeTrials = 0;
var engine = new StockLadderNestingEngine(() => new CallbackNester(request =>
{
var quantity = Assert.Single(request.Parts).Quantity;
if (request.Stock.Id == "large" && quantity == 5) fullDemandLargeTrials++;
// Deliberately fail to reproduce the fifth piece on the cheaper merged sheet.
return new PlateCandidate(Enumerable.Range(0, System.Math.Min(quantity, 4))
.Select(i => new NestJobPlacement("a", i, i % 2 * 4, i / 2 * 4, 0)));
}));
var engine = new StockLadderNestingEngine(() =>
new CallbackNester(request =>
{
var quantity = Assert.Single(request.Parts).Quantity;
if (request.Stock.Id == "large" && quantity == 5)
fullDemandLargeTrials++;
// Deliberately fail to reproduce the fifth piece on the cheaper merged sheet.
return new PlateCandidate(
Enumerable
.Range(0, System.Math.Min(quantity, 4))
.Select(i => new NestJobPlacement("a", i, i % 2 * 4, i / 2 * 4, 0))
);
})
);
var result = engine.Solve(job);
Assert.True(fullDemandLargeTrials >= 2); // Construction AND equivalent-demand repack ran.
Assert.Equal(NestJobStatus.Complete, result.Status);
@@ -166,7 +250,9 @@ public class StockLadderTests
var job = new NestJob(new[] { part }, new[] { new NestPlateStock("s", new Size(10, 10)) });
if (reject)
{
var error = Assert.Throws<ArgumentException>(() => new StockLadderNestingEngine().Solve(job));
var error = Assert.Throws<ArgumentException>(() =>
new StockLadderNestingEngine().Solve(job)
);
Assert.Contains("Open geometry leaves the closed material region", error.Message);
}
else
@@ -183,12 +269,21 @@ public class StockLadderTests
var remaining = job.Parts.ToDictionary(p => p.Id, p => p.Quantity);
foreach (var sheet in result.Plates)
{
NestJobValidator.ValidateCandidate(new PlateCandidate(sheet.Placements), sheet.Stock, remaining, parts);
foreach (var pose in sheet.Placements) remaining[pose.PartId]--;
NestJobValidator.ValidateCandidate(
new PlateCandidate(sheet.Placements),
sheet.Stock,
remaining,
parts
);
foreach (var pose in sheet.Placements)
remaining[pose.PartId]--;
}
foreach (var part in job.Parts)
{
var poses = result.Plates.SelectMany(p => p.Placements).Where(p => p.PartId == part.Id).ToList();
var poses = result
.Plates.SelectMany(p => p.Placements)
.Where(p => p.PartId == part.Id)
.ToList();
Assert.Equal(Enumerable.Range(0, poses.Count), poses.Select(p => p.InstanceIndex));
var fulfillment = result.Fulfillment.Single(p => p.PartId == part.Id);
Assert.Equal(part.Quantity, fulfillment.Placed + fulfillment.Unplaced);
@@ -204,9 +299,13 @@ public class StockLadderTests
}
}
private sealed class CallbackNester(Func<PlatePlacementRequest, PlateCandidate> callback) : IPlateNester
private sealed class CallbackNester(Func<PlatePlacementRequest, PlateCandidate> callback)
: IPlateNester
{
public PlateCandidate Place(PlatePlacementRequest request, IProgress<NestJobProgress>? progress = null,
CancellationToken token = default) => callback(request);
public PlateCandidate Place(
PlatePlacementRequest request,
IProgress<NestJobProgress>? progress = null,
CancellationToken token = default
) => callback(request);
}
}