mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-06 16:55:47 -05:00
A converted outline whose seam closes within the admitted 1e-6 gap could only be entered at its seam: entered anywhere else, the arc after the seam started off its own circle and every emission was refused as an inconsistent radius. The search then spent its whole budget on that part and the plate never planned. Emission now bridges the gap with a straight move and cuts the arc as drawn; the verifier already admits that slack. An entry on the seam vertex is left as before. Each prefix of a part re-checked its earlier contours' leads against every other part's material. One search now remembers each verdict by the lead curve's exact bits and the material's reference, charging the same native query budget; replay never uses it. On the reported plate the memo alone gives a byte-identical result in 57 s instead of 12 min 47 s; with the seam fix the plate plans Ready in about 17 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
211 lines
12 KiB
C#
211 lines
12 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>
|
|
/// One lead motion of a fixed emission that failed validation. <see cref="Obstacle"/> indexes the
|
|
/// other materials passed to the check, or is null when the lead meets its own part's material
|
|
/// or could not be checked.
|
|
/// </summary>
|
|
internal sealed record FixedLeadFailure(int Motion, LeadPathValidationResult Result, int? Obstacle);
|
|
|
|
/// <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) =>
|
|
Check(execution, target, otherMaterials, null, null, token);
|
|
|
|
/// <summary>As the public check, remembering other-material results in <paramref name="memo"/>.</summary>
|
|
internal static LeadPathValidationResult Check(OwnedExecution execution, LeadMaterialSnapshot target,
|
|
IReadOnlyList<LeadMaterialSnapshot> otherMaterials, LeadContactMemo memo, CancellationToken token) =>
|
|
Check(execution, target, otherMaterials, null, null, token, memo);
|
|
|
|
/// <summary>
|
|
/// As the public check, but a lead motion that <paramref name="isFixed"/> owns (by motion
|
|
/// index) is part of a fixed emission: its failure is added to <paramref name="fixedFailures"/>
|
|
/// and checking continues. Any other failure is returned exactly as the public check returns it.
|
|
/// </summary>
|
|
internal static LeadPathValidationResult Check(OwnedExecution execution, LeadMaterialSnapshot target,
|
|
IReadOnlyList<LeadMaterialSnapshot> otherMaterials, Func<int, bool> isFixed,
|
|
List<FixedLeadFailure> fixedFailures, CancellationToken token, LeadContactMemo memo = null)
|
|
{
|
|
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;
|
|
var (failure, obstacle) = CheckMotion(i);
|
|
if (failure == null)
|
|
continue;
|
|
if (isFixed?.Invoke(i) != true || fixedFailures == null)
|
|
return failure;
|
|
fixedFailures.Add(new(i, failure, obstacle));
|
|
}
|
|
// No lead motion is valid for direct contour piercing. The caller still
|
|
// checks rapid travel and contour accounting for the complete program.
|
|
return new(true, true, null);
|
|
|
|
(LeadPathValidationResult Failure, int? Obstacle) CheckMotion(int i)
|
|
{
|
|
var move = execution.Motions[i];
|
|
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."), null);
|
|
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;
|
|
var breakLead = false;
|
|
if (adjacentIndex >= 0 && adjacentIndex < execution.Motions.Count)
|
|
{
|
|
var adjacent = execution.Motions[adjacentIndex];
|
|
// A break cut lies inside its part's material, so its lead joins no boundary:
|
|
// the joint is the cut's own start (or end for a lead-out).
|
|
if (!adjacent.Rapid && adjacent.Layer == LayerType.Break
|
|
&& adjacent.Curve != null && adjacent.Length > PostVerificationGeometry.Epsilon)
|
|
{
|
|
var edge = execution.Motions[groupEdge];
|
|
var joint = move.Layer == LayerType.Leadin ? edge.End : edge.Curve.Start;
|
|
var cutJoint = move.Layer == LayerType.Leadin ? adjacent.Curve.Start : adjacent.End;
|
|
if (joint.DistanceTo(cutJoint) <= PostVerificationGeometry.Epsilon)
|
|
{
|
|
genuineJoint = true;
|
|
breakLead = true;
|
|
if (groupEdge == i)
|
|
allowed = joint;
|
|
}
|
|
}
|
|
else 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."), null);
|
|
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."), null);
|
|
// A break cut's lead runs through its own breakoff scrap: entering the target's
|
|
// material is expected there, but touching any of its boundaries is not.
|
|
var failure = breakLead
|
|
? MaterialContact(move.Curve, target, null, ref budget, token, allowInterior: true)
|
|
: CheckMaterial(target, allowed);
|
|
if (failure != null)
|
|
return (new(true, false, breakLead
|
|
? $"Break cut lead motion {i} contacts its part's boundary ({failure}). Try a shorter lead-in or move the tabs."
|
|
: $"Lead motion {i} contacts or enters target material outside its adjacent contour joint ({failure})."), null);
|
|
for (var other = 0; other < otherMaterials.Count; other++)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
var material = otherMaterials[other];
|
|
if (ReferenceEquals(material, target))
|
|
continue;
|
|
if (OtherContact(material))
|
|
return (new(true, false, "Lead contacts or enters another placed material. "
|
|
+ "Try spacing the parts farther apart or reducing the lead-in/lead-out length, then replan. "
|
|
+ "For locked parts, edit the leads or unlock the part before replanning."), other);
|
|
}
|
|
return (null, null);
|
|
|
|
string CheckMaterial(LeadMaterialSnapshot material, Vector? permittedJoint) =>
|
|
MaterialContact(move.Curve, material, permittedJoint, ref budget, token);
|
|
|
|
// Exactly the unmemoized check: a remembered scan charges the queries it ran
|
|
// and throws where running it again would exhaust the budget.
|
|
bool OtherContact(LeadMaterialSnapshot material)
|
|
{
|
|
if (memo == null)
|
|
return CheckMaterial(material, null) != null;
|
|
if (memo.TryGet(move.Curve, material, out var scan))
|
|
{
|
|
if (budget - scan.Queries < 0)
|
|
throw new NotSupportedException("Lead validation exceeds the native query limit.");
|
|
budget -= scan.Queries;
|
|
return scan.Contact;
|
|
}
|
|
var before = budget;
|
|
var contact = CheckMaterial(material, null) != null;
|
|
memo.Add(move.Curve, material, new(contact, before - budget));
|
|
return contact;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex) when (ex is ArgumentException or NotSupportedException)
|
|
{
|
|
return new(false, false, ex.Message);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// How a native lead curve contacts or enters complete material, or null when it stays clear.
|
|
/// A contact at <paramref name="permittedJoint"/> is allowed. Exceeding the budget throws.
|
|
/// </summary>
|
|
internal static string MaterialContact(PostVerificationGeometry.Curve curve, LeadMaterialSnapshot material,
|
|
Vector? permittedJoint, ref int budget, CancellationToken token, bool allowInterior = false)
|
|
{
|
|
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 = 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 !allowInterior && material.ContainsMaterial(curve.Midpoint, token) ? "interior" : null;
|
|
}
|
|
}
|