fix(engine): reject unrepresentable nesting output before geometry checks
This commit is contained in:
@@ -6,32 +6,78 @@ namespace OpenNest.Engine.Tests.Jobs;
|
||||
|
||||
public class NestLayoutCheckTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(null, false)]
|
||||
[InlineData(1, true)]
|
||||
[InlineData(2, false)]
|
||||
[InlineData(3, false)]
|
||||
public void MaxPlatesCountsPhysicalSheetsIncludingEmptyOnes(int? maxPlates, bool exceedsLimit)
|
||||
{
|
||||
var stock = new NestPlateStock("sheet", new Size(48, 96));
|
||||
var job = new NestJob(Array.Empty<NestJobPart>(), new[] { stock }, new NestJobOptions(maxPlates: maxPlates));
|
||||
var result = new NestJobResult(NestJobStatus.Complete, NestJobStopReason.Completed,
|
||||
new[]
|
||||
{
|
||||
new NestJobPlateResult(7, stock, Array.Empty<NestJobPlacement>()),
|
||||
new NestJobPlateResult(42, stock, Array.Empty<NestJobPlacement>()),
|
||||
}, Array.Empty<PartFulfillment>(), Array.Empty<StockUsage>());
|
||||
|
||||
var violations = NestLayoutCheck.Violations(job, result);
|
||||
|
||||
if (exceedsLimit)
|
||||
Assert.Contains(violations, v => v.Contains("MaxPlates") && v.Contains("2") && v.Contains("1"));
|
||||
else
|
||||
Assert.Empty(violations);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("ghost", 1, 1, 0)]
|
||||
[InlineData(null, 1, 1, 0)]
|
||||
[InlineData("p", double.NaN, 1, 0)]
|
||||
[InlineData("p", 1, double.PositiveInfinity, 0)]
|
||||
[InlineData("p", 1, 1, double.NaN)]
|
||||
public void MalformedPlacementsProduceDiagnosticsRatherThanThrowing(string? partId, double x, double y, double rotation)
|
||||
{
|
||||
var stock = new NestPlateStock("sheet", new Size(48, 96));
|
||||
var job = new NestJob(new[]
|
||||
{
|
||||
new NestJobPart("p", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle()), 1),
|
||||
}, new[] { stock });
|
||||
var result = new NestJobResult(NestJobStatus.Complete, NestJobStopReason.Completed,
|
||||
new[] { new NestJobPlateResult(0, stock, new[] { new NestJobPlacement(partId!, 0, x, y, rotation) }) },
|
||||
Array.Empty<PartFulfillment>(), Array.Empty<StockUsage>());
|
||||
|
||||
var violations = NestLayoutCheck.Violations(job, result);
|
||||
|
||||
Assert.NotEmpty(violations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TangentDiscsClearAtSafeMarginAcrossRadiiAnglesAndTolerances()
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var radius in new[] { 0.1, 1.0, 10.0 })
|
||||
foreach (var tolerance in new[] { 0.0, 0.0005, 0.01 })
|
||||
foreach (var spacing in new[] { 0.0, 0.25 })
|
||||
{
|
||||
var program = new Program();
|
||||
program.MoveTo(radius, 0);
|
||||
program.Codes.Add(new ArcMove(radius, 0, 0, 0, RotationType.CW));
|
||||
var geometry = JobPartGeometry.Read(PartGeometrySnapshot.FromProgram(program));
|
||||
// Two inscribed engine outlines can underestimate true extent by t each.
|
||||
var distance = 2 * radius + spacing
|
||||
+ NestTolerances.SafeClearanceMargin(tolerance) - 2 * tolerance;
|
||||
for (var degrees = 0; degrees < 360; degrees += 15)
|
||||
{
|
||||
var angle = degrees * System.Math.PI / 180;
|
||||
var a = new NestJobPlacement("disc", 0, 0.12345, -0.54321, angle / 3);
|
||||
var b = new NestJobPlacement("disc", 1, a.X + distance * System.Math.Cos(angle),
|
||||
a.Y + distance * System.Math.Sin(angle), -angle / 7);
|
||||
Assert.True(NestLayoutCheck.Clears(geometry, a, geometry, b, spacing));
|
||||
Assert.True(NestLayoutCheck.Clears(geometry, b, geometry, a, spacing));
|
||||
count++;
|
||||
}
|
||||
}
|
||||
foreach (var tolerance in new[] { 0.0, 0.0005, 0.01 })
|
||||
foreach (var spacing in new[] { 0.0, 0.25 })
|
||||
{
|
||||
var program = new Program();
|
||||
program.MoveTo(radius, 0);
|
||||
program.Codes.Add(new ArcMove(radius, 0, 0, 0, RotationType.CW));
|
||||
var geometry = JobPartGeometry.Read(PartGeometrySnapshot.FromProgram(program));
|
||||
// Two inscribed engine outlines can underestimate true extent by t each.
|
||||
var distance = 2 * radius + spacing
|
||||
+ NestTolerances.SafeClearanceMargin(tolerance) - 2 * tolerance;
|
||||
for (var degrees = 0; degrees < 360; degrees += 15)
|
||||
{
|
||||
var angle = degrees * System.Math.PI / 180;
|
||||
var a = new NestJobPlacement("disc", 0, 0.12345, -0.54321, angle / 3);
|
||||
var b = new NestJobPlacement("disc", 1, a.X + distance * System.Math.Cos(angle),
|
||||
a.Y + distance * System.Math.Sin(angle), -angle / 7);
|
||||
Assert.True(NestLayoutCheck.Clears(geometry, a, geometry, b, spacing));
|
||||
Assert.True(NestLayoutCheck.Clears(geometry, b, geometry, a, spacing));
|
||||
count++;
|
||||
}
|
||||
}
|
||||
Assert.Equal(432, count);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
@@ -45,6 +46,7 @@ public class NestPipelineTests
|
||||
var result = NestPipeline.Run(Request("Default", item));
|
||||
|
||||
Assert.True(result.IsValid, string.Join("; ", result.Violations));
|
||||
Assert.True(result.CanKeep);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
var parts = result.Plates.SelectMany(p => p.Parts).ToList();
|
||||
Assert.Equal(10, parts.Count);
|
||||
@@ -58,6 +60,8 @@ public class NestPipelineTests
|
||||
[Fact]
|
||||
public void OverlappingEngineOutputIsReportedByDrawingNameWithoutThrowing()
|
||||
{
|
||||
var item = Item("bracket", 2);
|
||||
var before = PartGeometrySnapshot.FromProgram(item.Drawing.Program).Motions;
|
||||
var engine = new StubEngine(job =>
|
||||
OnePlate(
|
||||
job,
|
||||
@@ -66,29 +70,167 @@ public class NestPipelineTests
|
||||
)
|
||||
);
|
||||
|
||||
var result = NestPipeline.Run(engine, "Overlapper", Request("Overlapper", Item("bracket", 2)));
|
||||
var result = NestPipeline.Run(engine, "Overlapper", Request("Overlapper", item));
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.True(result.CanKeep);
|
||||
Assert.Contains(result.Violations, v => v.Contains("bracket") && v.Contains("spacing"));
|
||||
Assert.Equal(2, result.Plates.Single().Parts.Count);
|
||||
Assert.All(result.Plates.Single().Parts, part => Assert.Same(item.Drawing, part.BaseDrawing));
|
||||
Assert.Equal(before, PartGeometrySnapshot.FromProgram(item.Drawing.Program).Motions);
|
||||
Assert.Equal(2, item.Quantity);
|
||||
Assert.Equal(0, item.Drawing.Quantity.Nested);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("ghost")]
|
||||
[InlineData(null)]
|
||||
public void UnknownOrNullRequirementMakesTheEntireProposalNonKeepable(string? partId)
|
||||
{
|
||||
var item = Item("bracket", 1);
|
||||
var before = PartGeometrySnapshot.FromProgram(item.Drawing.Program).Motions;
|
||||
var engine = new StubEngine(job => new NestJobResult(
|
||||
NestJobStatus.Complete,
|
||||
NestJobStopReason.Completed,
|
||||
new[]
|
||||
{
|
||||
new NestJobPlateResult(0, job.Plates[0], new[] { new NestJobPlacement(job.Parts[0].Id, 0, 1, 1, 0) }),
|
||||
new NestJobPlateResult(1, job.Plates[0], new[] { new NestJobPlacement(partId!, 0, 30, 1, 0) }),
|
||||
},
|
||||
Array.Empty<PartFulfillment>(),
|
||||
Array.Empty<StockUsage>()
|
||||
));
|
||||
|
||||
var result = NestPipeline.Run(engine, "Ghost", Request("Ghost", item));
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.False(result.CanKeep);
|
||||
Assert.Contains(result.Violations, v => v.Contains(partId ?? "null") && v.Contains("Plate 1"));
|
||||
Assert.Empty(result.Plates);
|
||||
Assert.Equal(2, result.Raw.Plates.Count);
|
||||
Assert.Equal(before, PartGeometrySnapshot.FromProgram(item.Drawing.Program).Motions);
|
||||
Assert.Equal(1, item.Quantity);
|
||||
Assert.Equal(0, item.Drawing.Quantity.Nested);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(double.NaN, 1, 0, "X")]
|
||||
[InlineData(double.PositiveInfinity, 1, 0, "X")]
|
||||
[InlineData(double.NegativeInfinity, 1, 0, "X")]
|
||||
[InlineData(1, double.NaN, 0, "Y")]
|
||||
[InlineData(1, double.PositiveInfinity, 0, "Y")]
|
||||
[InlineData(1, double.NegativeInfinity, 0, "Y")]
|
||||
[InlineData(1, 1, double.NaN, "Rotation")]
|
||||
[InlineData(1, 1, double.PositiveInfinity, "Rotation")]
|
||||
[InlineData(1, 1, double.NegativeInfinity, "Rotation")]
|
||||
public void NonfinitePoseIsReportedAndNeverProposed(double x, double y, double rotation, string field)
|
||||
{
|
||||
var item = Item("bracket", 2);
|
||||
var before = PartGeometrySnapshot.FromProgram(item.Drawing.Program).Motions;
|
||||
var engine = new StubEngine(job => OnePlate(job,
|
||||
new NestJobPlacement(job.Parts[0].Id, 0, 1, 1, 0),
|
||||
new NestJobPlacement(job.Parts[0].Id, 1, x, y, rotation)));
|
||||
|
||||
var result = NestPipeline.Run(engine, "Nonfinite", Request("Nonfinite", item));
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.False(result.CanKeep);
|
||||
Assert.Empty(result.Plates);
|
||||
Assert.Contains(result.Violations, v => v.Contains(field) && v.Contains("finite") && v.Contains("bracket"));
|
||||
Assert.Equal(before, PartGeometrySnapshot.FromProgram(item.Drawing.Program).Motions);
|
||||
Assert.Equal(2, item.Quantity);
|
||||
Assert.Equal(0, item.Drawing.Quantity.Nested);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlacementForUnknownRequirementIsAViolationNotSilentlyDropped()
|
||||
public void ReportsEveryStructuralViolationWithoutMaterializingAnyGeometry()
|
||||
{
|
||||
var engine = new StubEngine(job =>
|
||||
OnePlate(
|
||||
job,
|
||||
new NestJobPlacement(job.Parts[0].Id, 0, 1, 1, 0),
|
||||
new NestJobPlacement("ghost", 0, 30, 1, 0)
|
||||
)
|
||||
);
|
||||
var engine = new StubEngine(job => OnePlate(job,
|
||||
new NestJobPlacement("ghost", 0, double.NaN, double.PositiveInfinity, double.NegativeInfinity),
|
||||
new NestJobPlacement(null!, 0, 1, 1, 0),
|
||||
new NestJobPlacement(job.Parts[0].Id, 0, double.NegativeInfinity, 1, 0)));
|
||||
|
||||
var result = NestPipeline.Run(engine, "Ghost", Request("Ghost", Item("bracket", 1)));
|
||||
var result = NestPipeline.Run(engine, "Malformed", Request("Malformed", Item("bracket", 1)));
|
||||
|
||||
Assert.False(result.CanKeep);
|
||||
Assert.Empty(result.Plates);
|
||||
Assert.Equal(6, result.Violations.Count);
|
||||
Assert.Contains(result.Violations, v => v.Contains("ghost") && v.Contains("not part of this job"));
|
||||
Assert.Contains(result.Violations, v => v.Contains("null") && v.Contains("not part of this job"));
|
||||
Assert.Equal(2, result.Violations.Count(v => v.Contains("nonfinite X")));
|
||||
Assert.Contains(result.Violations, v => v.Contains("nonfinite Y"));
|
||||
Assert.Contains(result.Violations, v => v.Contains("nonfinite Rotation"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExceedingMaxPlatesIsInvalidButRemainsFullyKeepable()
|
||||
{
|
||||
var item = Item("bracket", 2);
|
||||
var request = Request("TooManySheets", item) with { Options = new NestJobOptions(maxPlates: 1) };
|
||||
var engine = new StubEngine(job => new NestJobResult(
|
||||
NestJobStatus.Complete,
|
||||
NestJobStopReason.Completed,
|
||||
Enumerable.Range(0, 2).Select(i => new NestJobPlateResult(i, job.Plates[0],
|
||||
new[] { new NestJobPlacement(job.Parts[0].Id, i, 1, 1, 0) })),
|
||||
new[] { new PartFulfillment(job.Parts[0].Id, 2, 2, 0) },
|
||||
new[] { new StockUsage(job.Plates[0].Id, 2, null) }
|
||||
));
|
||||
|
||||
var result = NestPipeline.Run(engine, "TooManySheets", request);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Contains(result.Violations, v => v.Contains("ghost"));
|
||||
Assert.Single(result.Plates.Single().Parts);
|
||||
Assert.True(result.CanKeep);
|
||||
Assert.Contains(result.Violations, v => v.Contains("MaxPlates") && v.Contains("2") && v.Contains("1"));
|
||||
Assert.Equal(2, result.Plates.Count);
|
||||
Assert.All(result.Plates, plate => Assert.Same(item.Drawing, Assert.Single(plate.Parts).BaseDrawing));
|
||||
Assert.Equal(2, item.Quantity);
|
||||
Assert.Equal(0, item.Drawing.Quantity.Nested);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidStockIsRejectedBeforeCallingAnArbitraryEngine()
|
||||
{
|
||||
var called = false;
|
||||
var engine = new StubEngine(job =>
|
||||
{
|
||||
called = true;
|
||||
return OnePlate(job);
|
||||
});
|
||||
var request = Request("Unchecked", Item("bracket", 1)) with
|
||||
{
|
||||
Stock = new[] { new NestPlateStock("bad", new Size(48, 96), partSpacing: double.NaN) },
|
||||
};
|
||||
|
||||
var error = Assert.Throws<ArgumentException>(() => NestPipeline.Run(engine, "Unchecked", request));
|
||||
|
||||
Assert.Contains("Invalid stock", error.Message);
|
||||
Assert.False(called);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void InvalidGeometryIsRejectedBeforeCallingAnArbitraryEngine(bool nonfinite)
|
||||
{
|
||||
var item = Item("bracket", 1);
|
||||
if (nonfinite)
|
||||
((Motion)item.Drawing.Program.Codes[1]).EndPoint = new Vector(double.NaN, 0);
|
||||
else
|
||||
item.Drawing.Program.Codes.Clear();
|
||||
var called = false;
|
||||
var engine = new StubEngine(job =>
|
||||
{
|
||||
called = true;
|
||||
return OnePlate(job);
|
||||
});
|
||||
|
||||
var error = Assert.Throws<ArgumentException>(() =>
|
||||
NestPipeline.Run(engine, "Unchecked", Request("Unchecked", item)));
|
||||
|
||||
Assert.Contains("Geometry must contain finite motions", error.Message);
|
||||
Assert.False(called);
|
||||
Assert.Equal(1, item.Quantity);
|
||||
Assert.Equal(0, item.Drawing.Quantity.Nested);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Engine.Jobs.Adapters;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Engine.Tests.Jobs;
|
||||
|
||||
public class NestResultBinderTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("ghost", 1, 1, 0)]
|
||||
[InlineData(null, 1, 1, 0)]
|
||||
[InlineData("p", double.NaN, 1, 0)]
|
||||
[InlineData("p", 1, double.PositiveInfinity, 0)]
|
||||
[InlineData("p", 1, 1, double.NaN)]
|
||||
public void MalformedSheetCannotBePartiallyBound(string? partId, double x, double y, double rotation)
|
||||
{
|
||||
var drawing = new Drawing("bracket", TestDrawingFactory.Rectangle());
|
||||
var before = PartGeometrySnapshot.FromProgram(drawing.Program).Motions;
|
||||
var sheet = new NestJobPlateResult(0, new NestPlateStock("sheet", new Size(48, 96)),
|
||||
new[]
|
||||
{
|
||||
new NestJobPlacement("p", 0, 1, 1, 0),
|
||||
new NestJobPlacement(partId!, 1, x, y, rotation),
|
||||
});
|
||||
var drawings = new Dictionary<string, Drawing> { ["p"] = drawing };
|
||||
|
||||
Assert.Throws<ArgumentException>(() => NestResultBinder.Bind(sheet, drawings));
|
||||
|
||||
Assert.Equal(before, PartGeometrySnapshot.FromProgram(drawing.Program).Motions);
|
||||
Assert.Equal(0, drawing.Quantity.Nested);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user