using System; using System.Collections.Generic; using System.Linq; using System.Threading; using OpenNest.Diagnostics; using OpenNest.Geometry; namespace OpenNest.CNC.CuttingPlanning; /// A complete unsafe result is distinct from an incomplete/unsupported check. public sealed record LeadPathValidationResult(bool IsComplete, bool IsClear, string Reason); /// /// One lead motion of a fixed emission that failed validation. 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. /// internal sealed record FixedLeadFailure(int Motion, LeadPathValidationResult Result, int? Obstacle); /// Certifies actual emitted native lead paths against owned nominal material. public static class LeadPathValidator { public static LeadPathValidationResult Check(OwnedExecution execution, LeadMaterialSnapshot target, IReadOnlyList otherMaterials, CancellationToken token = default) => Check(execution, target, otherMaterials, null, null, token); /// As the public check, remembering other-material results in . internal static LeadPathValidationResult Check(OwnedExecution execution, LeadMaterialSnapshot target, IReadOnlyList otherMaterials, LeadContactMemo memo, CancellationToken token) => Check(execution, target, otherMaterials, null, null, token, memo); /// /// As the public check, but a lead motion that owns (by motion /// index) is part of a fixed emission: its failure is added to /// and checking continues. Any other failure is returned exactly as the public check returns it. /// internal static LeadPathValidationResult Check(OwnedExecution execution, LeadMaterialSnapshot target, IReadOnlyList otherMaterials, Func isFixed, List 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); } } /// /// How a native lead curve contacts or enters complete material, or null when it stays clear. /// A contact at is allowed. Exceeding the budget throws. /// 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; } }