diff --git a/OpenNest.Core/Geometry/SlideContact.cs b/OpenNest.Core/Geometry/SlideContact.cs index 89be8eb6..dc04616f 100644 --- a/OpenNest.Core/Geometry/SlideContact.cs +++ b/OpenNest.Core/Geometry/SlideContact.cs @@ -41,6 +41,11 @@ namespace OpenNest.Geometry // the joint's tangents; otherwise the neighbour reads as a second boundary there. internal const double JointTolerance = 1e-6; + // Loops are recovered with ShapeBuilder's chaining tolerance: a source contour that + // closes within it (PEP rounding leaves 1e-6 gaps) is a loop everywhere else, and an + // unrecovered loop leaves every contact with the whole part unresolved and blocking. + internal const double ChainTolerance = Tolerance.ChainTolerance; + private readonly List entities; private readonly int[] loopOf; private readonly int[] previous; @@ -373,7 +378,7 @@ namespace OpenNest.Geometry } } - private static bool Near(Vector a, Vector b) => a.DistanceTo(b) <= IncidenceTolerance; + private static bool Near(Vector a, Vector b) => a.DistanceTo(b) <= ChainTolerance; private static double SignedArea(List entities, int first, int last) { diff --git a/OpenNest.Tests/Fill/CompactorTests.cs b/OpenNest.Tests/Fill/CompactorTests.cs index 591a14ef..4d0d91b9 100644 --- a/OpenNest.Tests/Fill/CompactorTests.cs +++ b/OpenNest.Tests/Fill/CompactorTests.cs @@ -387,6 +387,39 @@ namespace OpenNest.Tests.Fill AssertClearance(moving, frame, spacing); } + [Theory] + [InlineData(0)] + [InlineData(0.25)] + public void Push_RestingInHoleOfFrameWithUnclosedSourceContour_SlidesAwayFromFloor( + double spacing + ) + { + // PEP-rounded contours can miss closing by 1e-6, inside ShapeBuilder's chaining + // tolerance. The slide classifier must chain them the same way, or the frame's + // material sides are unknown and the floor contact blocks every direction. + var program = new OpenNest.CNC.Program(); + program.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0))); + program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(60, 0))); + program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(60, 60))); + program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 60))); + program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0.000001))); + program.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(10, 10))); + program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(50, 10))); + program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(50, 50))); + program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(10, 50))); + program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(10, 10))); + var frame = new Part(new Drawing("frame", program)); + var moving = MakeRectPart(10 + spacing, 10 + spacing, 5, 5); + var plate = new Plate(100, 100) { PartSpacing = spacing }; + plate.Parts.Add(frame); + plate.Parts.Add(moving); + + var distance = Compactor.Push(new List { moving }, plate, PushDirection.Up); + + Assert.Equal(35 - 2 * spacing, distance, 7); + Assert.Equal(50 - spacing, moving.BoundingBox.Top, 7); + } + private static void AssertClearance(Part moving, Part obstacle, double spacing) { var clearance = double.MaxValue; diff --git a/docs/geometry/directional-slides.md b/docs/geometry/directional-slides.md index 9cc63c0e..1915cffa 100644 --- a/docs/geometry/directional-slides.md +++ b/docs/geometry/directional-slides.md @@ -6,7 +6,7 @@ For initially non-overlapping closed material boundaries, a directional slide st `SpatialQuery.DirectionalDistance` overloads and CPU best-fit batches use the same event sources and contact resolver. Events carry both contact points in their initial world frames, rather than only a snapped distance. The classifier uses closed-loop material sectors, native-curve containment for hole depth, and the curvature of the supporting boundary at a tangential contact. A full-circle arc has no physical corner at its seam. Raw ray helpers remain first-touch primitives, not material-aware slide queries. -Open/incomplete chains and ambiguous contacts conservatively block. This is not an overlap-repair operation or a general replacement for layout validation. Caller-provided contact topology must represent the same boundaries and offsets as the query. Prepared geometry must not be mutated; prepare a classifier before sharing it between parallel queries. Edge-array queries still sort their arrays, and recover loop order from private copies before classifying contacts. +Open/incomplete chains and ambiguous contacts conservatively block. Loops are recovered with ShapeBuilder's `Tolerance.ChainTolerance`, so a source contour that closes within it (PEP-rounded drawings can miss by 1e-6) stays a closed loop; a stricter closure test would leave the whole part unresolved and freeze any part resting against it. This is not an overlap-repair operation or a general replacement for layout validation. Caller-provided contact topology must represent the same boundaries and offsets as the query. Prepared geometry must not be mutated; prepare a classifier before sharing it between parallel queries. Edge-array queries still sort their arrays, and recover loop order from private copies before classifying contacts. ## Callers