fix(cutting): skip checks only for well-clear, well-conditioned extents

The delta review found two more ways the extent filter could skip a
check a native query would have flagged:

- Native line intersections accept points 0.00001 outside each line's
  bounding box, so a lead 0.000003 from another part touched it while
  their extents were 0.000003 apart, beyond the 1e-6 margin.
- Around a circle of radius 5e11, rounding let the native query count
  a rapid at x = -0.00001 as touching although the circle's extent
  started at x = 0; the margin scaled only with the rapid's own size.

Rather than chase each tolerance, the filter now has one narrow rule:
Extent.IsClearOf skips only when both extents are finite, lie within
1e6 and are more than 0.001 apart on some axis. 0.001 is ten times the
widest absolute band of any native contact query (the 0.00001 box
allowance and the 0.0001 contact reach of tiny arcs), and within 1e6
rounding stays far below it. Larger geometry is always checked in
full, as before the filter. Planning speed is unchanged (144-part grid
about 0.5-0.8 s).

All three reproductions are regression tests, with a test of the rule
itself.
This commit is contained in:
aj committed 2026-10-06 00:20:41 -04:00
1 parent 1c0320d17e
commit 8c21c62ec3
5 files changed
+109 -37

No files matched your search

@@ -13,13 +13,6 @@ public sealed record LeadPathValidationResult(bool IsComplete, bool IsClear, str
/// <summary>Certifies actual emitted native lead paths against owned nominal material.</summary>
public static class LeadPathValidator
{
/// <summary>
/// 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.
/// </summary>
internal const double ClearanceMargin = 1e-6;
public static LeadPathValidationResult Check(OwnedExecution execution, LeadMaterialSnapshot target,
IReadOnlyList<LeadMaterialSnapshot> 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.");
@@ -10,12 +10,6 @@ namespace OpenNest.CNC.CuttingPlanning;
/// <summary>Direct XY completed-contour checker. This is not physical machine safety.</summary>
public sealed class ReleasedContourState
{
/// <summary>
/// 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.
/// </summary>
internal const double ClearanceMargin = 1e-6;
private readonly List<Obstacle> 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,
@@ -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));
/// <summary>The largest absolute coordinate, for scaling tolerances; NaN when any bound is.</summary>
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)));
/// <summary>
/// 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).
/// </summary>
internal const double ClearMargin = 1e-3;
/// <summary>
/// True only when the extents are farther apart than <paramref name="margin"/> 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
/// <see cref="ClearMargin"/>; beyond it, where rounding of large supports can exceed any fixed
/// margin, every check runs.
/// </summary>
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);
/// <summary>
/// True only when both extents are finite, lie within <see cref="WellConditionedLimit"/> and
/// are more than <see cref="ClearMargin"/> apart on some axis, so no native query can count
/// anything in one as touching or entering the other. Everything else must be checked.
/// </summary>
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)
@@ -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)
+7 -6
View File
@@ -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.