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:
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user