using System; using System.Collections.Generic; using System.Linq; namespace OpenNest; /// /// Plans and applies the same automatic-cutoff settings to a stable, ordered set of plates. /// Callers must prevent concurrent edits for the duration of either operation. /// public static class AutomaticCutOffBatch { /// Detached plans in plate order. Invalid input identifies the one-based plate number. public static IReadOnlyList Create(IReadOnlyList plates, AutomaticCutOffOptions options, CutOffSettings settings) { ArgumentNullException.ThrowIfNull(plates); ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(settings); if (plates.Distinct(ReferenceEqualityComparer.Instance).Count() != plates.Count) throw new ArgumentException("Each plate must occur only once.", nameof(plates)); var plans = new List(plates.Count); for (var index = 0; index < plates.Count; index++) { try { plans.Add(AutomaticCutOffPlanner.Create(plates[index], options, settings)); } catch (ArgumentException error) { throw new ArgumentException($"Plate {index + 1}: {error.Message}", nameof(plates), error); } } return plans.AsReadOnly(); } /// /// Replans all plates before changing any. A blocking plan returns without applying any /// definitions; empty/unchanged plates are untouched. On failure, restores cutoff state /// on every touched plate, reporting explicitly if any restoration also fails. /// Returned plans describe the fresh proposal, not preview parts to accept into a plate. /// public static IReadOnlyList Apply(IReadOnlyList plates, AutomaticCutOffOptions options, CutOffSettings settings) => Stage(plates, options, settings).Plans; /// /// Applies like , but keeps the recovery state so the caller can take the /// new cut-offs back off again () when the work they /// were added for is abandoned. A blocked batch changes nothing and has nothing to revert. /// public static AutomaticCutOffStaging Stage(IReadOnlyList plates, AutomaticCutOffOptions options, CutOffSettings settings) { var plans = Create(plates, options, settings); if (plans.Any(plan => plan.HasBlockingDiagnostics)) return new AutomaticCutOffStaging(plans, []); var restoreActions = new List<(int PlateNumber, Action Restore)>(); try { for (var index = 0; index < plates.Count; index++) { var plan = plans[index]; if (plan.Definitions.Count == 0) continue; var plate = plates[index]; // Register recovery before the first observable mutation, including AddRange. restoreActions.Add((index + 1, CaptureRestore(plate, plan))); plate.CutOffs.AddRange(plan.Definitions); plate.RegenerateCutOffs(settings); } } catch (Exception applyError) { var rollbackErrors = RestoreAll(restoreActions); if (rollbackErrors.Count > 0) throw new InvalidOperationException( $"Apply failed: {applyError.Message}\nRestoring cut-offs also failed: " + string.Join("; ", rollbackErrors.Select(e => e.Message)) + "\nThe nest may be incomplete; review it before saving or cutting.", new AggregateException(new[] { applyError }.Concat(rollbackErrors))); throw new InvalidOperationException( $"No new cut-offs were retained; original cut-offs were restored. {applyError.Message}", applyError); } return new AutomaticCutOffStaging(plans, restoreActions); } internal static List RestoreAll(IReadOnlyList<(int PlateNumber, Action Restore)> restoreActions) { var errors = new List(); for (var index = restoreActions.Count - 1; index >= 0; index--) { var saved = restoreActions[index]; try { saved.Restore(); } catch (Exception rollbackError) { // A broken observer on one plate must not prevent recovery of the others. errors.Add(new InvalidOperationException( $"Plate {saved.PlateNumber}: {rollbackError.Message}", rollbackError)); } } return errors; } private static Action CaptureRestore(Plate plate, AutomaticCutOffPlan plan) { // Regeneration replaces drawing programs and removes/reinserts cutoff parts. // Save only that state; real parts, poses, programs and quantities stay untouched. var programs = plate.CutOffs.Select(c => (CutOff: c, Program: c.Drawing.Program)).ToArray(); var parts = plate.Parts.Select((part, index) => (Part: part, Index: index)) .Where(p => p.Part.BaseDrawing.IsCutOff).ToArray(); return () => { foreach (var definition in plan.Definitions) plate.CutOffs.Remove(definition); for (var index = plate.Parts.Count - 1; index >= 0; index--) { if (plate.Parts[index].BaseDrawing.IsCutOff) plate.Parts.RemoveAt(index); } foreach (var saved in programs) saved.CutOff.Drawing.Program = saved.Program; foreach (var saved in parts) plate.Parts.Insert(saved.Index, saved.Part); }; } } /// /// Cut-offs added by that are still provisional: either /// them once the work they belong to is committed, or them. /// Owner-thread only, like the plates it changed. /// public sealed class AutomaticCutOffStaging { private IReadOnlyList<(int PlateNumber, Action Restore)> restoreActions; internal AutomaticCutOffStaging(IReadOnlyList plans, IReadOnlyList<(int PlateNumber, Action Restore)> restoreActions) { Plans = plans; this.restoreActions = restoreActions; Definitions = plans.SelectMany(plan => plan.HasBlockingDiagnostics ? [] : plan.Definitions).ToArray(); } /// The fresh plans, one per plate in the order given. public IReadOnlyList Plans { get; } /// True when any plate blocked the batch; then nothing was added. public bool IsBlocked => Plans.Any(plan => plan.HasBlockingDiagnostics); /// The definitions this staging added (empty when blocked or nothing was needed). public IReadOnlyList Definitions { get; } /// True while the added cut-offs can still be reverted. public bool CanRevert => restoreActions.Count > 0; /// Makes the added cut-offs permanent; a later does nothing. public void Keep() => restoreActions = []; /// /// Removes the added cut-offs and restores each touched plate's previous cut-off programs and /// sequence. Real parts are not touched. Runs once; throws if any plate could not be restored. /// public void Revert() { var actions = restoreActions; restoreActions = []; var errors = AutomaticCutOffBatch.RestoreAll(actions); if (errors.Count > 0) throw new InvalidOperationException( "Removing the added cut-offs failed: " + string.Join("; ", errors.Select(e => e.Message)) + "\nThe nest may be incomplete; review it before saving or cutting.", new AggregateException(errors)); } }