using System; using System.Collections.Generic; using System.Linq; using System.Threading; using OpenNest.CNC; using OpenNest.CNC.CuttingPlanning; using OpenNest.Geometry; namespace OpenNest.Diagnostics; /// /// Pre-post review of a plate's common-line groups (): every member /// still shares a span one kerf from another member, the group's kerf is still the plate's, and no /// member's lead or shared cut runs into a partner one kerf away (a lead on a shared edge would cut /// the partner). A plate that cuts its common lines once must cut each shared span exactly once, /// on its centerline, by the member cut first; one set to cut them twice must have no shared cuts. /// Reports only; nothing is repaired, and an unreadable lead check is incomplete, never clear. /// internal static class CommonLineReview { internal static void Add(Plate plate, int plateNumber, List findings, CancellationToken token) { if (plate.CommonLines is not { Count: > 0 } groups) return; var plateKerf = plate.CuttingParameters?.Kerf is double k && double.IsFinite(k) && k > 0 ? k : (double?)null; foreach (var group in groups) { token.ThrowIfCancellationRequested(); var first = Number(plate, group.Members[0]); if (plateKerf is { } kerf && System.Math.Abs(kerf - group.Kerf) > CommonLineGeometry.GapTolerance) findings.Add(new(PostVerificationKind.CommonLine, plateNumber, first, null, $"Common line was placed for a {group.Kerf:0.####} kerf, but the plate now cuts with a " + $"{kerf:0.####} kerf. Rejoin the parts at the current kerf before posting.")); var check = CommonLineGeometry.Check(group); foreach (var problem in check.Problems) findings.Add(new(PostVerificationKind.CommonLine, plateNumber, Number(plate, problem.Part), null, $"'{problem.Part.BaseDrawing.Name}' {problem.Message}") { Location = problem.Part.BoundingBox?.Center }); Leads(plate, plateNumber, group, findings, token); if (check.IsIntact) SharedCuts(plate, plateNumber, group, check, findings, token); } } // The shared cuts the members' programs make, against the spans the group derives now. private static void SharedCuts(Plate plate, int plateNumber, CommonLineGroup group, CommonLineCheck check, List findings, CancellationToken token) { var cuts = new List<(Part Part, ExecutionMotion Motion)>(); foreach (var member in group.Members) { token.ThrowIfCancellationRequested(); try { cuts.AddRange(ExecutionMotionReader.Read(member.Program, member.Location, null, token).Motions .Where(motion => !motion.Rapid && motion.Layer == LayerType.CommonLine && motion.Curve != null) .Select(motion => (member, motion))); } catch (Exception exception) when (IsInvalid(exception)) { // The rapid check reports the unreadable program. } } if (plate.CutCommonLinesTwice) { if (cuts.Count > 0) findings.Add(new(PostVerificationKind.CommonLine, plateNumber, Number(plate, cuts[0].Part), null, "The plate is set to cut its common lines twice, but this part's program cuts a shared line " + "once. Plan cutting again before posting.") { Location = cuts[0].Motion.Curve.Midpoint }); return; } foreach (var span in check.Spans) { token.ThrowIfCancellationRequested(); var covering = cuts.Where(cut => Covers(cut.Motion, span)).ToList(); var first = Number(plate, span.First); var second = Number(plate, span.Second); var middle = (span.Start + span.End) * 0.5; if (covering.Count == 0) { findings.Add(new(PostVerificationKind.CommonLine, plateNumber, first, second, "This common line is not cut once on its centerline. Plan cutting cuts each shared line once; " + "or set the plate to cut its common lines twice.") { Location = middle }); continue; } if (covering.Count > 1) findings.Add(new(PostVerificationKind.CommonLine, plateNumber, first, second, "This common line is cut more than once. Plan cutting again before posting.") { Location = middle }); // The partner's remainder ends in this kerf, so the part that cuts it must come first. var owner = covering[0].Part; var partner = ReferenceEquals(owner, span.First) ? span.Second : span.First; if (Number(plate, owner) is int o && Number(plate, partner) is int p && o > p) findings.Add(new(PostVerificationKind.CommonLine, plateNumber, o, p, "The shared line is cut after the partner whose remainder ends in its kerf. Plan cutting " + "again, or move the part that cuts it ahead of its partner.") { Location = middle }); } } // A shared cut on the span's centerline that covers the whole span. private static bool Covers(ExecutionMotion motion, CommonLineSpan span) { var curve = motion.Curve; if (curve.IsFiniteArc) return false; var length = span.Length; var along = (span.End - span.Start) * (1 / length); double Offset(Vector point) => System.Math.Abs((point.X - span.Start.X) * along.Y - (point.Y - span.Start.Y) * along.X); double At(Vector point) => (point.X - span.Start.X) * along.X + (point.Y - span.Start.Y) * along.Y; var tolerance = CommonLineGeometry.GapTolerance; if (Offset(curve.Start) > tolerance || Offset(curve.End) > tolerance) return false; var from = System.Math.Min(At(curve.Start), At(curve.End)); var to = System.Math.Max(At(curve.Start), At(curve.End)); return from <= tolerance && to >= length - tolerance; } private static void Leads(Plate plate, int plateNumber, CommonLineGroup group, List findings, CancellationToken token) { var members = group.Members; var materials = new LeadMaterialSnapshot[members.Count]; for (var i = 0; i < members.Count; i++) { token.ThrowIfCancellationRequested(); try { materials[i] = LeadMaterialSnapshot.Capture(FixedLeadReview.Clean(members[i]), members[i].Location, token); } catch (Exception exception) when (IsInvalid(exception)) { materials[i] = null; } if (materials[i] is not { IsComplete: true }) findings.Add(new(PostVerificationKind.Incomplete, plateNumber, Number(plate, members[i]), null, "Common-line lead check: " + (materials[i]?.Reason ?? "the clean material could not be captured."))); } for (var i = 0; i < members.Count; i++) { token.ThrowIfCancellationRequested(); if (materials[i] is not { IsComplete: true } own) continue; var partners = Enumerable.Range(0, members.Count) .Where(j => j != i && materials[j] is { IsComplete: true }).ToArray(); OwnedExecution execution; try { execution = ExecutionMotionReader.Read(members[i].Program, members[i].Location, null, token); } catch (Exception exception) when (IsInvalid(exception)) { continue; // The rapid check reports the unreadable program. } var failures = new List(); var result = LeadPathValidator.Check(execution, own, partners.Select(j => materials[j]).ToArray(), _ => true, failures, token); if (!result.IsComplete) findings.Add(new(PostVerificationKind.Incomplete, plateNumber, Number(plate, members[i]), null, "Common-line lead check: " + result.Reason)); // Only partners matter here: a lead into the part's own material is the lead checks' to report. foreach (var failure in failures.Where(f => f.Obstacle is int)) { var partner = members[partners[failure.Obstacle!.Value]]; var move = execution.Motions[failure.Motion]; findings.Add(new(PostVerificationKind.CommonLine, plateNumber, Number(plate, members[i]), Number(plate, partner), move.Layer == LayerType.CommonLine ? $"A shared cut of '{members[i].BaseDrawing.Name}' runs into '{partner.BaseDrawing.Name}', its " + "common-line partner. Rejoin the parts one kerf apart and plan cutting again." : $"A lead of '{members[i].BaseDrawing.Name}' runs into '{partner.BaseDrawing.Name}', its " + "common-line partner one kerf away. Move the lead off the shared edge (Plan Cutting does).") { Location = move.Curve?.Midpoint ?? move.End }); } } } private static int? Number(Plate plate, Part part) { var index = plate.Parts.IndexOf(part); return index < 0 ? null : index + 1; } private static bool IsInvalid(Exception exception) => exception is ArgumentException or InvalidOperationException or NotSupportedException or ArithmeticException; }