fix(geometry): let parts push off a frame whose contour misses closing by 1e-6

PEP-rounded drawings can leave a contour 1e-6 short of closing. ShapeBuilder
chains it as a loop at Tolerance.ChainTolerance, but the slide classifier
re-recovered loops at 1e-7, so the whole part became unresolved and every
contact with it blocked. Parts placed in its cutout at exactly the part
spacing could not be pushed (Y/X keys) in any direction, even away from the
wall. Recover loops with the same tolerance ShapeBuilder uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-10-28 14:03:38 -04:00
1 parent c6b6678a2f
commit b8e8cc387d
3 files changed
+40 -2

No files matched your search

+6 -1
View File
@@ -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<Entity> 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<Entity> entities, int first, int last)
{
+33
View File
@@ -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<Part> { 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;
+1 -1
View File
@@ -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