diff --git a/OpenNest.Core/CNC/CuttingPlanning/ContourProgramVerifier.cs b/OpenNest.Core/CNC/CuttingPlanning/ContourProgramVerifier.cs index e3a989a1..1c75515b 100644 --- a/OpenNest.Core/CNC/CuttingPlanning/ContourProgramVerifier.cs +++ b/OpenNest.Core/CNC/CuttingPlanning/ContourProgramVerifier.cs @@ -75,12 +75,13 @@ internal static class ContourProgramVerifier // Capture admits a nominal ring that closes within NominalContourClosure.Tolerance, and // preparation agrees at that inclusive boundary. A rotated tool path bridges that one // admitted joint, so this ring's positional and length agreement carries exactly that - // admitted slack. Direction, coverage order and retrace accounting stay at the epsilon. + // admitted slack, whether bridging it shortens or lengthens the cut. var tolerance = AdmittedClosureSlack(ring); var nominalLength = ring.Sum(c => c.Length); var cutLength = run.Curves.Sum(c => c.Length); var missing = nominalLength - cutLength; - if (missing < -Epsilon) return false; // Never a second circuit/retrace. + // Bridging the admitted joint may add up to its gap; a second circuit/retrace never passes. + if (missing < -tolerance) return false; if (missing > tolerance) { // Fixed legacy payloads have no certified selected tab/entry metadata. @@ -135,7 +136,9 @@ internal static class ContourProgramVerifier foreach (var curve in actual) { var consumed = 0.0; - while (consumed < curve.Length - Epsilon) + // Like the nominal side below, a remainder within the slack is consumed: a path that + // bridges the admitted joint leaves exactly that much on the edge it extends. + while (consumed < curve.Length - tolerance) { Query(token, ref budget); if (index >= expected.Count) diff --git a/OpenNest.Tests/CuttingPlanning/SeamClosureReplayTests.cs b/OpenNest.Tests/CuttingPlanning/SeamClosureReplayTests.cs index 77f42f27..0d88898b 100644 --- a/OpenNest.Tests/CuttingPlanning/SeamClosureReplayTests.cs +++ b/OpenNest.Tests/CuttingPlanning/SeamClosureReplayTests.cs @@ -77,6 +77,35 @@ public class SeamClosureReplayTests Assert.True(result.IndependentlyReplayed); } + [Theory] + [InlineData(5e-7)] + [InlineData(1e-6)] + public void AdmittedClosureSeam_ThatLengthensThePath_StillReplays(double seam) + { + // The ring stops short of its start across the following edge, so bridging the seam adds + // up to the admitted gap to the cut length and extends that edge before the next corner + // (A0227-08's sprocket outline). Entering on the top edge puts the seam mid-path. + var program = new Program(); + program.MoveTo(0, 0); + program.LineTo(0, 10); + program.LineTo(10, 10); + program.LineTo(10, 0); + program.LineTo(0, -seam); + var settings = ExplicitContourTests.Parameters(); + var part = new Part(new Drawing("seam", program)); + var snapshot = Capture(part, settings); + var source = snapshot.Placements[0]; + Assert.True(source.Material!.IsComplete, source.Material.Reason); + + var choice = source.Prepared!.Entry(0, 1, new Vector(5, 10)); + var emitted = source.Prepared.Emit([choice]); + var result = CuttingPlanService.ReplayPrograms(snapshot, + [source.Propose(emitted, Read(emitted), [choice])], 0, default); + + Assert.Equal(CuttingPlanStatus.Ready, result.Status); + Assert.True(result.IndependentlyReplayed); + } + [Fact] public void AdmittedClosureSeam_StillPlansWithGeneratedLeadIns() {