mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-07 07:52:11 -04:00
feat(cutting): apply verified plans atomically
Plate-scoped cutting plan requests now record the plate's exact state at capture, and CuttingPlanService.Apply installs Ready, replayed proposals for a whole scope at once: - Any change after capture (order, pose bits, program reference or in-place content, drawing program, lock/lead-in flags, settings, quantity, size, quadrant or cutoff definitions) returns Stale with nothing changed. - Order changes without PartAdded/PartRemoved, so drawing quantities and sentinel plates are untouched; ObservableList.Reorder exposes the same operation and Plate.PartsReordered is raised once per changed plate. - Regenerated parts receive owned copies of the replayed program and of the settings captured with the request; fixed programs stay in place. - An install failure restores every plate exactly; an observer failure after publication is reported as a refresh error, not a rollback.
This commit is contained in:
1 parent
ebc54f0653
commit
9c530ca7d2
11 files changed
+1186
-10
No files matched your search
@@ -28,6 +28,21 @@ public sealed class CuttingPlanRequest
|
||||
MaxEntries = maxEntries;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plate scope: plans the plate's current parts and captures its exact state, so a Ready
|
||||
/// result can later be applied through <see cref="CuttingPlanService.Apply"/>.
|
||||
/// </summary>
|
||||
public CuttingPlanRequest(Plate plate, Vector startPoint = default, int expansionBudget = 20000,
|
||||
CuttingParameters confirmedParameters = null, IEnumerable<Part> eligibleParts = null,
|
||||
bool preservePartOrder = false, int maxEntries = 16)
|
||||
: this(plate?.Parts, startPoint, expansionBudget, confirmedParameters, eligibleParts,
|
||||
preservePartOrder, maxEntries)
|
||||
{
|
||||
Plate = plate;
|
||||
}
|
||||
|
||||
/// <summary>The plate scope, or null for a detached part list that cannot be applied.</summary>
|
||||
public Plate Plate { get; }
|
||||
public CuttingParameters ConfirmedParameters { get; }
|
||||
public IReadOnlyList<Part> EligibleParts { get; }
|
||||
public bool PreservePartOrder { get; }
|
||||
@@ -44,8 +59,11 @@ public sealed class CuttingPlanSnapshot
|
||||
internal CuttingPlanSnapshot(IEnumerable<FixedProgramPlacement> placements, Vector startPoint,
|
||||
int expansionBudget, CuttingPlanStatus? failure = null, IEnumerable<CuttingPlanFinding> findings = null,
|
||||
bool regeneration = false, bool preservePartOrder = false, int maxEntries = 16,
|
||||
Action<int> expansionObserver = null)
|
||||
Action<int> expansionObserver = null, PlateCuttingState plateState = null,
|
||||
CuttingParameters ownedParameters = null)
|
||||
{
|
||||
PlateState = plateState;
|
||||
OwnedParameters = ownedParameters;
|
||||
Placements = Array.AsReadOnly(placements.ToArray());
|
||||
StartPoint = startPoint;
|
||||
ExpansionBudget = expansionBudget;
|
||||
@@ -57,6 +75,10 @@ public sealed class CuttingPlanSnapshot
|
||||
ExpansionObserver = expansionObserver;
|
||||
}
|
||||
|
||||
/// <summary>Exact captured plate state for plate-scoped requests; null for detached part lists.</summary>
|
||||
internal PlateCuttingState PlateState { get; }
|
||||
/// <summary>Owned copy of the confirmed parameters taken at capture; never the caller's object.</summary>
|
||||
internal CuttingParameters OwnedParameters { get; }
|
||||
internal bool Regeneration { get; }
|
||||
internal bool PreservePartOrder { get; }
|
||||
internal int MaxEntries { get; }
|
||||
@@ -162,4 +184,6 @@ public sealed class CuttingPlanResult
|
||||
public int Expansions { get; }
|
||||
public double RapidDistance { get; }
|
||||
public bool IndependentlyReplayed { get; }
|
||||
/// <summary>The captured input this result was planned from; binds Apply to its freshness record.</summary>
|
||||
internal CuttingPlanSnapshot Snapshot { get; set; }
|
||||
}
|
||||
@@ -23,6 +23,16 @@ public static class CuttingPlanService
|
||||
try
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
PlateCuttingState plateState = null;
|
||||
if (request?.Plate != null)
|
||||
{
|
||||
// Exact freshness record first, on the caller thread; the planned list must be it.
|
||||
plateState = PlateCuttingState.Capture(request.Plate, token);
|
||||
if (request.Parts == null || !request.Parts.SequenceEqual(plateState.Order, ReferenceEqualityComparer.Instance))
|
||||
throw new ArgumentException("The plate's parts changed after the request was created.");
|
||||
if (request.Parts.Count == 0 && request.ExpansionBudget > 0 && request.MaxEntries > 0)
|
||||
return new([], request.StartPoint, request.ExpansionBudget, plateState: plateState);
|
||||
}
|
||||
if (request?.Parts == null || request.Parts.Count == 0 || request.ExpansionBudget <= 0 || request.MaxEntries <= 0)
|
||||
throw new ArgumentException("A nonempty source list and positive expansion budget are required.");
|
||||
var eligible = new HashSet<Part>(ReferenceEqualityComparer.Instance);
|
||||
@@ -80,7 +90,9 @@ public static class CuttingPlanService
|
||||
placements[0].Execution.RapidDistanceFrom(request.StartPoint);
|
||||
return new(placements, request.StartPoint, request.ExpansionBudget, regeneration: request.ConfirmedParameters != null,
|
||||
preservePartOrder: request.PreservePartOrder, maxEntries: request.MaxEntries,
|
||||
expansionObserver: request.ExpansionObserver);
|
||||
expansionObserver: request.ExpansionObserver, plateState: plateState,
|
||||
ownedParameters: request.ConfirmedParameters == null ? null
|
||||
: OwnedCuttingParameters.Copy(request.ConfirmedParameters));
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -105,6 +117,51 @@ public static class CuttingPlanService
|
||||
|
||||
/// <summary>Worker-only planning uses owned values; live identities are never dereferenced.</summary>
|
||||
public static CuttingPlanResult Plan(CuttingPlanSnapshot snapshot, CancellationToken token = default)
|
||||
{
|
||||
var result = PlanCaptured(snapshot, token);
|
||||
result.Snapshot = snapshot;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs exactly the replayed proposals of Ready plate-scoped results, all or nothing.
|
||||
/// Each plate must still match the state captured with its request; otherwise Stale and
|
||||
/// nothing changes. Run on the thread that owns the plates. Never replans.
|
||||
/// </summary>
|
||||
public static CuttingCommitResult Apply(IEnumerable<CuttingPlanResult> results, CancellationToken token = default) =>
|
||||
Apply(results, token, null);
|
||||
|
||||
// beforeInstall is the commit's install-boundary test seam.
|
||||
internal static CuttingCommitResult Apply(IEnumerable<CuttingPlanResult> results, CancellationToken token,
|
||||
Action<Plate, Part> beforeInstall)
|
||||
{
|
||||
var plans = new List<PlateCuttingPlan>();
|
||||
foreach (var result in results ?? [])
|
||||
{
|
||||
var snapshot = result?.Snapshot;
|
||||
if (result?.Status != CuttingPlanStatus.Ready || !result.IndependentlyReplayed
|
||||
|| snapshot?.PlateState == null || result.ProposedOrder.Count != snapshot.Placements.Count)
|
||||
return new(CuttingCommitStatus.InvalidInput,
|
||||
"Only Ready, independently replayed plate-scoped proposals can be applied.");
|
||||
var programs = new List<PlannedPartProgram>();
|
||||
foreach (var proposal in result.ProposedOrder)
|
||||
{
|
||||
if (!proposal.IsRegenerated)
|
||||
continue;
|
||||
if (snapshot.OwnedParameters == null)
|
||||
return new(CuttingCommitStatus.InvalidInput, "A regenerated proposal has no captured settings.",
|
||||
snapshot.PlateState.Plate);
|
||||
// Fresh owned copies: a result can be applied at most once per captured state,
|
||||
// and nothing installed aliases the proposal or another part's settings.
|
||||
programs.Add(new(proposal.SourcePart, proposal.CopyProgram(),
|
||||
OwnedCuttingParameters.Copy(snapshot.OwnedParameters)));
|
||||
}
|
||||
plans.Add(new(snapshot.PlateState, result.ProposedOrder.Select(p => p.SourcePart), programs));
|
||||
}
|
||||
return CuttingPlanCommit.Apply(plans, token, beforeInstall);
|
||||
}
|
||||
|
||||
private static CuttingPlanResult PlanCaptured(CuttingPlanSnapshot snapshot, CancellationToken token)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
return new(CuttingPlanStatus.Cancelled);
|
||||
@@ -112,6 +169,9 @@ public static class CuttingPlanService
|
||||
return new(CuttingPlanStatus.InvalidInput);
|
||||
if (snapshot.Failure is { } failure)
|
||||
return new(failure, findings: snapshot.Findings);
|
||||
if (snapshot.Placements.Count == 0)
|
||||
return snapshot.PlateState == null ? new(CuttingPlanStatus.InvalidInput)
|
||||
: new(CuttingPlanStatus.Ready, independentlyReplayed: true); // Empty plate: unchanged no-op.
|
||||
try
|
||||
{
|
||||
var fixedFindings = new List<CuttingPlanFinding>();
|
||||
|
||||
Reference in new issue
Block a user