mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-10 09:54:13 -04:00
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.
126 lines
7.1 KiB
C#
126 lines
7.1 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using OpenNest.Diagnostics;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.CNC.CuttingPlanning;
|
|
|
|
/// <summary>A complete unsafe result is distinct from an incomplete/unsupported check.</summary>
|
|
public sealed record LeadPathValidationResult(bool IsComplete, bool IsClear, string Reason);
|
|
|
|
/// <summary>Certifies actual emitted native lead paths against owned nominal material.</summary>
|
|
public static class LeadPathValidator
|
|
{
|
|
public static LeadPathValidationResult Check(OwnedExecution execution, LeadMaterialSnapshot target,
|
|
IReadOnlyList<LeadMaterialSnapshot> otherMaterials, CancellationToken token = default)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
if (execution == null || target == null || otherMaterials == null)
|
|
return new(false, false, "Missing execution or material snapshots.");
|
|
if (!target.IsComplete || otherMaterials.Any(m => m == null || !m.IsComplete))
|
|
return new(false, false, "Material snapshot is incomplete.");
|
|
try
|
|
{
|
|
var budget = 1000000;
|
|
for (var i = 0; i < execution.Motions.Count; i++)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
var move = execution.Motions[i];
|
|
if (move.Layer is not (LayerType.Leadin or LayerType.Leadout))
|
|
continue;
|
|
if (move.Rapid || move.Start is not { } start || move.Curve == null
|
|
|| !double.IsFinite(move.Length) || move.Length <= PostVerificationGeometry.Epsilon
|
|
|| start.DistanceTo(move.Curve.Start) > PostVerificationGeometry.Epsilon
|
|
|| move.End.DistanceTo(move.Curve.End) > PostVerificationGeometry.Epsilon)
|
|
return new(false, false, "Lead motion is missing, degenerate or inconsistent.");
|
|
Vector? allowed = null;
|
|
var groupEdge = i;
|
|
var step = move.Layer == LayerType.Leadin ? 1 : -1;
|
|
while (groupEdge + step >= 0 && groupEdge + step < execution.Motions.Count
|
|
&& execution.Motions[groupEdge + step].Layer == move.Layer)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
if (--budget < 0)
|
|
throw new NotSupportedException("Lead validation exceeds the native query limit.");
|
|
groupEdge += step;
|
|
}
|
|
var adjacentIndex = groupEdge + step;
|
|
var genuineJoint = false;
|
|
if (adjacentIndex >= 0 && adjacentIndex < execution.Motions.Count)
|
|
{
|
|
var adjacent = execution.Motions[adjacentIndex];
|
|
if (!adjacent.Rapid && adjacent.Layer is LayerType.Cut or LayerType.Display
|
|
&& adjacent.Curve != null && adjacent.Length > PostVerificationGeometry.Epsilon)
|
|
{
|
|
var edge = execution.Motions[groupEdge];
|
|
var joint = move.Layer == LayerType.Leadin ? edge.End : edge.Curve.Start;
|
|
var contourJoint = move.Layer == LayerType.Leadin ? adjacent.Curve.Start : adjacent.End;
|
|
foreach (var boundary in target.Rings.SelectMany(r => r))
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
if (--budget < 0)
|
|
throw new NotSupportedException("Lead validation exceeds the native query limit.");
|
|
if (joint.DistanceTo(contourJoint) <= PostVerificationGeometry.Epsilon
|
|
&& boundary.SameSupport(adjacent.Curve)
|
|
&& boundary.Contains(adjacent.Curve.Start) && boundary.Contains(adjacent.End)
|
|
&& boundary.Contains(adjacent.Curve.Midpoint)
|
|
&& adjacent.Length <= boundary.Length + PostVerificationGeometry.Epsilon)
|
|
{
|
|
genuineJoint = true;
|
|
if (groupEdge == i)
|
|
allowed = joint;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!genuineJoint)
|
|
return new(true, false, "Lead chain has no genuine adjacent target contour entry or exit.");
|
|
if (allowed is { } jointPoint
|
|
&& (move.Layer == LayerType.Leadin ? start : move.End).DistanceTo(jointPoint) <= PostVerificationGeometry.Epsilon)
|
|
return new(true, false, "A positive-length lead returns to its contour joint; contact is not endpoint-only.");
|
|
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))
|
|
continue;
|
|
if (CheckMaterial(material, null) != null)
|
|
return new(true, false, "Lead contacts or enters another placed material.");
|
|
}
|
|
|
|
string CheckMaterial(LeadMaterialSnapshot material, Vector? permittedJoint)
|
|
{
|
|
foreach (var boundary in material.Rings.SelectMany(r => r))
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
if (--budget < 0)
|
|
throw new NotSupportedException("Lead validation exceeds the native query limit.");
|
|
var contacts = move.Curve.Contacts(boundary, out var overlap);
|
|
if (overlap || contacts.Any(p => permittedJoint is not { } joint
|
|
|| p.DistanceTo(joint) > PostVerificationGeometry.Epsilon))
|
|
return $"boundary; allowed={permittedJoint}; contacts={string.Join(";", contacts)}; overlap={overlap}";
|
|
}
|
|
// With all other boundary contacts excluded, the connected open path
|
|
// has constant material membership. Use the native arc midpoint, not
|
|
// the chord midpoint or endpoints (which can both lie in scrap).
|
|
return material.ContainsMaterial(move.Curve.Midpoint, token) ? "interior" : null;
|
|
}
|
|
}
|
|
// Missing leads are the ReleasedContourState check's responsibility.
|
|
return new(true, true, null);
|
|
}
|
|
catch (Exception ex) when (ex is ArgumentException or NotSupportedException)
|
|
{
|
|
return new(false, false, ex.Message);
|
|
}
|
|
}
|
|
}
|