mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-29 17:01:06 -04:00
fix(planning): accept arc leads tangent to a straight edge at their joint
An arc lead-in meeting a straight window edge tangentially has a double root at its joint. Rounding split it into two contacts a few 1e-8 apart, or dropped it so the exact ray fallback reported the joint as an uncertain contact. Either refused a valid entry, so N26-27U's windows lost most of their entries and the plate never planned. Treat a contact within 1e-6 of a joint its boundary passes through as the joint, and let the exact line fallback skip ray hits at contacts already recorded. The pinned-hole test now expects the other hole's nearest entry rather than the fallback both runs used to share. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c798be9b95
commit
6a85593bd4
4 files changed
+67
-10
No files matched your search
@@ -294,13 +294,25 @@ public static class LeadPathValidator
|
||||
if (--budget < 0)
|
||||
throw new NotSupportedException("Lead validation exceeds the native query limit.");
|
||||
var contacts = curve.Contacts(boundary, out var overlap);
|
||||
if (overlap || contacts.Any(p => permittedJoint is not { } joint
|
||||
|| p.DistanceTo(joint) > PostVerificationGeometry.Epsilon))
|
||||
if (overlap || contacts.Any(p => permittedJoint is not { } joint || !AtJoint(p, joint, boundary)))
|
||||
return $"boundary; allowed={permittedJoint}; contacts={string.Join(";", contacts)}; overlap={overlap}";
|
||||
}
|
||||
// With all other boundary contacts excluded, the connected open path
|
||||
// has constant material membership. Use the native arc midpoint, not
|
||||
// the chord midpoint or endpoints (which can both lie in scrap).
|
||||
return !allowInterior && material.ContainsMaterial(curve.Midpoint, token) ? "interior" : null;
|
||||
|
||||
// A lead tangent to the boundary it joins (an arc lead-in meeting a straight edge) has a
|
||||
// double root at the joint that rounding splits into two contacts a few 1e-8 apart; one
|
||||
// within JointTolerance of a joint the boundary itself passes through is still the joint.
|
||||
static bool AtJoint(Vector point, Vector joint, PostVerificationGeometry.Curve boundary)
|
||||
{
|
||||
var distance = point.DistanceTo(joint);
|
||||
return distance <= PostVerificationGeometry.Epsilon
|
||||
|| distance <= JointTolerance && boundary.Contains(joint);
|
||||
}
|
||||
}
|
||||
|
||||
// As SlideContact's: far below any kerf, far above a tangent root's rounding drift.
|
||||
private const double JointTolerance = 1e-6;
|
||||
}
|
||||
@@ -397,9 +397,9 @@ internal static class PostVerificationGeometry
|
||||
points.Add(point);
|
||||
// Existing exact line/ray contact semantics guard native queries which
|
||||
// suppress very short or nearly parallel intersections. Uncertainty refuses.
|
||||
if (Center is null && !SameSupport(other) && nativeContactCount == 0 && ExactLineContact(this, other))
|
||||
if (Center is null && !SameSupport(other) && nativeContactCount == 0 && ExactLineContact(this, other, points))
|
||||
throw new NotSupportedException("Native contact query is numerically uncertain.");
|
||||
if (other.Center is null && Center.HasValue && nativeContactCount == 0 && ExactLineContact(other, this))
|
||||
if (other.Center is null && Center.HasValue && nativeContactCount == 0 && ExactLineContact(other, this, points))
|
||||
throw new NotSupportedException("Native contact query is numerically uncertain.");
|
||||
return points;
|
||||
|
||||
@@ -415,16 +415,35 @@ internal static class PostVerificationGeometry
|
||||
/// endpoint (both stop half-way when both endpoints are contained): a line meeting a
|
||||
/// tangent arc at their shared vertex is otherwise rediscovered there as a contact the
|
||||
/// native query missed, because rounding can drop the tangent root of its quadratic.
|
||||
/// For the same reason a ray hit within <see cref="RaySeamTolerance"/> of a
|
||||
/// <paramref name="recorded"/> contact is that contact: an arc lead ending tangent on a
|
||||
/// straight edge, mid-edge, has its end recorded and its double root dropped.
|
||||
/// </summary>
|
||||
private static bool ExactLineContact(Curve line, Curve curve)
|
||||
private static bool ExactLineContact(Curve line, Curve curve, IReadOnlyList<Vector> recorded)
|
||||
{
|
||||
var direction = (line.End - line.Start) * (1 / line.Length);
|
||||
var startRecorded = curve.Contains(line.Start);
|
||||
var endRecorded = curve.Contains(line.End);
|
||||
var forward = endRecorded ? startRecorded ? line.Length / 2 : 0 : line.Length;
|
||||
var backward = startRecorded ? endRecorded ? line.Length / 2 : 0 : line.Length;
|
||||
return (forward > 0 && curve.ContactAfterStart(line.Start, direction, forward))
|
||||
|| (backward > 0 && curve.ContactAfterStart(line.End, direction * -1, backward));
|
||||
return (forward > 0 && Unrecorded(line.Start, direction, forward))
|
||||
|| (backward > 0 && Unrecorded(line.End, direction * -1, backward));
|
||||
|
||||
bool Unrecorded(Vector origin, Vector ray, double length)
|
||||
{
|
||||
var hits = new List<double>();
|
||||
curve.ContactAfterStart(origin, ray, length, hits);
|
||||
foreach (var distance in hits)
|
||||
{
|
||||
var hit = origin + ray * distance;
|
||||
var known = false;
|
||||
foreach (var point in recorded)
|
||||
known |= point.DistanceTo(hit) <= RaySeamTolerance;
|
||||
if (!known)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -46,6 +46,30 @@ public class LeadPathValidationTests
|
||||
AssertUnsafe(straight, target);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 0)]
|
||||
[InlineData(69.833896, 18.839189)] // A rotated part's window edge, as N26-27U's.
|
||||
[InlineData(-14.2733, 31.610594)]
|
||||
public void ArcLeadTangentToAWindowEdge_TouchesOnlyAtItsJoint(double dx, double dy)
|
||||
{
|
||||
// The semicircle's double root at the joint rounds into two contacts a few 1e-8 apart.
|
||||
var clean = Rectangle(20 + dx, 10 + dy, 40 + dx, 40 + dy);
|
||||
clean.Codes.AddRange(Rectangle(25.1 + dx, 17.776689 + dy, 38.378 + dx, 30.026689 + dy).Codes);
|
||||
var target = LeadMaterialSnapshot.Capture(clean, Vector.Zero);
|
||||
Assert.True(target.IsComplete, target.Reason);
|
||||
for (var joint = 17.901689; joint < 30; joint += 1.37)
|
||||
{
|
||||
var radius = 0.0625;
|
||||
var lead = new Program();
|
||||
lead.MoveTo(38.378 - 2 * radius + dx, joint + dy);
|
||||
lead.Codes.Add(new ArcMove(new Vector(38.378 + dx, joint + dy), new Vector(38.378 - radius + dx, joint + dy),
|
||||
RotationType.CW)
|
||||
{ Layer = LayerType.Leadin });
|
||||
lead.LineTo(38.378 + dx, 17.776689 + dy);
|
||||
AssertClear(lead, target);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OtherHoleIsScrap_AndLineArcTangenciesReject()
|
||||
{
|
||||
|
||||
@@ -79,10 +79,12 @@ public class PinnedContourTests
|
||||
Assert.True(pinnedResult.IndependentlyReplayed);
|
||||
var choices = Assert.Single(pinnedResult.ProposedOrder).ContourChoices;
|
||||
Assert.Equal(new Vector(4, 3), Assert.Single(choices, c => c.ContourOrdinal == 0).Point);
|
||||
// The other hole and the outside are chosen exactly as for the same drawing without pins.
|
||||
// The outside is chosen exactly as for the same drawing without pins. The other hole stays
|
||||
// automatic, entered nearest where the pinned hole is left: (6, 3), facing the pin at (4, 3).
|
||||
var plain = Assert.Single(plainResult.ProposedOrder).ContourChoices;
|
||||
Assert.Equal(plain.Where(c => c.ContourOrdinal != 0).Select(c => (c.ContourOrdinal, c.EntityOrdinal, c.Point)),
|
||||
choices.Where(c => c.ContourOrdinal != 0).Select(c => (c.ContourOrdinal, c.EntityOrdinal, c.Point)));
|
||||
Assert.Equal(plain.Where(c => c.ContourOrdinal == 2).Select(c => (c.ContourOrdinal, c.EntityOrdinal, c.Point)),
|
||||
choices.Where(c => c.ContourOrdinal == 2).Select(c => (c.ContourOrdinal, c.EntityOrdinal, c.Point)));
|
||||
Assert.True(Assert.Single(choices, c => c.ContourOrdinal == 1).Point.DistanceTo(new Vector(6, 3)) < 1e-6);
|
||||
// The pinned hole uses the pin's straight lead; the others use the nest's arc lead-ins.
|
||||
var leadIns = ExecutionMotionReader.Read(pinnedResult.ProposedOrder[0].CopyProgram(), Vector.Zero, null, default)
|
||||
.Motions.Where(m => m.Layer == LayerType.Leadin).ToArray();
|
||||
|
||||
Reference in new issue
Block a user