diff --git a/OpenNest.Core/CNC/CuttingPlanning/LeadPathValidator.cs b/OpenNest.Core/CNC/CuttingPlanning/LeadPathValidator.cs
index cec569e..dde1eea 100644
--- a/OpenNest.Core/CNC/CuttingPlanning/LeadPathValidator.cs
+++ b/OpenNest.Core/CNC/CuttingPlanning/LeadPathValidator.cs
@@ -13,13 +13,6 @@ public sealed record LeadPathValidationResult(bool IsComplete, bool IsClear, str
/// Certifies actual emitted native lead paths against owned nominal material.
public static class LeadPathValidator
{
- ///
- /// Relative separation beyond which another material is skipped. Scaled by coordinate size,
- /// it stays far above both the contact tolerance and the rounding of the extents themselves,
- /// so a lead touching a boundary within tolerance is always checked.
- ///
- internal const double ClearanceMargin = 1e-6;
-
public static LeadPathValidationResult Check(OwnedExecution execution, LeadMaterialSnapshot target,
IReadOnlyList otherMaterials, CancellationToken token = default)
{
@@ -92,13 +85,12 @@ public static class LeadPathValidator
if (failure != null)
return new(true, false, $"Lead motion {i} contacts or enters target material outside its adjacent contour joint ({failure}).");
var reach = move.Curve.Extent;
- var margin = ClearanceMargin * (1 + reach.Magnitude);
foreach (var material in otherMaterials)
{
token.ThrowIfCancellationRequested();
// Material lies within its extent, so a lead well clear of that extent can
// neither touch it nor lie inside it.
- if (ReferenceEquals(material, target) || reach.IsSeparatedFrom(material.Extent, margin))
+ if (ReferenceEquals(material, target) || reach.IsClearOf(material.Extent))
continue;
if (CheckMaterial(material, null) != null)
return new(true, false, "Lead contacts or enters another placed material.");
diff --git a/OpenNest.Core/CNC/CuttingPlanning/ReleasedContourState.cs b/OpenNest.Core/CNC/CuttingPlanning/ReleasedContourState.cs
index be454bb..f975009 100644
--- a/OpenNest.Core/CNC/CuttingPlanning/ReleasedContourState.cs
+++ b/OpenNest.Core/CNC/CuttingPlanning/ReleasedContourState.cs
@@ -10,12 +10,6 @@ namespace OpenNest.CNC.CuttingPlanning;
/// Direct XY completed-contour checker. This is not physical machine safety.
public sealed class ReleasedContourState
{
- ///
- /// Relative separation beyond which a completed contour is skipped. Scaled by coordinate size,
- /// it stays far above the contact tolerance and the rounding of the extents themselves.
- ///
- internal const double ClearanceMargin = 1e-6;
-
private readonly List obstacles = new();
public ReleasedContourState Copy()
@@ -65,12 +59,11 @@ public sealed class ReleasedContourState
var reach = new PostVerificationGeometry.Extent(System.Math.Min(start.X, move.End.X),
System.Math.Min(start.Y, move.End.Y), System.Math.Max(start.X, move.End.X),
System.Math.Max(start.Y, move.End.Y));
- var margin = ClearanceMargin * (1 + reach.Magnitude);
foreach (var obstacle in obstacles)
{
token.ThrowIfCancellationRequested();
// A rapid well clear of a contour's extent can neither cross nor touch it.
- if (reach.IsSeparatedFrom(obstacle.Extent, margin))
+ if (reach.IsClearOf(obstacle.Extent))
continue;
if (PostVerificationGeometry.Crosses(start, move.End, obstacle.Curves, token))
findings.Add(new(PostVerificationKind.RapidCrossing, plate, part, obstacle.Part,
diff --git a/OpenNest.Core/Diagnostics/PostVerificationGeometry.cs b/OpenNest.Core/Diagnostics/PostVerificationGeometry.cs
index 0711cc3..4de99cc 100644
--- a/OpenNest.Core/Diagnostics/PostVerificationGeometry.cs
+++ b/OpenNest.Core/Diagnostics/PostVerificationGeometry.cs
@@ -21,22 +21,34 @@ internal static class PostVerificationGeometry
internal Extent Union(Extent other) => new(System.Math.Min(MinX, other.MinX),
System.Math.Min(MinY, other.MinY), System.Math.Max(MaxX, other.MaxX), System.Math.Max(MaxY, other.MaxY));
- /// The largest absolute coordinate, for scaling tolerances; NaN when any bound is.
- internal double Magnitude => System.Math.Max(System.Math.Max(System.Math.Abs(MinX), System.Math.Abs(MaxX)),
- System.Math.Max(System.Math.Abs(MinY), System.Math.Abs(MaxY)));
+ ///
+ /// The gap two extents need before their checks may be skipped: ten times the widest
+ /// absolute band any native contact query allows beyond an extent (the 0.00001 bounding-box
+ /// allowance of native intersections, the 0.0001 contact reach of tiny arcs).
+ ///
+ internal const double ClearMargin = 1e-3;
///
- /// True only when the extents are farther apart than on some axis,
- /// so nothing inside one can touch or enter the other. An extent with any nonfinite bound
- /// (including the empty one) is never separated.
+ /// Coordinates up to which extents may be skipped at all. Within it rounding stays far below
+ /// ; beyond it, where rounding of large supports can exceed any fixed
+ /// margin, every check runs.
///
- internal bool IsSeparatedFrom(Extent other, double margin) => IsFinite && other.IsFinite
- && double.IsFinite(margin)
- && (MaxX + margin < other.MinX || other.MaxX + margin < MinX
- || MaxY + margin < other.MinY || other.MaxY + margin < MinY);
+ internal const double WellConditionedLimit = 1e6;
- private bool IsFinite => double.IsFinite(MinX) && double.IsFinite(MinY)
- && double.IsFinite(MaxX) && double.IsFinite(MaxY);
+ ///
+ /// True only when both extents are finite, lie within and
+ /// are more than apart on some axis, so no native query can count
+ /// anything in one as touching or entering the other. Everything else must be checked.
+ ///
+ internal bool IsClearOf(Extent other) => IsWellConditioned && other.IsWellConditioned
+ && (MaxX + ClearMargin < other.MinX || other.MaxX + ClearMargin < MinX
+ || MaxY + ClearMargin < other.MinY || other.MaxY + ClearMargin < MinY);
+
+ // Math.Max propagates NaN and no comparison with NaN holds, so NaN and infinite bounds
+ // (including the empty extent's) fail this test too.
+ private bool IsWellConditioned =>
+ System.Math.Max(System.Math.Max(System.Math.Abs(MinX), System.Math.Abs(MaxX)),
+ System.Math.Max(System.Math.Abs(MinY), System.Math.Abs(MaxY))) <= WellConditionedLimit;
}
internal static void Validate(Vector point)
diff --git a/OpenNest.Tests/CuttingPlanning/NearbyMaterialCheckTests.cs b/OpenNest.Tests/CuttingPlanning/NearbyMaterialCheckTests.cs
index 15db0d8..9a2e7a9 100644
--- a/OpenNest.Tests/CuttingPlanning/NearbyMaterialCheckTests.cs
+++ b/OpenNest.Tests/CuttingPlanning/NearbyMaterialCheckTests.cs
@@ -145,14 +145,88 @@ public class NearbyMaterialCheckTests
Assert.Contains("uncertain", result.Reason);
}
+ [Fact]
+ public void LeadEndingWithinTheNativeBoxAllowanceOfAnotherPart_IsRefused()
+ {
+ // The other part lies 0.000003 above the lead: outside both extents, but within the
+ // 0.00001 allowance the native line intersection grants its bounding boxes.
+ var target = LeadMaterialSnapshot.Capture(LeadPathValidationTests.Rectangle(0, -2, 2, 0), Vector.Zero);
+ var other = LeadMaterialSnapshot.Capture(LeadPathValidationTests.Rectangle(-1, 0.000003, -0.25, 1), Vector.Zero);
+ Assert.True(other.IsComplete, other.Reason);
+ var lead = new Program();
+ lead.MoveTo(-0.5, 0);
+ lead.Codes.Add(new LinearMove(0, 0) { Layer = LayerType.Leadin });
+ lead.LineTo(0, -2);
+
+ var result = LeadPathValidator.Check(Read(lead), target, [target, other]);
+
+ Assert.True(result.IsComplete, result.Reason);
+ Assert.False(result.IsClear);
+ }
+
+ [Fact]
+ public void RapidBesideAHugeCompletedCircle_IsStillACrossing()
+ {
+ // Radius 5e11 about (5e11, 0): its extent starts at x = 0, but at this size rounding lets
+ // the native query count a rapid at x = -0.00001 as touching it.
+ var state = new ReleasedContourState();
+ state.Check(Read(HugeCircle()), null, 1);
+ var rapid = new Program();
+ rapid.MoveTo(-1e-5, 1);
+
+ var findings = state.Check(Read(rapid), new Vector(-1e-5, -1), 2);
+
+ Assert.Contains(findings, f => f.Kind == PostVerificationKind.RapidCrossing && f.OtherPartNumber == 1);
+ }
+
+ [Fact]
+ public void LeadBesideAHugeCircle_IsRefused()
+ {
+ var huge = LeadMaterialSnapshot.Capture(HugeCircle(), Vector.Zero);
+ Assert.True(huge.IsComplete, huge.Reason);
+ var target = LeadMaterialSnapshot.Capture(LeadPathValidationTests.Rectangle(-2, -1, -0.002, -0.5), Vector.Zero);
+ var lead = new Program();
+ lead.MoveTo(-1e-5, 1);
+ lead.Codes.Add(new LinearMove(-1e-5, -0.5) { Layer = LayerType.Leadin });
+ lead.Codes.Add(new LinearMove(-0.002, -0.5) { Layer = LayerType.Leadin });
+ lead.LineTo(-2, -0.5);
+
+ var result = LeadPathValidator.Check(Read(lead), target, [target, huge]);
+
+ Assert.True(result.IsComplete, result.Reason);
+ Assert.False(result.IsClear);
+ }
+
+ [Theory]
+ [InlineData(2.002, 0, true)] // 0.002 apart: clear of the 0.001 margin.
+ [InlineData(2.0005, 0, false)] // 0.0005 apart: inside the margin.
+ [InlineData(2.002, 2e6, false)] // Clear, but beyond the well-conditioned range: always checked.
+ public void IsClearOf_OnlyWellApartAndWellConditionedExtentsAreSkipped(double left, double offset, bool clear)
+ {
+ var a = new PostVerificationGeometry.Extent(offset, offset, offset + 2, offset + 2);
+ var b = new PostVerificationGeometry.Extent(offset + left, offset, offset + left + 1, offset + 1);
+
+ Assert.Equal(clear, a.IsClearOf(b));
+ Assert.Equal(clear, b.IsClearOf(a));
+ }
+
[Fact]
public void ExtentWithANaNBound_IsNeverSeparated()
{
var partial = new PostVerificationGeometry.Extent(double.NaN, 0, double.NaN, 1);
var other = new PostVerificationGeometry.Extent(0, 4, 1, 5);
- Assert.False(partial.IsSeparatedFrom(other, 1e-6));
- Assert.False(other.IsSeparatedFrom(partial, 1e-6));
+ Assert.False(partial.IsClearOf(other));
+ Assert.False(other.IsClearOf(partial));
+ Assert.False(PostVerificationGeometry.Extent.None.IsClearOf(other)); // Infinite bounds.
+ }
+
+ private static Program HugeCircle()
+ {
+ var p = new Program();
+ p.MoveTo(0, 0);
+ p.Codes.Add(new ArcMove(Vector.Zero, new Vector(5e11, 0), RotationType.CCW));
+ return p;
}
private static Program CleanCircle(double radius)
diff --git a/docs/cutting-planner.md b/docs/cutting-planner.md
index 1e2ce36..3f7fda4 100644
--- a/docs/cutting-planner.md
+++ b/docs/cutting-planner.md
@@ -102,12 +102,13 @@ shortened, disabled or substituted as a search fallback.
Every candidate rapid is checked against contours already completed, including
earlier holes in the same part. Future contours are not yet obstacles. Rapid and
-lead checks skip contours and material whose extents are more than 1e-6 x
-(1 + coordinate size) clear of the motion; anything closer, touching included, gets
-the full native check. An arc's extent is its whole supporting circle widened to
-the distance at which the native contact query still counts it as touched (for a
-very small arc up to 1e-4 beyond its radius), and an extent with any nonfinite
-bound is never skipped. Actual
+lead checks skip contours and material whose extents are more than 0.001 clear
+of the motion, ten times the widest band any native contact query allows beyond an
+extent; anything closer, touching included, gets the full native check. An arc's
+extent is its whole supporting circle widened to the distance at which the native
+contact query still counts it as touched (for a very small arc up to 0.0001 beyond
+its radius). Nothing is skipped when either extent has a nonfinite bound or reaches
+beyond 1e6, where rounding of large supports can exceed any fixed margin. Actual
lead-in and lead-out line/arc paths must stay in target scrap and avoid other
placed material; holes in other parts remain scrap. Tangent/coincident contacts
outside the genuine target contour joint and numerically uncertain queries refuse.