Files
OpenNest/OpenNest.Tests/CuttingPlanning/NativeContactRegressionTests.cs
T

196 lines
9.2 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 _));
}
[Fact]
public void ReusedNativeCurves_KeepContactAndContainmentVerdictsAcrossCallsAndThreads()
{
var lineStart = new Vector(-2, 0);
var lineEnd = new Vector(2, 0);
var arcCenter = new Vector(0, 0.003);
var arcStart = new Vector(0.003, 0.003);
var circleCenter = new Vector(0, 0);
var circleStart = new Vector(0.003, 0);
var line = PostVerificationGeometry.Curve.Create(lineStart, lineEnd, null, false);
var arc = PostVerificationGeometry.Curve.Create(arcStart, new Vector(-0.003, 0.003), arcCenter, false);
var circle = PostVerificationGeometry.Curve.Create(circleStart, circleStart, circleCenter, false);
var pairs = new[] { (line, arc), (arc, line), (line, circle), (circle, line), (arc, circle), (circle, arc) };
var probes = new[] { Vector.Zero, new Vector(0.003, 0), new Vector(0, 0.006) };
string Observe(PostVerificationGeometry.Curve a, PostVerificationGeometry.Curve b)
{
var inside = string.Join(",", probes.Select(p => $"{a.Contains(p)}:{b.Contains(p)}"));
try
{
var contacts = a.Contacts(b, out var overlap);
return $"{inside}|{overlap}|" + string.Join(";", contacts.Select(p =>
$"{BitConverter.DoubleToInt64Bits(p.X)}:{BitConverter.DoubleToInt64Bits(p.Y)}"));
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException)
{
return $"{inside}|{ex.GetType().Name}:{ex.Message}";
}
}
var expected = pairs.Select(p => Observe(
PostVerificationGeometry.Curve.Create(p.Item1.Start, p.Item1.End,
ReferenceEquals(p.Item1, arc) ? arcCenter : ReferenceEquals(p.Item1, circle) ? circleCenter : null, false),
PostVerificationGeometry.Curve.Create(p.Item2.Start, p.Item2.End,
ReferenceEquals(p.Item2, arc) ? arcCenter : ReferenceEquals(p.Item2, circle) ? circleCenter : null, false))).ToArray();
for (var repeat = 0; repeat < 3; repeat++)
Assert.Equal(expected, pairs.Select(p => Observe(p.Item1, p.Item2)).ToArray());
Parallel.For(0, 8, _ => Assert.Equal(expected, pairs.Select(p => Observe(p.Item1, p.Item2)).ToArray()));
// A separate pair has not initialized either native wrapper before the readers race.
var coldLine = PostVerificationGeometry.Curve.Create(lineStart, lineEnd, null, false);
var coldCircle = PostVerificationGeometry.Curve.Create(circleStart, circleStart, circleCenter, false);
Parallel.For(0, 8, _ => Assert.Equal(expected[2], Observe(coldLine, coldCircle)));
}
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;
}
}