mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-12 18:44:12 -04:00
196 lines
9.2 KiB
C#
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;
|
|
}
|
|
}
|