Files
OpenNest/OpenNest.Tests/CuttingPlanning/NativeContactRegressionTests.cs
T
aj 18a61717fe fix(cutting): accept tangent line-arc joints in material capture
Plan Cutting refused ordinary filleted parts with "Native contact query is
numerically uncertain." Material capture checks every curve pair of a ring,
adjacent ones included. Where a line meets a tangent arc at their shared
vertex, rounding can drop the tangent root of the native line/circle
quadratic; the exact ray cast from the line's far end then reached the
vertex, which is already recorded as an endpoint contact, and was read as a
contact the native query missed. Rounded rectangles rotated off-axis were
refused 1037 times in 1080 before this change and 0 times after.

The exactness rays now stop short of a line endpoint the other curve already
contains (half-way from each end when both are contained), so together they
still cover every other point of the line and any unrecorded contact still
refuses. The native kernel and tolerances are unchanged.

Regressions: four tangent-fillet rings and a rotated filleted part planned
through CuttingPlanBatch (red before, green after); a line ending inside a
small circle's contact band stays uncertain in both directions. Mutations that
treat either endpoint as always contained, drop the start ray, or count
recorded endpoints as native contacts all fail.

Project Memory: 41880037
2026-10-06 10:37:07 -04:00

152 lines
6.5 KiB
C#

using OpenNest.CNC;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.Diagnostics;
using OpenNest.Engine.CuttingPlanning;
using OpenNest.Geometry;
namespace OpenNest.Tests.CuttingPlanning;
public class NativeContactRegressionTests
{
[Fact]
public void TangentArcLead_RefusesFilteredSupportingCircleNaNs()
{
var target = LeadMaterialSnapshot.Capture(LeadPathValidationTests.Rectangle(0, -1, 1, 1), Vector.Zero);
var obstacle = new Program();
obstacle.MoveTo(-0.22, 0.26);
obstacle.ArcTo(-0.22, 0.26, -0.22, 0.16, RotationType.CCW);
var material = LeadMaterialSnapshot.Capture(obstacle, Vector.Zero);
Assert.True(target.IsComplete, target.Reason);
Assert.True(material.IsComplete, material.Reason);
var lead = new Program();
lead.MoveTo(-0.2, 0);
lead.Codes.Add(new ArcMove(Vector.Zero, new Vector(-0.1, 0), RotationType.CW) { Layer = LayerType.Leadin });
lead.LineTo(0, 1);
var execution = ExecutionMotionReader.Read(lead, Vector.Zero, null, default);
var witness = new Vector(-0.16, 0.08);
Assert.True(execution.Motions[1].Curve.Contains(witness));
Assert.True(material.Rings[0][0].Contains(witness));
var result = LeadPathValidator.Check(execution, target, [material]);
Assert.False(result.IsComplete);
Assert.False(result.IsClear);
}
[Theory]
[InlineData(0.1, -0.22, 0.16)]
[InlineData(0.2, -0.28, 0.24)]
[InlineData(0.05, -0.19, 0.12)]
public void CircularNearTangency_BothDirectionsNeverCertifyNoContact(double radius, double x, double y)
{
var a = PostVerificationGeometry.Curve.Create(new Vector(-0.2, 0), Vector.Zero, new Vector(-0.1, 0), true);
var center = new Vector(x, y);
var b = PostVerificationGeometry.Curve.Create(center + new Vector(0, radius), center + new Vector(0, -radius), center, true);
foreach (var reverse in new[] { false, true })
{
try
{
var contacts = (reverse ? b : a).Contacts(reverse ? a : b, out var overlap);
Assert.True(overlap || contacts.Count > 0, "Near-tangent circular query silently certified no contact.");
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException)
{
// Numerical uncertainty is a refusal, never a disjoint certificate.
}
}
}
[Fact]
public void TouchingArcSplitHoles_RefuseMaterialCertificate()
{
var source = LeadPathValidationTests.Rectangle(-1, -1, 1, 1);
foreach (var center in new[] { new Vector(-0.1, 0), new Vector(-0.22, 0.16) })
{
source.MoveTo(center.X, center.Y + 0.1);
source.ArcTo(center.X, center.Y - 0.1, center.X, center.Y, RotationType.CCW);
source.ArcTo(center.X, center.Y + 0.1, center.X, center.Y, RotationType.CCW);
}
var snapshot = LeadMaterialSnapshot.Capture(source, Vector.Zero);
Assert.False(snapshot.IsComplete);
Assert.NotNull(snapshot.Reason);
}
// A line meeting a fillet arc tangentially at their shared vertex: rounding can drop the
// tangent root of the native line/circle quadratic, and the exact ray from the line's far end
// then rediscovered the already-recorded vertex as a missed contact.
[Theory]
[InlineData(0.125, 7, 0, 0)]
[InlineData(1.0, 33, 55.6, 4.8)]
[InlineData(0.5, 61, 110.3, 57.1)]
[InlineData(0.25, 80, 0, 0)]
public void TangentFilletRing_CertifiesAsSimpleMaterial(double radius, double degrees, double x, double y)
{
var snapshot = LeadMaterialSnapshot.Capture(
RoundedRectangle(7, 3, radius, degrees * System.Math.PI / 180, new Vector(x, y)), Vector.Zero);
Assert.True(snapshot.IsComplete, snapshot.Reason);
}
[Fact]
public void RotatedFilletedPart_PlansReadyOnAPlate()
{
var plate = new Nest().CreatePlate();
plate.Size = new Size(100, 100);
plate.Parts.Add(new Part(new Drawing("filleted",
RoundedRectangle(7, 3, 1, 33 * System.Math.PI / 180, Vector.Zero)), new Vector(20, 20)));
var proposal = CuttingPlanBatch.Capture([plate], ExplicitContourTests.Parameters(), false).Plan();
Assert.True(proposal.CanApply, string.Join(Environment.NewLine, proposal.Describe("in")));
}
// The line ends 0.0000015 above a radius-0.003 circle: inside the circle's exact contact band,
// where the native query finds nothing. That end is not a recorded contact, so the ray that
// reaches it from the other end must still run.
[Theory]
[InlineData(false)]
[InlineData(true)]
public void LineEndingInsideACircleContactBand_StaysUncertain(bool reversed)
{
const double radius = 0.003;
var near = new Vector(0, radius + 0.0000015);
var far = new Vector(-0.01, near.Y);
var line = reversed
? PostVerificationGeometry.Curve.Create(near, far, null, false)
: PostVerificationGeometry.Curve.Create(far, near, null, false);
var circle = PostVerificationGeometry.Curve.Create(new Vector(radius, 0), new Vector(radius, 0), Vector.Zero, false);
Assert.False(circle.Contains(near));
Assert.Throws<NotSupportedException>(() => line.Contacts(circle, out _));
Assert.Throws<NotSupportedException>(() => circle.Contacts(line, out _));
}
private static Program RoundedRectangle(double width, double height, double radius, double angle, Vector at)
{
var cos = System.Math.Cos(angle);
var sin = System.Math.Sin(angle);
Vector Place(double x, double y) => new(at.X + x * cos - y * sin, at.Y + x * sin + y * cos);
var program = new Program();
var start = Place(radius, 0);
program.MoveTo(start.X, start.Y);
void Line(double x, double y)
{
var end = Place(x, y);
program.LineTo(end.X, end.Y);
}
void Fillet(double x, double y, double cx, double cy)
{
var end = Place(x, y);
var center = Place(cx, cy);
program.ArcTo(end.X, end.Y, center.X, center.Y, RotationType.CCW);
}
Line(width - radius, 0);
Fillet(width, radius, width - radius, radius);
Line(width, height - radius);
Fillet(width - radius, height, width - radius, height - radius);
Line(radius, height);
Fillet(0, height - radius, radius, height - radius);
Line(0, radius);
Fillet(radius, 0, radius, radius);
return program;
}
}