using System.Collections.Generic; using System.Linq; using OpenNest.CNC.CuttingStrategy; using OpenNest.Engine.CuttingPlanning; namespace OpenNest.Engine.Sequencing { /// Applies a sequencer's exit-first route as the plate's cutting order. public static class PlateSequencing { /// Sequences the plates present when the operation starts. public static void ApplyAll(IEnumerable plates, SequenceParameters parameters) { // Reordering parts can make PlateManager replace the trailing empty // plate. Snapshot before applying so those events cannot invalidate // enumeration of the nest's live plate collection. foreach (var plate in plates.ToArray()) Apply(plate, parameters); } public static void Apply(Plate plate, SequenceParameters parameters) { var sequencer = PartSequencerFactory.Create(parameters); // Cut-first parts (test coupons) get their own route ahead of the rest, // so they stay grouped instead of interleaving with the main route. var first = plate.Parts.Where(p => p.BaseDrawing.CutFirst).ToList(); var rest = plate.Parts.Where(p => !p.BaseDrawing.CutFirst).ToList(); var ordered = CutOrder(sequencer, first, plate).Concat(CutOrder(sequencer, rest, plate)).ToList(); // Enforce dependencies AFTER reversing the exit-first route. Checking // the sequencer's output itself would invert the safety rule on apply. var cutOrder = OrderCutOffsFirst(ordered, plate); plate.Parts.Clear(); foreach (var part in cutOrder) plate.Parts.Add(part); } /// /// Moves the given cutoffs' segment parts to where the cutoff rules need them while every /// other part and cutoff keeps its position: each segment just before the first part in /// its span of the nominal line (see ), or last when it splits /// the whole sheet. One that crosses nothing stays put, unless it would follow a /// sheet-separating cutoff. /// public static void PlaceCutOffs(Plate plate, IEnumerable definitions) { var bounds = plate.BoundingBox(includeParts: false); var moving = definitions .SelectMany(definition => CutOffSegments.Of(plate, definition) .Select(segment => (Definition: definition, Segment: segment))) .ToList(); if (moving.Count == 0) return; var movingParts = new HashSet(moving.Select(item => item.Segment.Part), ReferenceEqualityComparer.Instance); var existing = new Dictionary(ReferenceEqualityComparer.Instance); foreach (var cutOff in plate.CutOffs) existing[cutOff.Drawing] = cutOff; var order = plate.Parts.ToList(); var separating = new List(); foreach (var (definition, segment) in moving) { var part = segment.Part; if (CutOff.SeparatesSheet(part, definition, bounds)) { order.Remove(part); separating.Add(part); continue; } var target = order.FindIndex(other => !other.BaseDrawing.IsCutOff && CuttingDependencyGraph.CutOffCrosses(definition.Axis, definition.Position, segment.SpanStart, segment.SpanEnd, other.BoundingBox, bounds)); if (target < 0) target = order.FindIndex(other => other.BaseDrawing.IsCutOff && !movingParts.Contains(other) && existing.TryGetValue(other.BaseDrawing, out var cut) && CutOff.SeparatesSheet(other, cut, bounds)); if (target < 0 || target > order.IndexOf(part)) continue; order.Remove(part); order.Insert(target, part); } order.AddRange(separating); // Touch only the moved cutoff parts; real parts never leave the plate. foreach (var part in movingParts) plate.Parts.Remove(part); for (var index = 0; index < order.Count; index++) { if (movingParts.Contains(order[index])) plate.Parts.Insert(index, order[index]); } } private static IEnumerable CutOrder(IPartSequencer sequencer, List parts, Plate plate) { if (parts.Count == 0) return Enumerable.Empty(); return sequencer.Sequence(parts, plate).Select(p => p.Part).Reverse(); } internal static List OrderCutOffsFirst(List ordered, Plate plate) { var definitions = new Dictionary(ReferenceEqualityComparer.Instance); foreach (var cutOff in plate.CutOffs) definitions[cutOff.Drawing] = cutOff; var bounds = plate.BoundingBox(includeParts: false); // Each cutoff part cuts one segment of its line and precedes the parts in that segment's span. var spans = new Dictionary(ReferenceEqualityComparer.Instance); foreach (var cutOff in plate.CutOffs) foreach (var segment in CutOffSegments.Of(cutOff, ordered, bounds)) spans[segment.Part] = (segment.SpanStart, segment.SpanEnd); // A cutoff that splits the whole sheet goes after every part and other cutoff, so the // head never travels back across it. It passes through no part. var separating = ordered.Where(part => part.BaseDrawing.IsCutOff && definitions.TryGetValue(part.BaseDrawing, out var definition) && CutOff.SeparatesSheet(part, definition, bounds)).ToList(); if (separating.Count > 0) { var last = new HashSet(separating, ReferenceEqualityComparer.Instance); ordered = ordered.Where(part => !last.Contains(part)).ToList(); } var cuts = ordered.Select((part, index) => (Part: part, Index: index)) .Where(item => item.Part.BaseDrawing.IsCutOff).ToList(); if (cuts.Count == 0) return ordered.Concat(separating).ToList(); // Cutoffs keep the route's order; within one cutoff, the segments due before the same // part keep the order they were regenerated in (the line's program order). var firstOnRoute = cuts.GroupBy(item => item.Part.BaseDrawing) .ToDictionary(group => group.Key, group => group.Min(item => item.Index)); cuts = cuts.OrderBy(item => firstOnRoute[item.Part.BaseDrawing]) .ThenBy(item => plate.Parts.IndexOf(item.Part)).ToList(); var emitted = new bool[ordered.Count]; var result = new List(ordered.Count); for (var i = 0; i < ordered.Count; i++) { var part = ordered[i]; if (!part.BaseDrawing.IsCutOff) { foreach (var cut in cuts) { if (emitted[cut.Index]) continue; // An orphaned cutoff still must not follow potentially // crossed parts when its nominal span cannot be recovered. if (!definitions.TryGetValue(cut.Part.BaseDrawing, out var definition) || !spans.TryGetValue(cut.Part, out var span) || CuttingDependencyGraph.CutOffCrosses(definition.Axis, definition.Position, span.Start, span.End, part.BoundingBox, bounds)) { result.Add(cut.Part); emitted[cut.Index] = true; } } } if (!emitted[i]) { result.Add(part); emitted[i] = true; } } result.AddRange(separating); return result; } } }