mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-13 01:21:55 -04:00
fix(cutting): keep ray parity consistent across nominal closure seams
Point-in-material used exact Y comparisons, but rings are only nominally closed: capture admits closure seams and PEP-converted or rotated geometry carries sub-micro-inch vertex jogs. When a jog straddled the test ray's Y, the two edges at that shared vertex disagreed which side it was on and the parity flipped, so a point ten inches outside a material read as interior. LeadMaterialSnapshot reported leads as entering a neighbour they could not touch, and the enclosing-perimeter check refused to capture materials whose rings it misclassified. Snap the ray comparison by the admitted seam tolerance so both edges at a shared vertex decide together. Features larger than the tolerance still count, and containment of a point within the tolerance of an edge stays as ambiguous as before. On the 93-plate corpus 83 plates are untouched, one previously refused plate now plans and replays, and 48/93 plates reach Ready. Accepting more leads also changed one plate's first-found route to a longer rapid path; that is search ranking, recorded with the measurements. Also drops an extent-prefilter fast path measured at -6.8% corpus time: it cleared leads whose native contact was reported beyond any fixed bbox margin, which NearbyMaterialCheckTests deliberately pins as a refusal.
This commit is contained in:
1 parent
df94824054
commit
e3bcc77145
2 files changed
+137
-2
No files matched your search
@@ -9,6 +9,10 @@ namespace OpenNest.Diagnostics;
|
||||
internal static class PostVerificationGeometry
|
||||
{
|
||||
internal const double Epsilon = 1e-8;
|
||||
// Matches NominalContourClosure.Tolerance: the largest gap capture admits as a closed ring.
|
||||
// Ray parity snaps vertex Y comparisons within this band so an admitted seam cannot flip an
|
||||
// exterior point to interior (see Curve.CrossesRay).
|
||||
internal const double RaySeamTolerance = 1e-6;
|
||||
private const double TwoPi = 2 * System.Math.PI;
|
||||
|
||||
/// <summary>An axis-aligned extent used only to skip queries that cannot meet.</summary>
|
||||
@@ -385,8 +389,18 @@ internal static class PostVerificationGeometry
|
||||
|
||||
internal bool CrossesRay(Vector point)
|
||||
{
|
||||
// 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
|
||||
// PEP-converted or rotated geometry carries sub-micro-inch vertex jogs, so an exact
|
||||
// comparison can split one vertex across the ray and report far-away exterior points as
|
||||
// interior — which made LeadPathValidator refuse leads that could touch nothing.
|
||||
// Snapping within the admitted seam tolerance keeps the decision consistent on both
|
||||
// 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 > point.Y) != (End.Y > point.Y)
|
||||
return (Start.Y > rayY) != (End.Y > rayY)
|
||||
&& Start.X + (point.Y - Start.Y) * (End.X - Start.X) / (End.Y - Start.Y) > point.X;
|
||||
|
||||
// Split arcs at vertical extrema, giving monotone-Y pieces. Apply the same
|
||||
@@ -407,7 +421,7 @@ internal static class PostVerificationGeometry
|
||||
var b = StartAngle + System.Math.Sign(Sweep) * breaks[i];
|
||||
var ya = i == 1 ? Start.Y : center.Y + Radius * System.Math.Sin(a);
|
||||
var yb = i == breaks.Count - 1 ? End.Y : center.Y + Radius * System.Math.Sin(b);
|
||||
if ((ya > point.Y) == (yb > point.Y))
|
||||
if ((ya > rayY) == (yb > rayY))
|
||||
continue;
|
||||
var dy = point.Y - center.Y;
|
||||
var dx = System.Math.Sqrt(System.Math.Max(0, Radius * Radius - dy * dy));
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.Diagnostics;
|
||||
using OpenNest.Geometry;
|
||||
using Xunit;
|
||||
using Curve = OpenNest.Diagnostics.PostVerificationGeometry.Curve;
|
||||
|
||||
namespace OpenNest.Tests.CuttingPlanning;
|
||||
|
||||
/// <summary>
|
||||
/// Captured material outlines are only nominally closed: capture admits closure slack up to
|
||||
/// NominalContourClosure.Tolerance, and PEP-converted or rotated geometry carries sub-micro-inch
|
||||
/// vertex jogs where two segments meet. The point-in-material ray test must stay consistent across
|
||||
/// such a jog. Without that, a jog lying exactly on the test ray's Y reports far-away exterior
|
||||
/// points as interior, so LeadPathValidator refuses leads that cannot touch anything: plans get
|
||||
/// needlessly expensive routes or give up inside the search budget.
|
||||
/// </summary>
|
||||
public class MaterialContainmentTests
|
||||
{
|
||||
[Fact]
|
||||
public void SubMicroinchJogAtRayHeight_KeepsDistantExteriorPointExterior()
|
||||
{
|
||||
var rings = Ring(
|
||||
Line((21.2, 0.6), (31.2, 0.6)),
|
||||
Line((31.2, 0.6000001), (41.2, 0.6000001)), // admitted seam jog on the ray line
|
||||
Line((41.2, 0.6000001), (41.2, 46.16)),
|
||||
Line((41.2, 46.16), (21.2, 46.16)),
|
||||
Line((21.2, 46.16), (21.2, 0.6)));
|
||||
|
||||
Assert.False(LeadMaterialSnapshot.Inside(new Vector(20.75, 0.6), rings[0], CancellationToken.None));
|
||||
Assert.False(LeadMaterialSnapshot.Inside(new Vector(20.75, 0.6000001), rings[0], CancellationToken.None));
|
||||
Assert.False(LeadMaterialSnapshot.Inside(new Vector(20.75, 0.60000005), rings[0], CancellationToken.None));
|
||||
Assert.True(LeadMaterialSnapshot.Inside(new Vector(30, 20), rings[0], CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClosedWithoutJog_ReportsTheSameAnswersAsTheJoggedCopy()
|
||||
{
|
||||
var clean = Ring(
|
||||
Line((21.2, 0.6), (41.2, 0.6)),
|
||||
Line((41.2, 0.6), (41.2, 46.16)),
|
||||
Line((41.2, 46.16), (21.2, 46.16)),
|
||||
Line((21.2, 46.16), (21.2, 0.6)));
|
||||
var jogged = Ring(
|
||||
Line((21.2, 0.6), (31.2, 0.6)),
|
||||
Line((31.2, 0.6000001), (41.2, 0.6000001)),
|
||||
Line((41.2, 0.6000001), (41.2, 46.16)),
|
||||
Line((41.2, 46.16), (21.2, 46.16)),
|
||||
Line((21.2, 46.16), (21.2, 0.6)));
|
||||
|
||||
foreach (var y in new[] { 0.6, 0.60000005, 0.6000001 })
|
||||
{
|
||||
var expected = LeadMaterialSnapshot.Inside(new Vector(20.75, y), clean[0], CancellationToken.None);
|
||||
Assert.Equal(expected, LeadMaterialSnapshot.Inside(new Vector(20.75, y), jogged[0], CancellationToken.None));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CircleTestedExactlyAtAnExtremum_KeepsExteriorExteriorAndInteriorInterior()
|
||||
{
|
||||
// Full circle r=5 about (10,10) as four quarter arcs. Testing the ray exactly along both
|
||||
// vertical extrema is where the half-open rule and arc splitting have to agree.
|
||||
var center = new Vector(10, 10);
|
||||
var rings = Ring(
|
||||
Curve.Create(new Vector(15, 10), new Vector(10, 15), center, false),
|
||||
Curve.Create(new Vector(10, 15), new Vector(5, 10), center, false),
|
||||
Curve.Create(new Vector(5, 10), new Vector(10, 5), center, false),
|
||||
Curve.Create(new Vector(10, 5), new Vector(15, 10), center, false));
|
||||
|
||||
Assert.False(LeadMaterialSnapshot.Inside(new Vector(-4, 10), rings[0], CancellationToken.None));
|
||||
Assert.False(LeadMaterialSnapshot.Inside(new Vector(-4, 5), rings[0], CancellationToken.None));
|
||||
Assert.False(LeadMaterialSnapshot.Inside(new Vector(-4, 15), rings[0], CancellationToken.None));
|
||||
Assert.True(LeadMaterialSnapshot.Inside(new Vector(10, 10), rings[0], CancellationToken.None));
|
||||
Assert.True(LeadMaterialSnapshot.Inside(new Vector(7.5, 10), rings[0], CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SeamJogBetweenLineAndArc_KeepsDistantExteriorPointExterior()
|
||||
{
|
||||
// Rounded-left-edge material whose semicircular edge meets the bottom line 1e-7 apart:
|
||||
// an admitted closure seam sitting exactly on a datum Y shared with neighbours.
|
||||
var center = new Vector(1, 23.38000005);
|
||||
var rings = Ring(
|
||||
Line((1, 0.6), (41.2, 0.6)),
|
||||
Line((41.2, 0.6), (41.2, 46.16)),
|
||||
Line((41.2, 46.16), (1, 46.16)),
|
||||
Curve.Create(new Vector(1, 46.16), new Vector(1, 0.6000001), center, false));
|
||||
|
||||
Assert.False(LeadMaterialSnapshot.Inside(new Vector(0.5, 0.6), rings[0], CancellationToken.None));
|
||||
Assert.False(LeadMaterialSnapshot.Inside(new Vector(-4, 0.6), rings[0], CancellationToken.None));
|
||||
Assert.False(LeadMaterialSnapshot.Inside(new Vector(-4, 0.6000001), rings[0], CancellationToken.None));
|
||||
Assert.True(LeadMaterialSnapshot.Inside(new Vector(30, 20), rings[0], CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RealConcavityIsNotErasedByTheRayTolerance()
|
||||
{
|
||||
// A 0.01 in deep notch is far above any admitted seam tolerance: it must still count.
|
||||
var rings = Ring(
|
||||
Line((0, 0), (1, 0)),
|
||||
Line((1, 0), (1, 1)),
|
||||
Line((1, 1), (0.5, 1)),
|
||||
Line((0.5, 1), (0.5, 0.99)), // notch floor
|
||||
Line((0.5, 0.99), (0, 0.99)),
|
||||
Line((0, 0.99), (0, 0)));
|
||||
|
||||
// The 0.01 in notch sits above the left part's top edge; the right tower rises through it.
|
||||
Assert.False(LeadMaterialSnapshot.Inside(new Vector(0.25, 0.995), rings[0], CancellationToken.None));
|
||||
Assert.True(LeadMaterialSnapshot.Inside(new Vector(0.75, 0.995), rings[0], CancellationToken.None));
|
||||
Assert.True(LeadMaterialSnapshot.Inside(new Vector(0.25, 0.5), rings[0], CancellationToken.None));
|
||||
Assert.False(LeadMaterialSnapshot.Inside(new Vector(-1, 0.5), rings[0], CancellationToken.None));
|
||||
}
|
||||
|
||||
private static IReadOnlyList<IReadOnlyList<Curve>> Ring(params Curve[] curves) =>
|
||||
new List<IReadOnlyList<Curve>> { new List<Curve>(curves) };
|
||||
|
||||
private static Curve Line((double X, double Y) start, (double X, double Y) end) =>
|
||||
Curve.Create(new Vector(start.X, start.Y), new Vector(end.X, end.Y), null, false);
|
||||
}
|
||||
Reference in new issue
Block a user