From 508b7db0df849ac43ddfc7bb2115fc7742f76d51 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 30 Oct 2026 08:41:17 -0400 Subject: [PATCH] fix(engine): stop rejecting arc pairs placed at the part spacing On a plate with clamps, MaterialsTooClose measured gaps on polygons faceted at 0.001, which understate an arc gap by more than the 0.0005 slack. A pair interlocked exactly at the spacing read as too close, so FillGroup discarded every copy and the fill silently placed nothing. Confirm the gap on NativeContours, as the layout check and pre-post review already do. Co-Authored-By: Claude Opus 5.5 --- .../Jobs/ClampZoneInteractiveFillTests.cs | 40 +++++++++++++++++++ OpenNest.Engine/Jobs/InteractiveClamps.cs | 14 ++----- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/OpenNest.Engine.Tests/Jobs/ClampZoneInteractiveFillTests.cs b/OpenNest.Engine.Tests/Jobs/ClampZoneInteractiveFillTests.cs index 9ed521a1..7d522864 100644 --- a/OpenNest.Engine.Tests/Jobs/ClampZoneInteractiveFillTests.cs +++ b/OpenNest.Engine.Tests/Jobs/ClampZoneInteractiveFillTests.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using OpenNest.CNC; using OpenNest.Engine.Fill; +using OpenNest.Engine.Jobs; using OpenNest.Engine.Jobs.Placement; using OpenNest.Geometry; using Xunit; @@ -188,6 +189,45 @@ public class ClampZoneInteractiveFillTests Assert.Equal(0, parts.Count % 2); } + /// An interlocked pair of crescents, their arcs exactly 0.25 apart at + /// zero. Faceting those arcs at 0.001 reads about 0.2493. + private static List InterlockedCrescents(double nudge = 0) + { + var program = new Program(); + program.MoveTo(0, 0); + program.ArcTo(new Vector(-7.3094835341, 0), new Vector(-3.6547417671, 2.625426178), + RotationType.CCW); + program.ArcTo(new Vector(0, 0), new Vector(-3.6547417671, 3.4121639199), RotationType.CW); + var drawing = new Drawing("crescent", program); + + var flipped = new Part(drawing); + flipped.Rotate(System.Math.PI); + flipped.Offset(new Vector(17.790933720357735 + nudge, 41.393296427203666)); + return [new Part(drawing, new Vector(23.975496771940087, 35.352829360961906)), flipped]; + } + + [Fact] + public void FillGroupKeepsAnArcPairInterlockedExactlyAtThePartSpacing() + { + var plate = new Plate(new Size(60, 120)) { PartSpacing = 0.25 }; + plate.ClampZones.Add(new ClampZone("front", new Box(19.25, 0, 1.5, 0.625))); + + var parts = PlateFillService.FillGroup("Fill", plate, InterlockedCrescents(), + plate.WorkArea(), null, CancellationToken.None); + + Assert.True(parts.Count >= 100, $"expected a full sheet of pairs, got {parts.Count}"); + } + + [Fact] + public void MaterialsTooCloseMeasuresArcsRatherThanTheirFacets() + { + var atSpacing = InterlockedCrescents(); + Assert.False(InteractiveClamps.MaterialsTooClose(atSpacing[0], atSpacing[1], 0.25)); + + var tooClose = InterlockedCrescents(nudge: -0.01); + Assert.True(InteractiveClamps.MaterialsTooClose(tooClose[0], tooClose[1], 0.25)); + } + [Fact] public void PackAreaKeepsAllowedOverhangOnlyAtThePlateBoundary() { diff --git a/OpenNest.Engine/Jobs/InteractiveClamps.cs b/OpenNest.Engine/Jobs/InteractiveClamps.cs index c32eb6e8..41facf34 100644 --- a/OpenNest.Engine/Jobs/InteractiveClamps.cs +++ b/OpenNest.Engine/Jobs/InteractiveClamps.cs @@ -124,16 +124,10 @@ internal static class InteractiveClamps return true; if (spacing <= NestTolerances.SpacingSlack) return false; - var ringsA = new List { outerA }; - var ringsB = new List { outerB }; - if (holesA != null) ringsA.AddRange(holesA); - if (holesB != null) ringsB.AddRange(holesB); - foreach (var ringA in ringsA) - foreach (var ringB in ringsB) - if (Clearance.BoundaryDistance(ringA, ringB).Distance - < spacing - NestTolerances.SpacingSlack) - return true; - return false; + // The profiles are already in world space. Facets at the polygons' tolerance understate + // an arc gap by more than the slack, which would reject a pair placed at the spacing. + return NativeContours.Of(a, new Vector(0, 0)) + .TooClose(NativeContours.Of(b, new Vector(0, 0)), spacing); } private static List HolePolygons(ShapeProfile profile)