mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-08 06:12:33 -04:00
fix(cutting): skip checks only for well-clear, well-conditioned extents
The delta review found two more ways the extent filter could skip a check a native query would have flagged: - Native line intersections accept points 0.00001 outside each line's bounding box, so a lead 0.000003 from another part touched it while their extents were 0.000003 apart, beyond the 1e-6 margin. - Around a circle of radius 5e11, rounding let the native query count a rapid at x = -0.00001 as touching although the circle's extent started at x = 0; the margin scaled only with the rapid's own size. Rather than chase each tolerance, the filter now has one narrow rule: Extent.IsClearOf skips only when both extents are finite, lie within 1e6 and are more than 0.001 apart on some axis. 0.001 is ten times the widest absolute band of any native contact query (the 0.00001 box allowance and the 0.0001 contact reach of tiny arcs), and within 1e6 rounding stays far below it. Larger geometry is always checked in full, as before the filter. Planning speed is unchanged (144-part grid about 0.5-0.8 s). All three reproductions are regression tests, with a test of the rule itself.
This commit is contained in:
1 parent
1c0320d17e
commit
8c21c62ec3
5 files changed
+109
-37
No files matched your search
@@ -13,13 +13,6 @@ public sealed record LeadPathValidationResult(bool IsComplete, bool IsClear, str
|
||||
/// <summary>Certifies actual emitted native lead paths against owned nominal material.</summary>
|
||||
public static class LeadPathValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Relative separation beyond which another material is skipped. Scaled by coordinate size,
|
||||
/// it stays far above both the contact tolerance and the rounding of the extents themselves,
|
||||
/// so a lead touching a boundary within tolerance is always checked.
|
||||
/// </summary>
|
||||
internal const double ClearanceMargin = 1e-6;
|
||||
|
||||
public static LeadPathValidationResult Check(OwnedExecution execution, LeadMaterialSnapshot target,
|
||||
IReadOnlyList<LeadMaterialSnapshot> otherMaterials, CancellationToken token = default)
|
||||
{
|
||||
@@ -92,13 +85,12 @@ public static class LeadPathValidator
|
||||
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;
|
||||
var margin = ClearanceMargin * (1 + reach.Magnitude);
|
||||
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.IsSeparatedFrom(material.Extent, margin))
|
||||
if (ReferenceEquals(material, target) || reach.IsClearOf(material.Extent))
|
||||
continue;
|
||||
if (CheckMaterial(material, null) != null)
|
||||
return new(true, false, "Lead contacts or enters another placed material.");
|
||||
|
||||
@@ -10,12 +10,6 @@ namespace OpenNest.CNC.CuttingPlanning;
|
||||
/// <summary>Direct XY completed-contour checker. This is not physical machine safety.</summary>
|
||||
public sealed class ReleasedContourState
|
||||
{
|
||||
/// <summary>
|
||||
/// Relative separation beyond which a completed contour is skipped. Scaled by coordinate size,
|
||||
/// it stays far above the contact tolerance and the rounding of the extents themselves.
|
||||
/// </summary>
|
||||
internal const double ClearanceMargin = 1e-6;
|
||||
|
||||
private readonly List<Obstacle> obstacles = new();
|
||||
|
||||
public ReleasedContourState Copy()
|
||||
@@ -65,12 +59,11 @@ public sealed class ReleasedContourState
|
||||
var reach = new PostVerificationGeometry.Extent(System.Math.Min(start.X, move.End.X),
|
||||
System.Math.Min(start.Y, move.End.Y), System.Math.Max(start.X, move.End.X),
|
||||
System.Math.Max(start.Y, move.End.Y));
|
||||
var margin = ClearanceMargin * (1 + reach.Magnitude);
|
||||
foreach (var obstacle in obstacles)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
// A rapid well clear of a contour's extent can neither cross nor touch it.
|
||||
if (reach.IsSeparatedFrom(obstacle.Extent, margin))
|
||||
if (reach.IsClearOf(obstacle.Extent))
|
||||
continue;
|
||||
if (PostVerificationGeometry.Crosses(start, move.End, obstacle.Curves, token))
|
||||
findings.Add(new(PostVerificationKind.RapidCrossing, plate, part, obstacle.Part,
|
||||
|
||||
@@ -21,22 +21,34 @@ internal static class PostVerificationGeometry
|
||||
internal Extent Union(Extent other) => new(System.Math.Min(MinX, other.MinX),
|
||||
System.Math.Min(MinY, other.MinY), System.Math.Max(MaxX, other.MaxX), System.Math.Max(MaxY, other.MaxY));
|
||||
|
||||
/// <summary>The largest absolute coordinate, for scaling tolerances; NaN when any bound is.</summary>
|
||||
internal double Magnitude => System.Math.Max(System.Math.Max(System.Math.Abs(MinX), System.Math.Abs(MaxX)),
|
||||
System.Math.Max(System.Math.Abs(MinY), System.Math.Abs(MaxY)));
|
||||
/// <summary>
|
||||
/// The gap two extents need before their checks may be skipped: ten times the widest
|
||||
/// absolute band any native contact query allows beyond an extent (the 0.00001 bounding-box
|
||||
/// allowance of native intersections, the 0.0001 contact reach of tiny arcs).
|
||||
/// </summary>
|
||||
internal const double ClearMargin = 1e-3;
|
||||
|
||||
/// <summary>
|
||||
/// True only when the extents are farther apart than <paramref name="margin"/> on some axis,
|
||||
/// so nothing inside one can touch or enter the other. An extent with any nonfinite bound
|
||||
/// (including the empty one) is never separated.
|
||||
/// Coordinates up to which extents may be skipped at all. Within it rounding stays far below
|
||||
/// <see cref="ClearMargin"/>; beyond it, where rounding of large supports can exceed any fixed
|
||||
/// margin, every check runs.
|
||||
/// </summary>
|
||||
internal bool IsSeparatedFrom(Extent other, double margin) => IsFinite && other.IsFinite
|
||||
&& double.IsFinite(margin)
|
||||
&& (MaxX + margin < other.MinX || other.MaxX + margin < MinX
|
||||
|| MaxY + margin < other.MinY || other.MaxY + margin < MinY);
|
||||
internal const double WellConditionedLimit = 1e6;
|
||||
|
||||
private bool IsFinite => double.IsFinite(MinX) && double.IsFinite(MinY)
|
||||
&& double.IsFinite(MaxX) && double.IsFinite(MaxY);
|
||||
/// <summary>
|
||||
/// True only when both extents are finite, lie within <see cref="WellConditionedLimit"/> and
|
||||
/// are more than <see cref="ClearMargin"/> apart on some axis, so no native query can count
|
||||
/// anything in one as touching or entering the other. Everything else must be checked.
|
||||
/// </summary>
|
||||
internal bool IsClearOf(Extent other) => IsWellConditioned && other.IsWellConditioned
|
||||
&& (MaxX + ClearMargin < other.MinX || other.MaxX + ClearMargin < MinX
|
||||
|| MaxY + ClearMargin < other.MinY || other.MaxY + ClearMargin < MinY);
|
||||
|
||||
// Math.Max propagates NaN and no comparison with NaN holds, so NaN and infinite bounds
|
||||
// (including the empty extent's) fail this test too.
|
||||
private bool IsWellConditioned =>
|
||||
System.Math.Max(System.Math.Max(System.Math.Abs(MinX), System.Math.Abs(MaxX)),
|
||||
System.Math.Max(System.Math.Abs(MinY), System.Math.Abs(MaxY))) <= WellConditionedLimit;
|
||||
}
|
||||
|
||||
internal static void Validate(Vector point)
|
||||
|
||||
@@ -145,14 +145,88 @@ public class NearbyMaterialCheckTests
|
||||
Assert.Contains("uncertain", result.Reason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LeadEndingWithinTheNativeBoxAllowanceOfAnotherPart_IsRefused()
|
||||
{
|
||||
// The other part lies 0.000003 above the lead: outside both extents, but within the
|
||||
// 0.00001 allowance the native line intersection grants its bounding boxes.
|
||||
var target = LeadMaterialSnapshot.Capture(LeadPathValidationTests.Rectangle(0, -2, 2, 0), Vector.Zero);
|
||||
var other = LeadMaterialSnapshot.Capture(LeadPathValidationTests.Rectangle(-1, 0.000003, -0.25, 1), Vector.Zero);
|
||||
Assert.True(other.IsComplete, other.Reason);
|
||||
var lead = new Program();
|
||||
lead.MoveTo(-0.5, 0);
|
||||
lead.Codes.Add(new LinearMove(0, 0) { Layer = LayerType.Leadin });
|
||||
lead.LineTo(0, -2);
|
||||
|
||||
var result = LeadPathValidator.Check(Read(lead), target, [target, other]);
|
||||
|
||||
Assert.True(result.IsComplete, result.Reason);
|
||||
Assert.False(result.IsClear);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RapidBesideAHugeCompletedCircle_IsStillACrossing()
|
||||
{
|
||||
// Radius 5e11 about (5e11, 0): its extent starts at x = 0, but at this size rounding lets
|
||||
// the native query count a rapid at x = -0.00001 as touching it.
|
||||
var state = new ReleasedContourState();
|
||||
state.Check(Read(HugeCircle()), null, 1);
|
||||
var rapid = new Program();
|
||||
rapid.MoveTo(-1e-5, 1);
|
||||
|
||||
var findings = state.Check(Read(rapid), new Vector(-1e-5, -1), 2);
|
||||
|
||||
Assert.Contains(findings, f => f.Kind == PostVerificationKind.RapidCrossing && f.OtherPartNumber == 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LeadBesideAHugeCircle_IsRefused()
|
||||
{
|
||||
var huge = LeadMaterialSnapshot.Capture(HugeCircle(), Vector.Zero);
|
||||
Assert.True(huge.IsComplete, huge.Reason);
|
||||
var target = LeadMaterialSnapshot.Capture(LeadPathValidationTests.Rectangle(-2, -1, -0.002, -0.5), Vector.Zero);
|
||||
var lead = new Program();
|
||||
lead.MoveTo(-1e-5, 1);
|
||||
lead.Codes.Add(new LinearMove(-1e-5, -0.5) { Layer = LayerType.Leadin });
|
||||
lead.Codes.Add(new LinearMove(-0.002, -0.5) { Layer = LayerType.Leadin });
|
||||
lead.LineTo(-2, -0.5);
|
||||
|
||||
var result = LeadPathValidator.Check(Read(lead), target, [target, huge]);
|
||||
|
||||
Assert.True(result.IsComplete, result.Reason);
|
||||
Assert.False(result.IsClear);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(2.002, 0, true)] // 0.002 apart: clear of the 0.001 margin.
|
||||
[InlineData(2.0005, 0, false)] // 0.0005 apart: inside the margin.
|
||||
[InlineData(2.002, 2e6, false)] // Clear, but beyond the well-conditioned range: always checked.
|
||||
public void IsClearOf_OnlyWellApartAndWellConditionedExtentsAreSkipped(double left, double offset, bool clear)
|
||||
{
|
||||
var a = new PostVerificationGeometry.Extent(offset, offset, offset + 2, offset + 2);
|
||||
var b = new PostVerificationGeometry.Extent(offset + left, offset, offset + left + 1, offset + 1);
|
||||
|
||||
Assert.Equal(clear, a.IsClearOf(b));
|
||||
Assert.Equal(clear, b.IsClearOf(a));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtentWithANaNBound_IsNeverSeparated()
|
||||
{
|
||||
var partial = new PostVerificationGeometry.Extent(double.NaN, 0, double.NaN, 1);
|
||||
var other = new PostVerificationGeometry.Extent(0, 4, 1, 5);
|
||||
|
||||
Assert.False(partial.IsSeparatedFrom(other, 1e-6));
|
||||
Assert.False(other.IsSeparatedFrom(partial, 1e-6));
|
||||
Assert.False(partial.IsClearOf(other));
|
||||
Assert.False(other.IsClearOf(partial));
|
||||
Assert.False(PostVerificationGeometry.Extent.None.IsClearOf(other)); // Infinite bounds.
|
||||
}
|
||||
|
||||
private static Program HugeCircle()
|
||||
{
|
||||
var p = new Program();
|
||||
p.MoveTo(0, 0);
|
||||
p.Codes.Add(new ArcMove(Vector.Zero, new Vector(5e11, 0), RotationType.CCW));
|
||||
return p;
|
||||
}
|
||||
|
||||
private static Program CleanCircle(double radius)
|
||||
|
||||
@@ -102,12 +102,13 @@ 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 1e-6 x
|
||||
(1 + coordinate size) clear of the motion; anything closer, touching included, gets
|
||||
the full native check. 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 1e-4 beyond its radius), and an extent with any nonfinite
|
||||
bound is never skipped. Actual
|
||||
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
|
||||
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
|
||||
beyond 1e6, where rounding of large supports can exceed any fixed margin. Actual
|
||||
lead-in and lead-out line/arc paths must stay in target scrap and avoid other
|
||||
placed material; holes in other parts remain scrap. Tangent/coincident contacts
|
||||
outside the genuine target contour joint and numerically uncertain queries refuse.
|
||||
|
||||
Reference in new issue
Block a user