From 017e06de2d9b1bd98cd6a9f5ce40ffb2bf20fd01 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 16 Oct 2026 09:55:49 -0400 Subject: [PATCH] fix(cutting): replay seams whose bridge lengthens the cut A nominal ring may close within NominalContourClosure.Tolerance, and replay already carried that admitted gap as slack when bridging it shortens the cut. When the gap instead lies across the following edge, the emitted path starts that edge at the ring's end point, so the cut is longer by the gap. Two checks still compared at the exact epsilon and refused it as "invalid input": - the retrace guard rejected any surplus length beyond 1e-8; - Follows matched the edge's leftover sliver against the next nominal entity and failed its direction check. Both now use the ring's admitted slack, the same bound the shortfall and the nominal side of Follows already use. Exactly closed rings keep the epsilon, and a second circuit still exceeds the slack by a whole ring. A0227-08's sprocket outline misses its start by 1e-6 and refused planning in both keep-order and reorder modes; it now plans Ready and applies. Co-Authored-By: Claude Opus 5.5 --- .../CuttingPlanning/ContourProgramVerifier.cs | 9 ++++-- .../CuttingPlanning/SeamClosureReplayTests.cs | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) 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() {