diff --git a/OpenNest.Core/CNC/CuttingPlanning/LeadMaterialSnapshot.cs b/OpenNest.Core/CNC/CuttingPlanning/LeadMaterialSnapshot.cs index 50c1d1a..7d930e8 100644 --- a/OpenNest.Core/CNC/CuttingPlanning/LeadMaterialSnapshot.cs +++ b/OpenNest.Core/CNC/CuttingPlanning/LeadMaterialSnapshot.cs @@ -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 Rings { get; } - /// A conservative extent of every boundary; material lies within it. - internal PostVerificationGeometry.Extent Extent { get; } - /// Capture a stable, clean, rotation-baked program, applying location once. /// Unsupported or malformed geometry produces an incomplete snapshot; cancellation throws. public static LeadMaterialSnapshot Capture(Program cleanProgram, Vector location, diff --git a/OpenNest.Core/CNC/CuttingPlanning/LeadPathValidator.cs b/OpenNest.Core/CNC/CuttingPlanning/LeadPathValidator.cs index dde1eea..c354417 100644 --- a/OpenNest.Core/CNC/CuttingPlanning/LeadPathValidator.cs +++ b/OpenNest.Core/CNC/CuttingPlanning/LeadPathValidator.cs @@ -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."); diff --git a/OpenNest.Tests/CuttingPlanning/NearbyMaterialCheckTests.cs b/OpenNest.Tests/CuttingPlanning/NearbyMaterialCheckTests.cs index 9a2e7a9..5eb2a50 100644 --- a/OpenNest.Tests/CuttingPlanning/NearbyMaterialCheckTests.cs +++ b/OpenNest.Tests/CuttingPlanning/NearbyMaterialCheckTests.cs @@ -6,8 +6,9 @@ using OpenNest.Geometry; namespace OpenNest.Tests.CuttingPlanning; /// -/// 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. /// 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() { diff --git a/docs/cutting-planner.md b/docs/cutting-planner.md index 3f7fda4..fffed57 100644 --- a/docs/cutting-planner.md +++ b/docs/cutting-planner.md @@ -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