using System; using System.Collections.Generic; using System.Linq; using System.Threading; using OpenNest.CNC; using OpenNest.CNC.CuttingPlanning; using OpenNest.CNC.CuttingStrategy; using OpenNest.Geometry; namespace OpenNest.Engine.CuttingPlanning; /// /// Capture of a plate's common-line groups cut once (docs/common-line-cutting.md). Each group is /// cut member by member in 's order: a member's holes, the /// shared cuts it owns (open centerline cuts), then its remainder pieces, which end in kerf those /// shared cuts, or an earlier member's, opened. The owner of a shared cut therefore precedes its /// partner. Shared spans are derived from the placed geometry at capture; a broken group refuses. /// internal sealed class CommonLineCapture { private readonly Dictionary members = new(ReferenceEqualityComparer.Instance); private CommonLineCapture() { } /// Null when the request's plate cuts no common line once. /// A group is broken. internal static CommonLineCapture Of(CuttingPlanRequest request) { var plate = request?.Plate; if (plate == null || !plate.CutsCommonLinesOnce) return null; var capture = new CommonLineCapture(); foreach (var group in plate.CommonLines) { var check = CommonLineGeometry.Check(group); if (!check.IsIntact) { var problem = check.Problems[0]; throw new NotSupportedException( $"Part {IndexOf(request.Parts, problem.Part) + 1} {problem.Message} Rejoin its common line, " + "remove it, or cut the plate's common lines twice, then replan."); } var order = CommonLineCutting.Order(group, check); for (var i = 0; i < order.Members.Count; i++) { var part = order.Members[i]; if (IndexOf(request.Parts, part) < 0) throw new ArgumentException("A common-line member is not among the planned parts."); capture.members[part] = new Member(order, part, order.SpansOf(part).ToArray(), order.CutsOwnedBy(part).ToArray()); } } return capture; } /// Source ordinals of every member, for the rapid checker. internal IReadOnlySet MemberNumbers(IReadOnlyList parts) => members.Keys.Select(part => IndexOf(parts, part) + 1).ToHashSet(); /// (part, prerequisite) source ordinals: each shared cut's owner precedes its partner. internal IReadOnlyList<(int Part, int Prerequisite)> Order(IReadOnlyList parts) => members.Values.SelectMany(member => member.Owned) .Select(cut => (IndexOf(parts, cut.Partner), IndexOf(parts, cut.Owner))) .Distinct() .ToArray(); internal bool IsMember(Part part) => members.ContainsKey(part); /// The member's owned clean program with its shared spans on the Tab layer. /// A span does not lie along the program's edges. internal Program Mark(Part part, Program ownedClean, Vector location, int ordinal) { var member = members[part]; try { return CommonLineCutting.MarkSharedSpans(ownedClean, location, member.Spans, member.Order.Group.Kerf); } catch (ArgumentException exception) { throw new NotSupportedException($"Part {ordinal + 1} cannot be cut on its common line: {exception.Message}"); } } /// /// The member's shared cuts in part coordinates, each cut from whichever end its straight /// leads fit: the cut, its lead-in and its lead-out may touch no material, and the pierce keeps /// the pierce zone. Leads are never shortened: a cut whose leads fit at neither end refuses, /// naming the span. /// /// No lead fits a shared cut. internal CommonLinePerimeter Perimeter(Part part, int ordinal, IReadOnlyList parts, LeadMaterialSnapshot own, IReadOnlyList materials, CuttingParameters parameters, double pierceRadius, CancellationToken token) { var member = members[part]; var location = part.Location; var leadIn = parameters.ExternalLeadIn ?? new NoLeadIn(); var leadOut = parameters.ExternalLeadOut ?? new NoLeadOut(); var cuts = new List(); foreach (var cut in member.Owned) { token.ThrowIfCancellationRequested(); var forward = new CommonLineCutPath(cut.Span.Start - location, cut.Span.End - location); var reverse = new CommonLineCutPath(forward.End, forward.Start); string reason = null; CommonLineCutPath chosen = null; foreach (var path in new[] { forward, reverse }) { var program = new Program(Mode.Absolute); program.Codes.AddRange(ContourCuttingStrategy.CommonLineSharedCutCodes(path, leadIn, leadOut)); var execution = ExecutionMotionReader.ReadSupported(program, location, null, token); var result = LeadPathValidator.Check(execution, own, materials, pierceRadius, token); if (result.IsComplete && result.IsClear) { chosen = path; break; } reason ??= result.Reason; } if (chosen == null) throw new NotSupportedException(FormattableString.Invariant( $"No lead fits the common line between parts {ordinal + 1} and {IndexOf(parts, cut.Partner) + 1} from ({cut.Span.Start.X:0.####}, {cut.Span.Start.Y:0.####}) to ({cut.Span.End.X:0.####}, {cut.Span.End.Y:0.####}) at either end: ") + reason + " Shorten the external lead-in or lead-out, or cut the plate's common lines twice."); cuts.Add(chosen); } return new CommonLinePerimeter(member.Order.Group.Kerf, cuts); } private static int IndexOf(IReadOnlyList parts, Part part) { for (var i = 0; i < parts.Count; i++) if (ReferenceEquals(parts[i], part)) return i; return -1; } private sealed record Member(CommonLineCutOrder Order, Part Part, IReadOnlyList Spans, IReadOnlyList Owned); }