mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-17 18:31:43 -04:00
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.
122 lines
6.1 KiB
C#
122 lines
6.1 KiB
C#
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);
|
|
}
|