Files
OpenNest/OpenNest.Core/AutomaticCutOffBatch.cs
T

120 lines
5.1 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
namespace OpenNest;
/// <summary>
/// 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.
/// </summary>
public static class AutomaticCutOffBatch
{
/// <summary>Detached plans in plate order. Invalid input identifies the one-based plate number.</summary>
public static IReadOnlyList<AutomaticCutOffPlan> Create(IReadOnlyList<Plate> 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<AutomaticCutOffPlan>(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();
}
/// <summary>
/// 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.
/// </summary>
public static IReadOnlyList<AutomaticCutOffPlan> Apply(IReadOnlyList<Plate> plates,
AutomaticCutOffOptions options, CutOffSettings settings)
{
var plans = Create(plates, options, settings);
if (plans.Any(plan => plan.HasBlockingDiagnostics))
return 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 = new List<Exception>();
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.
rollbackErrors.Add(new InvalidOperationException(
$"Plate {saved.PlateNumber}: {rollbackError.Message}", rollbackError));
}
}
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 plans;
}
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);
};
}
}