fix(cutting): check every lead against all other material again

The second delta review compared the filtered checks with the previous
implementation on 5,400 generated cases. Rapid checks matched in every
case, but 114 lead checks differed: a long lead passing a small circle
or arc well over 0.001 away was reported clear, while the native
line/circle query, through rounding in its squared terms, reports a
contact there. A coordinate limit and a fixed margin cannot bound that
cancellation.

Lead checks therefore examine every other part's material again,
exactly as before the filter; LeadMaterialSnapshot no longer keeps an
extent. Rapid checks, including the pre-post review's, keep skipping
completed contours more than 0.001 clear of the rapid. The 900000-long
lead beside a radius-0.0001 circle is a regression test.

Planning a dense 144-part grid now takes about 19 s again (lead checks
dominate); a new part order is still found where the old search gave
up.
This commit is contained in:
aj committed 2026-10-06 00:47:46 -04:00
1 parent 8c21c62ec3
commit ea1270e1e7
4 files changed
+36 -18

No files matched your search

@@ -14,20 +14,12 @@ public sealed class LeadMaterialSnapshot
{
Rings = rings;
Reason = reason;
var extent = PostVerificationGeometry.Extent.None;
foreach (var ring in rings)
foreach (var curve in ring)
extent = extent.Union(curve.Extent);
Extent = extent;
}
public bool IsComplete => Reason == null;
public string Reason { get; }
internal IReadOnlyList<PostVerificationGeometry.Curve[]> Rings { get; }
/// <summary>A conservative extent of every boundary; material lies within it.</summary>
internal PostVerificationGeometry.Extent Extent { get; }
/// <summary>Capture a stable, clean, rotation-baked program, applying location once.
/// Unsupported or malformed geometry produces an incomplete snapshot; cancellation throws.</summary>
public static LeadMaterialSnapshot Capture(Program cleanProgram, Vector location,
@@ -84,13 +84,10 @@ public static class LeadPathValidator
var failure = CheckMaterial(target, allowed);
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;
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.IsClearOf(material.Extent))
if (ReferenceEquals(material, target))
continue;
if (CheckMaterial(material, null) != null)
return new(true, false, "Lead contacts or enters another placed material.");
@@ -6,8 +6,9 @@ using OpenNest.Geometry;
namespace OpenNest.Tests.CuttingPlanning;
/// <summary>
/// The lead and rapid checks skip material and contours whose extents are well clear of the
/// motion. These cases sit just inside that skip and must still be checked.
/// Rapid checks skip completed contours whose extents are well clear of the motion; lead checks
/// examine every other part's material. These cases sit just inside the skip, or show that lead
/// checks are never filtered, and must keep the native result.
/// </summary>
public class NearbyMaterialCheckTests
{
@@ -80,6 +81,31 @@ public class NearbyMaterialCheckTests
Assert.Contains(findings, f => f.Kind == PostVerificationKind.RapidCrossing && f.OtherPartNumber == 1);
}
[Fact]
public void LeadCheck_ExaminesMaterialWellClearOfTheLead()
{
// A 900000-long lead passes 0.0021 from a radius-0.0001 circle: far more than any skip
// margin, yet rounding in the native line/circle query reports a contact. Lead checks keep
// that native result rather than skipping the circle.
var target = LeadMaterialSnapshot.Capture(LeadPathValidationTests.Rectangle(0, -2, 2, 0.0021), Vector.Zero);
var circle = new Program();
circle.MoveTo(-1 + 0.0001, 0);
circle.Codes.Add(new ArcMove(new Vector(-1 + 0.0001, 0), new Vector(-1, 0), RotationType.CCW));
var far = LeadMaterialSnapshot.Capture(circle, Vector.Zero);
Assert.True(far.IsComplete, far.Reason);
var lead = new Program();
lead.MoveTo(-900000, 0.0021);
lead.Codes.Add(new LinearMove(0, 0.0021) { Layer = LayerType.Leadin });
lead.LineTo(0, -2);
var execution = Read(lead);
Assert.True(LeadPathValidator.Check(execution, target, [target]).IsClear);
var result = LeadPathValidator.Check(execution, target, [target, far]);
Assert.True(result.IsComplete, result.Reason);
Assert.False(result.IsClear);
}
[Fact]
public void RapidJustOutsideASmallCompletedCircle_IsStillACrossing()
{
+7 -4
View File
@@ -101,10 +101,13 @@ motions, never the nominal entry point alone. Existing lead styles are not
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 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
earlier holes in the same part. Future contours are not yet obstacles. Rapid
checks skip completed contours whose extents are more than 0.001 clear of the
rapid, ten times the widest band any native contact query allows beyond an
extent; anything closer, touching included, gets the full native check. Lead
checks examine every other part's material: native line/circle queries can report
rounding contacts for long leads far from small circles, and leads keep those
results. 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