fix(cutting): anchor near-corner entries to their native edge

This commit is contained in:
aj committed 2026-10-11 01:14:50 -04:00
1 parent dc142cc88d
commit 08acb092a7
3 files changed
+41 -5

No files matched your search

@@ -254,12 +254,11 @@ public sealed class PreparedContours
.Where(p => p.Kind == AutomaticEntryKind.ConvexCorner))
{
token.ThrowIfCancellationRequested();
var cornerPoint = End(shape.Entities[corner.EntityOrdinal]);
// Back INTO each incident edge from the corner: the edge ending at the
// corner retreats against its own travel, the edge starting at the corner
// advances along its own travel.
AddInset(corner.EntityOrdinal, cornerPoint, inward: false);
AddInset((corner.EntityOrdinal + 1) % count, cornerPoint, inward: true);
AddInset(corner.EntityOrdinal, inward: false);
AddInset((corner.EntityOrdinal + 1) % count, inward: true);
}
}
@@ -276,7 +275,7 @@ public sealed class PreparedContours
token.ThrowIfCancellationRequested();
return fallbacks;
void AddInset(int entityOrdinal, Vector corner, bool inward)
void AddInset(int entityOrdinal, bool inward)
{
// `inward` selects along the edge's own travel from its start; without it the
// point retreats against travel. Both ways move BACK INTO the edge from the
@@ -285,7 +284,9 @@ public sealed class PreparedContours
return;
var direction = (line.EndPoint - line.StartPoint).Normalize();
var offset = direction * (2 * lead);
var point = inward ? corner + offset : corner - offset;
// Adjacent native endpoints may differ within contour-continuity tolerance.
// Anchor to the selected edge itself, never the preceding edge's vertex.
var point = inward ? line.StartPoint + offset : line.EndPoint - offset;
// Strictly inside the edge by projection parameter (distance alone loses the
// sign when a short edge is overshoot): never the corner, never the far
// endpoint (where a reflex vertex might sit). Short edges omit the point.
@@ -310,6 +310,36 @@ public class ContourEntryCandidateTests
Assert.All(near, c => Assert.True(c.Kind > AutomaticEntryKind.TangentJoint));
}
[Fact]
public void NearCornerOnRoundedJointStaysOnItsSelectedLine()
{
// Native contour joints can differ within the accepted closure tolerance. The
// preceding edge's corner is off the following edge by one micron, while the
// near-corner entry must lie on the following edge itself.
var shape = new Shape();
shape.Entities.Add(new Line(0, 0, 10, 0));
shape.Entities.Add(new Line(10, 0, 10, 10));
shape.Entities.Add(new Line(10, 10.000001, 0, 10.000001));
shape.Entities.Add(new Line(0, 10.000001, 0, 0));
shape.UpdateBounds();
// Capture constructs this exact owned native shape after program conversion;
// build it directly to characterize its legal, sub-tolerance joint discrepancy.
var constructor = typeof(PreparedContours).GetConstructor(
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance,
null, [typeof(Shape[]), typeof(List<Entity>), typeof(OpenNest.CNC.CuttingStrategy.CuttingParameters)], null);
Assert.NotNull(constructor);
var prepared = (PreparedContours)constructor.Invoke([
new[] { shape }, new List<Entity>(), ExplicitContourTests.Parameters()]);
var near = prepared.AutomaticEntryCandidatesWithFallbacks(0)
.Where(c => c.Kind == AutomaticEntryKind.NearCorner).ToList();
Assert.NotEmpty(near);
Assert.All(near, c => Assert.Equal(c.Choice.Point,
prepared.Entry(c.Choice.ContourOrdinal, c.Choice.EntityOrdinal, c.Choice.Point).Point));
Assert.Contains(near, c => c.Choice.EntityOrdinal == 2 &&
System.Math.Abs(c.Choice.Point.Y - 10.000001) < 1e-10);
Assert.Throws<ArgumentException>(() => prepared.Entry(0, 2, new Vector(9.4, 10)));
}
[Fact]
public void ShortEdge_OmitsNearCornerInsteadOfExtrapolating()
{
+5
View File
@@ -192,6 +192,11 @@ more of the catalogue; a later side cannot lose a usable point merely because
an earlier side's scan passed it. Replacements preserve other covered sides and
global rank, never exceed the cap, and stop evaluating the tail once coverage
settles. Smaller caps retain rank priority rather than promising all-side coverage.
An inset near a convex corner is measured along the selected native straight edge from
that edge's own endpoint, not a neighboring entity's endpoint. This keeps the
candidate on its selected entity when a joined contour has an accepted rounding gap;
no entry-membership or lead-clearance tolerance is widened.
The next cut is the next unfinished part in a supplied order — recomputed
after every learned-order replan — or, in the full fallback search, the nearest
dependency-ready remaining part, stable-ordinal ties; the last part has no