using OpenNest.CNC; using OpenNest.Geometry; namespace OpenNest.Engine.Tests.Jobs; public class NestJobValidationTests { [Fact] public void IncrementalContoursUseAccumulatedCoordinates() { var program = new Program(Mode.Incremental); program.MoveTo(0, 0); program.LineTo(4, 0); 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 result = Solve(job, new NestJobPlacement("part", 0, 0, 0, 0)); Assert.Equal(NestJobStatus.Complete, result.Status); Assert.Equal(new PartFulfillment("part", 1, 1, 0), Assert.Single(result.Fulfillment)); } [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 result = Solve(job, new NestJobPlacement("outer", 0, 0, 0, 0), new NestJobPlacement("inner", 0, 4, 4, 0)); Assert.Equal(NestJobStatus.Complete, result.Status); Assert.Single(result.Plates); Assert.Equal(2, result.Plates[0].Placements.Count); } [Fact] public void UnknownOrOverproducingCandidateFailsBeforeCommitWithoutChangingInput() { var reports = new List(); 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) })); Assert.Throws(() => runner.Solve(job, new InlineProgress(reports.Add))); Assert.Equal(1, job.Parts[0].Quantity); Assert.Equal(1, job.Plates[0].Quantity); Assert.DoesNotContain(reports, report => report.Stage == NestJobStage.PlateCommitted); } [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) }); Assert.Throws(() => 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); } [Theory] [InlineData(true)] [InlineData(false)] public void OpenOrZeroLengthContoursAreRejected(bool zeroLength) { var program = new Program(); program.MoveTo(0, 0); 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) }); Assert.Throws(() => new NestJobRunner(_ => new CandidateNester(Array.Empty())).Solve(job)); } private static NestJobResult Solve(NestJob job, params NestJobPlacement[] placements) => new NestJobRunner(_ => new CandidateNester(placements)).Solve(job); private static Program RectangleWithHole() { var program = TestDrawingFactory.Rectangle(10, 10); program.MoveTo(3, 3); program.LineTo(3, 7); program.LineTo(7, 7); program.LineTo(7, 3); program.LineTo(3, 3); return program; } private sealed class CandidateNester(IEnumerable placements) : IPlateNester { public PlateCandidate Place(PlatePlacementRequest request, IProgress? progress = null, CancellationToken token = default) => new(placements); } private sealed class InlineProgress(Action report) : IProgress { public void Report(NestJobProgress value) => report(value); } }