mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-06 06:02:11 -04:00
fix(cutting): widen arc extents to their native contact band
Review of the extent filter (bfe2e51) found it could skip checks the
native queries would have flagged. ContactAfterStart counts a point as
touching an arc while its squared distance from the centre is within
1e-8 x max(1, 2r) of r^2, which for small arcs reaches well past the
radius (up to 1e-4 as r -> 0), more than the filter's margin. A rapid
0.000002 outside a radius-0.001 circle lost its crossing, the pre-post
review lost the same finding, and a lead grazing a radius-0.003 circle
turned from an uncertain check into a clear one.
An arc's extent now uses that contact reach, computed from the same
slack expression as ContactAfterStart, so every point a native query
can count as contact lies inside it. The three cases above are
regression tests.
This commit is contained in:
1 parent
2419a9e2e3
commit
c1c8d5fc36
2 files changed
+96
-4
No files matched your search
@@ -106,11 +106,13 @@ internal static class PostVerificationGeometry
|
||||
: new Line(Start, End);
|
||||
|
||||
/// <summary>
|
||||
/// A conservative axis-aligned extent: an arc's whole supporting circle, a line's endpoints.
|
||||
/// Nonfinite geometry yields NaN bounds, which no separation test can pass.
|
||||
/// A conservative axis-aligned extent of everything a native query may count as touching
|
||||
/// this curve: for an arc, its whole supporting circle widened by the contact band of
|
||||
/// <see cref="ContactAfterStart"/>; for a line, its endpoints. Nonfinite geometry yields NaN
|
||||
/// bounds, which no separation test can pass.
|
||||
/// </summary>
|
||||
internal Extent Extent => Center is { } center
|
||||
? new(center.X - Radius, center.Y - Radius, center.X + Radius, center.Y + Radius)
|
||||
? new(center.X - ContactReach, center.Y - ContactReach, center.X + ContactReach, center.Y + ContactReach)
|
||||
: new(System.Math.Min(Start.X, End.X), System.Math.Min(Start.Y, End.Y),
|
||||
System.Math.Max(Start.X, End.X), System.Math.Max(Start.Y, End.Y));
|
||||
|
||||
@@ -246,6 +248,13 @@ internal static class PostVerificationGeometry
|
||||
point.X - Center.Value.X)) <= System.Math.Abs(Sweep) + Epsilon / Radius
|
||||
|| point.DistanceTo(Start) <= Epsilon || point.DistanceTo(End) <= Epsilon;
|
||||
|
||||
// How far past r^2 a squared distance from the centre still counts as touching the arc.
|
||||
// For small arcs this band reaches well beyond the radius (sqrt(1e-8) = 1e-4 as r -> 0).
|
||||
private double ContactSlack => Epsilon * System.Math.Max(1, Radius * 2);
|
||||
|
||||
// The largest distance from the centre that ContactAfterStart can count as contact.
|
||||
private double ContactReach => System.Math.Sqrt(Radius * Radius + ContactSlack);
|
||||
|
||||
internal bool ContactAfterStart(Vector origin, Vector direction, double length)
|
||||
{
|
||||
if (Center is { } center)
|
||||
@@ -256,7 +265,7 @@ internal static class PostVerificationGeometry
|
||||
var projection = Dot(relative, direction);
|
||||
var perpendicular = Cross(relative, direction);
|
||||
var square = Radius * Radius - perpendicular * perpendicular;
|
||||
if (square < -Epsilon * System.Math.Max(1, Radius * 2))
|
||||
if (square < -ContactSlack)
|
||||
return false;
|
||||
var offset = System.Math.Sqrt(System.Math.Max(0, square));
|
||||
return Hit(projection - offset) || Hit(projection + offset);
|
||||
|
||||
@@ -80,6 +80,89 @@ public class NearbyMaterialCheckTests
|
||||
Assert.Contains(findings, f => f.Kind == PostVerificationKind.RapidCrossing && f.OtherPartNumber == 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RapidJustOutsideASmallCompletedCircle_IsStillACrossing()
|
||||
{
|
||||
// A circle of radius 0.001 counts as touched up to sqrt(r^2 + 1e-8) from its centre, beyond
|
||||
// its radius; the rapid passes 0.000002 outside the radius, inside that band.
|
||||
var state = new ReleasedContourState();
|
||||
Assert.Empty(state.Check(Read(Circle(0.001)), Vector.Zero, 1)
|
||||
.Where(f => f.Kind == PostVerificationKind.RapidCrossing));
|
||||
var next = new Program();
|
||||
next.MoveTo(0.001002, 0.002);
|
||||
|
||||
var findings = state.Check(Read(next), new Vector(0.001002, -0.002), 2);
|
||||
|
||||
Assert.Contains(findings, f => f.Kind == PostVerificationKind.RapidCrossing && f.OtherPartNumber == 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrePostReview_RapidJustOutsideASmallCircle_IsStillReported()
|
||||
{
|
||||
const double radius = 0.003;
|
||||
const double y = radius + 0.0000015;
|
||||
var first = new Part(new Drawing("circle", CleanCircle(radius)));
|
||||
var circle = Circle(radius);
|
||||
circle.Codes.Add(new LinearMove(-0.01, y) { Layer = LayerType.Scribe }); // Ends left of the circle.
|
||||
Assert.True(first.RestoreLeadInProgram(circle, true));
|
||||
var second = new Part(new Drawing("square",
|
||||
LeadPathValidationTests.Rectangle(0.01, y + 0.0002, 0.02, y + 0.0102)));
|
||||
var square = new Program();
|
||||
square.MoveTo(0.01, y);
|
||||
square.Codes.Add(new LinearMove(0.01, y + 0.0002) { Layer = LayerType.Leadin });
|
||||
square.LineTo(0.01, y + 0.0102); square.LineTo(0.02, y + 0.0102);
|
||||
square.LineTo(0.02, y + 0.0002); square.LineTo(0.01, y + 0.0002);
|
||||
Assert.True(second.RestoreLeadInProgram(square, true));
|
||||
var nest = new Nest();
|
||||
var plate = nest.CreatePlate();
|
||||
plate.Parts.Add(first);
|
||||
plate.Parts.Add(second);
|
||||
|
||||
var report = PostVerificationAnalyzer.Analyze(nest);
|
||||
|
||||
Assert.Contains(report.Findings, f => f.Kind == PostVerificationKind.RapidCrossing
|
||||
&& f.PartNumber == 2 && f.OtherPartNumber == 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LeadGrazingASmallCircle_StaysAnUncertainCheck()
|
||||
{
|
||||
// The lead passes 0.0000015 outside a radius-0.003 circle: inside its native contact band,
|
||||
// where the native query cannot certify contact either way.
|
||||
const double radius = 0.003;
|
||||
const double y = radius + 0.0000015;
|
||||
var target = LeadMaterialSnapshot.Capture(LeadPathValidationTests.Rectangle(0.01, y, 0.02, y + 0.01), Vector.Zero);
|
||||
var other = LeadMaterialSnapshot.Capture(CleanCircle(radius), Vector.Zero);
|
||||
Assert.True(other.IsComplete, other.Reason);
|
||||
var lead = new Program();
|
||||
lead.MoveTo(-0.01, y);
|
||||
lead.Codes.Add(new LinearMove(0.01, y) { Layer = LayerType.Leadin });
|
||||
lead.LineTo(0.01, y + 0.01);
|
||||
|
||||
var result = LeadPathValidator.Check(Read(lead), target, [target, other]);
|
||||
|
||||
Assert.False(result.IsComplete);
|
||||
Assert.Contains("uncertain", result.Reason);
|
||||
}
|
||||
|
||||
private static Program CleanCircle(double radius)
|
||||
{
|
||||
var p = new Program();
|
||||
p.MoveTo(radius, 0);
|
||||
p.Codes.Add(new ArcMove(new Vector(radius, 0), Vector.Zero, RotationType.CCW));
|
||||
return p;
|
||||
}
|
||||
|
||||
// A full circle about the origin cut after a lead-in from 0.0002 outside it.
|
||||
private static Program Circle(double radius)
|
||||
{
|
||||
var p = new Program();
|
||||
p.MoveTo(radius + 0.0002, 0);
|
||||
p.Codes.Add(new LinearMove(radius, 0) { Layer = LayerType.Leadin });
|
||||
p.Codes.Add(new ArcMove(new Vector(radius, 0), Vector.Zero, RotationType.CCW));
|
||||
return p;
|
||||
}
|
||||
|
||||
// A 2 x 2 square at (x, y) with a lead-in from 0.5 below its corner, optionally preceded by
|
||||
// a rapid to a given point.
|
||||
private static Program Square(double x, double y, Vector? via = null)
|
||||
|
||||
Reference in new issue
Block a user