From d3d7bc0299ae1a0e2156eee3fda2284c5cdf0e20 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 26 Oct 2026 10:36:00 -0400 Subject: [PATCH] fix(planning): cast containment rays clear of every vertex height Text cutouts on an ID tag sat a micro-inch below another letter's baseline. The parity ray, offset a fixed 1e-6 above the test point, ran exactly through that letter's seam vertex, whose two copies differ in the last bit, so one edge counted and the tag captured as "Nested cutouts are not supported material" at some plate locations but not others. The ray height now steps until no vertex (or arc top/bottom) lies within a small band of it. Co-Authored-By: Claude Opus 5.5 --- .../CuttingPlanning/LeadMaterialSnapshot.cs | 13 +--- .../Diagnostics/PostVerificationGeometry.cs | 59 +++++++++++++++++-- 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/OpenNest.Core/CNC/CuttingPlanning/LeadMaterialSnapshot.cs b/OpenNest.Core/CNC/CuttingPlanning/LeadMaterialSnapshot.cs index d1093f82..56640d25 100644 --- a/OpenNest.Core/CNC/CuttingPlanning/LeadMaterialSnapshot.cs +++ b/OpenNest.Core/CNC/CuttingPlanning/LeadMaterialSnapshot.cs @@ -353,15 +353,6 @@ public sealed class LeadMaterialSnapshot private int Row(double y) => (int)System.Math.Clamp(System.Math.Floor((y - whole.MinY) / height), 0, rows - 1); } - internal static bool Inside(Vector point, IReadOnlyList ring, CancellationToken token) - { - var inside = false; - foreach (var curve in ring) - { - token.ThrowIfCancellationRequested(); - if (curve.CrossesRay(point)) - inside = !inside; - } - return inside; - } + internal static bool Inside(Vector point, IReadOnlyList ring, CancellationToken token) => + PostVerificationGeometry.Inside(point, ring, token); } diff --git a/OpenNest.Core/Diagnostics/PostVerificationGeometry.cs b/OpenNest.Core/Diagnostics/PostVerificationGeometry.cs index cb31ab04..545edc90 100644 --- a/OpenNest.Core/Diagnostics/PostVerificationGeometry.cs +++ b/OpenNest.Core/Diagnostics/PostVerificationGeometry.cs @@ -110,17 +110,56 @@ internal static class PostVerificationGeometry // If there are no contacts except possibly departure, all open-segment points // have the same inside/outside status. A midpoint catches travel entirely inside // and departure into the interior, without flagging start-only outward contact. - var midpoint = start + delta * 0.5; + return Inside(start + delta * 0.5, curves, token); + } + + /// + /// Ray parity of against closed , cast at + /// so no vertex of them lies on the ray. + /// + internal static bool Inside(Vector point, IReadOnlyList curves, CancellationToken token) + { + var rayY = RayY(point, curves, token); var inside = false; foreach (var curve in curves) { token.ThrowIfCancellationRequested(); - if (curve.CrossesRay(midpoint)) + if (curve.CrossesRay(point, rayY)) inside = !inside; } return inside; } + /// + /// The height a parity ray from runs at: just above it, by about + /// , and clear of every vertex height of . + /// The two copies of a seam vertex can differ in the last bits; a ray running between them + /// counts one edge and not the other, so a point that happens to sit a tolerance below another + /// contour's vertex (text baselines a micro-inch apart) would read as inside it. + /// + internal static double RayY(Vector point, IReadOnlyList curves, CancellationToken token) + { + const int steps = 8; + const double band = RaySeamTolerance / (4 * steps); + for (var step = 0; step < steps; step++) + { + var rayY = point.Y + RaySeamTolerance * (1 + (double)step / steps); + var clear = true; + foreach (var curve in curves) + { + token.ThrowIfCancellationRequested(); + if (curve.HasVertexNear(rayY, band)) + { + clear = false; + break; + } + } + if (clear) + return rayY; + } + return point.Y + RaySeamTolerance; + } + internal static IReadOnlyList ContactPoints(Vector start, Vector end, IReadOnlyList curves, CancellationToken token) { @@ -467,7 +506,20 @@ internal static class PostVerificationGeometry } } - internal bool CrossesRay(Vector point) + /// + /// Whether an endpoint, or the top or bottom of an arc's circle, lies within + /// of height . + /// + internal bool HasVertexNear(double y, double band) => + System.Math.Abs(Start.Y - y) <= band || System.Math.Abs(End.Y - y) <= band + || Center is { } center && (System.Math.Abs(center.Y + Radius - y) <= band + || System.Math.Abs(center.Y - Radius - y) <= band); + + /// + /// Whether a ray running in +X from , at height + /// (see ), crosses this curve. + /// + internal bool CrossesRay(Vector point, double rayY) { // Ray parity needs both edges meeting at a shared vertex to agree which side of the ray // that vertex is on. Rings are only nominally closed: capture admits closure seams and @@ -478,7 +530,6 @@ internal static class PostVerificationGeometry // edges; real features (notches, tabs, arc spans) are far larger than the tolerance and // still count. Containment of a point that itself lies within the tolerance of a // boundary edge stays ambiguous by design, exactly as it was before. - var rayY = point.Y + RaySeamTolerance; if (Center is not { } center) return (Start.Y > rayY) != (End.Y > rayY) && Start.X + (point.Y - Start.Y) * (End.X - Start.X) / (End.Y - Start.Y) > point.X;