mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-11 08:34:13 -04:00
merge: integrate best-effort cutting on current master
This commit is contained in:
commit
81f3c006ab
10 files changed
+572
-49
No files matched your search
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using OpenNest.CNC.CuttingPlanning;
|
||||
using OpenNest.Diagnostics;
|
||||
|
||||
namespace OpenNest.Engine.CuttingPlanning;
|
||||
|
||||
/// <summary>
|
||||
/// A deliberately uncertified fallback, not a relaxed safety checker. Reuses closed native
|
||||
/// contour preparation and emission, retaining source order except for proven prerequisites.
|
||||
/// Unknown program semantics, invalid motions and unrepresentable contours still refuse.
|
||||
/// </summary>
|
||||
internal static class BestEffortCuttingPlan
|
||||
{
|
||||
internal static CuttingPlanResult Plan(CuttingPlanSnapshot snapshot, CancellationToken token)
|
||||
{
|
||||
if (snapshot.Failure is { } failure)
|
||||
return new(failure, findings: snapshot.Findings);
|
||||
if (!snapshot.Regeneration || snapshot.OwnedParameters == null)
|
||||
return new(CuttingPlanStatus.InvalidInput);
|
||||
var findings = snapshot.Findings.ToList();
|
||||
findings.Add(new(null, null, null, null, PostVerificationKind.Incomplete,
|
||||
"Best-effort plan: lead clearance, rapid travel and material containment are not certified. "
|
||||
+ "Review the complete plan before cutting. Source contours have not been repaired."));
|
||||
var order = new List<FixedProgramPlacement>();
|
||||
var done = new HashSet<int>();
|
||||
var position = snapshot.StartPoint;
|
||||
var distance = 0.0;
|
||||
var checker = new ReleasedContourState();
|
||||
try
|
||||
{
|
||||
while (order.Count < snapshot.Placements.Count)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var source = snapshot.PreservePartOrder ? snapshot.Placements[order.Count]
|
||||
: snapshot.Placements.FirstOrDefault(p => !done.Contains(p.SourceOrdinal)
|
||||
&& snapshot.Dependencies.IsReady(p.SourceOrdinal, done));
|
||||
if (source == null || !snapshot.Dependencies.IsReady(source.SourceOrdinal, done))
|
||||
return new(CuttingPlanStatus.ConstraintConflict, findings: findings);
|
||||
var proposal = source;
|
||||
if (source.Prepared is { } prepared)
|
||||
{
|
||||
var choices = new List<ContourChoice>();
|
||||
var approach = position - source.Location;
|
||||
for (var contour = 0; contour < prepared.Count; contour++)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
choices.Add(prepared.ClosestEntry(contour, approach));
|
||||
// Prefixes are standalone programs, not relative to the prior prefix.
|
||||
var prefix = ExecutionMotionReader.ReadSupported(prepared.EmitPrefix(choices),
|
||||
source.Location, position, token);
|
||||
approach = prefix.DeparturePoint - source.Location;
|
||||
}
|
||||
var program = prepared.Emit(choices);
|
||||
var emitted = ExecutionMotionReader.ReadSupported(program, source.Location, position, token);
|
||||
proposal = source.Propose(program, emitted, choices, token);
|
||||
}
|
||||
// Read the owned payload again; malformed/nonfinite output never reaches Apply.
|
||||
var execution = ExecutionMotionReader.ReadSupported(proposal.CopyProgram(), source.Location, position, token);
|
||||
if (!execution.HasCuttingContour)
|
||||
throw new ArgumentException("Best-effort output has no cutting contour.");
|
||||
findings.AddRange(checker.Check(execution, position, source.SourceOrdinal + 1, source.IsCutOff, token)
|
||||
.Select(f => new CuttingPlanFinding(f.PartNumber is int part ? part - 1 : null,
|
||||
f.PartNumber is int ordinal ? snapshot.Placements[ordinal - 1].SourcePart : null,
|
||||
f.OtherPartNumber is int other ? other - 1 : null,
|
||||
f.OtherPartNumber is int otherOrdinal ? snapshot.Placements[otherOrdinal - 1].SourcePart : null,
|
||||
f.Kind, f.Message)));
|
||||
distance += execution.RapidDistanceFrom(position);
|
||||
if (!double.IsFinite(distance))
|
||||
throw new ArgumentException("Best-effort travel is not finite.");
|
||||
position = execution.DeparturePoint;
|
||||
done.Add(source.SourceOrdinal);
|
||||
order.Add(proposal);
|
||||
}
|
||||
token.ThrowIfCancellationRequested();
|
||||
return new(CuttingPlanStatus.BestEffort, order, findings, rapidDistance: distance)
|
||||
{ Snapshot = snapshot };
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return new(CuttingPlanStatus.Cancelled);
|
||||
}
|
||||
catch (Exception exception) when (exception is ArgumentException or NotSupportedException
|
||||
or InvalidOperationException or ArithmeticException)
|
||||
{
|
||||
findings.Add(new(null, null, null, null, PostVerificationKind.Incomplete, exception.Message));
|
||||
return new(CuttingPlanStatus.UnsupportedGeometry, findings: findings);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ internal sealed class CuttingDependencyGraph
|
||||
}
|
||||
|
||||
internal static CuttingDependencyGraph Build(IReadOnlyList<DependencyNode> nodes, Box plate,
|
||||
CancellationToken token)
|
||||
CancellationToken token, Action<CuttingDependencyException> uncertain = null)
|
||||
{
|
||||
var edges = nodes.Select(_ => new SortedSet<int>()).ToArray();
|
||||
for (var cut = 0; cut < nodes.Count; cut++)
|
||||
@@ -98,9 +98,24 @@ internal sealed class CuttingDependencyGraph
|
||||
token.ThrowIfCancellationRequested();
|
||||
// Bounds only select candidates; containment itself is proven on native material.
|
||||
if (inner != host && !nodes[inner].IsCutOff && !nodes[host].IsCutOff
|
||||
&& Within(nodes[inner].CleanBounds, nodes[host].HostBounds)
|
||||
&& InsideCutout(nodes, inner, host, token))
|
||||
edges[host].Add(inner);
|
||||
&& Within(nodes[inner].CleanBounds, nodes[host].HostBounds))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (InsideCutout(nodes, inner, host, token))
|
||||
edges[host].Add(inner);
|
||||
}
|
||||
catch (CuttingDependencyException exception) when (uncertain != null
|
||||
&& exception.Status == CuttingPlanStatus.UnsupportedGeometry)
|
||||
{
|
||||
uncertain(exception);
|
||||
}
|
||||
catch (Exception exception) when (uncertain != null
|
||||
&& exception is ArgumentException or NotSupportedException)
|
||||
{
|
||||
uncertain(Ambiguous(inner, host, exception.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
var graph = new CuttingDependencyGraph(edges.Select(e => e.ToArray()).ToArray());
|
||||
if (graph.FindCycle() is { } cyclic)
|
||||
|
||||
@@ -99,7 +99,11 @@ public sealed class CuttingPlanBatch
|
||||
var reorder = preservePartOrder ? null : CuttingPlanService.Capture(CuttingPlanRequest.ForPlate(plate,
|
||||
expansionBudget: reorderBudget ?? budget, confirmedParameters: confirmedParameters), token);
|
||||
var overlap = PlateOverlapAnalyzer.Capture(plate.Parts.ToArray(), token);
|
||||
entries[index] = new(plate, plateNumbers?[index] ?? index + 1, reorder, keepOrder, overlap);
|
||||
var fallback = reorder ?? keepOrder;
|
||||
if (fallback.Failure == CuttingPlanStatus.UnsupportedGeometry)
|
||||
fallback = CuttingPlanService.Capture(CuttingPlanRequest.ForPlate(plate,
|
||||
confirmedParameters: confirmedParameters, preservePartOrder: preservePartOrder), token, bestEffort: true);
|
||||
entries[index] = new(plate, plateNumbers?[index] ?? index + 1, reorder, keepOrder, overlap, fallback);
|
||||
}
|
||||
return new(entries, owned);
|
||||
}
|
||||
@@ -112,7 +116,8 @@ public sealed class CuttingPlanBatch
|
||||
/// Checks and plans every plate from its captured snapshots. Safe on a worker: live plates are
|
||||
/// not read. Overlapping material blocks a plate whatever its route. A free search that ends
|
||||
/// without a complete plan within its budget is retried with the current order; the proposal
|
||||
/// reports that it kept the order.
|
||||
/// reports that it kept the order. Incomplete geometry also gets a separately labelled,
|
||||
/// unverified fallback when supported closed contours can still be emitted.
|
||||
/// </summary>
|
||||
public CuttingPlanProposal Plan(IProgress<CuttingPlanProgress> progress = null,
|
||||
CancellationToken token = default)
|
||||
@@ -140,19 +145,31 @@ public sealed class CuttingPlanBatch
|
||||
reorder = CuttingPlanService.Plan(entry.Reorder, token);
|
||||
if (reorder.Status != CuttingPlanStatus.NoSolutionWithinBudget)
|
||||
{
|
||||
plans[index] = new(entry.Plate, entry.Number, reorder, null, overlap);
|
||||
plans[index] = new(entry.Plate, entry.Number, WithFallback(entry, reorder, token), null, overlap);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
progress?.Report(new(index, entries.Length, entry.Number, CuttingPlanPhase.KeepingOrder));
|
||||
plans[index] = new(entry.Plate, entry.Number, CuttingPlanService.Plan(entry.KeepOrder, token), reorder,
|
||||
overlap);
|
||||
plans[index] = new(entry.Plate, entry.Number,
|
||||
WithFallback(entry, CuttingPlanService.Plan(entry.KeepOrder, token), token), reorder, overlap);
|
||||
}
|
||||
return new(plans, ownedParameters);
|
||||
}
|
||||
|
||||
private static CuttingPlanResult WithFallback(Entry entry, CuttingPlanResult strict, CancellationToken token)
|
||||
{
|
||||
if (strict.Status != CuttingPlanStatus.UnsupportedGeometry || entry.Fallback.Failure != null)
|
||||
return strict;
|
||||
var fallback = BestEffortCuttingPlan.Plan(entry.Fallback, token);
|
||||
if (fallback.Status != CuttingPlanStatus.BestEffort)
|
||||
return fallback.Status == CuttingPlanStatus.Cancelled ? fallback : strict;
|
||||
return new(CuttingPlanStatus.BestEffort, fallback.ProposedOrder,
|
||||
strict.Findings.Concat(fallback.Findings).Distinct(), rapidDistance: fallback.RapidDistance)
|
||||
{ Snapshot = entry.Fallback };
|
||||
}
|
||||
|
||||
private sealed record Entry(Plate Plate, int Number, CuttingPlanSnapshot Reorder, CuttingPlanSnapshot KeepOrder,
|
||||
PlateOverlapSnapshot Overlap);
|
||||
PlateOverlapSnapshot Overlap, CuttingPlanSnapshot Fallback);
|
||||
}
|
||||
|
||||
/// <summary>One plate's outcome inside a <see cref="CuttingPlanProposal"/>.</summary>
|
||||
@@ -188,6 +205,10 @@ public sealed class CuttingPlanPlateResult
|
||||
|
||||
public bool IsOverlapClear => Overlap is { IsComplete: true } && Overlap.Pairs.Count == 0;
|
||||
public bool IsReady => IsRouteReady && IsOverlapClear;
|
||||
|
||||
/// <summary>Owned readable output is available, but not all geometric checks passed.</summary>
|
||||
public bool CanApplyWithWarnings => Overlap != null && Overlap.Pairs.Count == 0
|
||||
&& (IsRouteReady || Result.Status == CuttingPlanStatus.BestEffort);
|
||||
public int PartCount => Result.ProposedOrder.Count;
|
||||
public int RegeneratedCount => Result.ProposedOrder.Count(p => p.IsRegenerated);
|
||||
|
||||
@@ -196,7 +217,7 @@ public sealed class CuttingPlanPlateResult
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The outcome of a batch. Apply is all-or-nothing and is offered only when every plate is ready.
|
||||
/// The outcome of a batch. Apply is all-or-nothing. Unverified output needs explicit acceptance.
|
||||
/// </summary>
|
||||
public sealed class CuttingPlanProposal
|
||||
{
|
||||
@@ -216,17 +237,26 @@ public sealed class CuttingPlanProposal
|
||||
public bool CanApply => Plates.Count > 0 && !IsCancelled && ownedParameters != null
|
||||
&& Plates.All(p => p.IsReady);
|
||||
|
||||
/// <summary>All plates have usable output, possibly requiring explicit warning acceptance.</summary>
|
||||
public bool CanApplyWithWarnings => Plates.Count > 0 && !IsCancelled && ownedParameters != null
|
||||
&& Plates.All(p => p.CanApplyWithWarnings);
|
||||
|
||||
public bool RequiresWarningAcceptance => CanApplyWithWarnings && !CanApply;
|
||||
|
||||
/// <summary>
|
||||
/// Installs every plate's replayed proposal through <see cref="CuttingPlanService.Apply"/>, on the
|
||||
/// thread that owns the plates. Only after every plate is applied does each one keep an owned copy
|
||||
/// of the confirmed parameters as its cutting settings; any other status changes nothing.
|
||||
/// </summary>
|
||||
public CuttingCommitResult Apply(CancellationToken token = default)
|
||||
public CuttingCommitResult Apply(CancellationToken token = default) => Apply(false, token);
|
||||
|
||||
/// <summary>Explicit per-proposal acceptance; never grants consent to post CNC output.</summary>
|
||||
public CuttingCommitResult Apply(bool acceptWarnings, CancellationToken token = default)
|
||||
{
|
||||
if (!CanApply)
|
||||
if (!(CanApply || acceptWarnings && CanApplyWithWarnings))
|
||||
return new(CuttingCommitStatus.InvalidInput,
|
||||
"Every plate must have a ready plan before anything is applied.");
|
||||
var commit = CuttingPlanService.Apply(Plates.Select(p => p.Result), token);
|
||||
var commit = CuttingPlanService.Apply(Plates.Select(p => p.Result), token, null, acceptWarnings);
|
||||
if (commit.Status == CuttingCommitStatus.Applied)
|
||||
foreach (var plate in Plates)
|
||||
plate.Plate.CuttingParameters = OwnedCuttingParameters.Copy(ownedParameters);
|
||||
@@ -234,18 +264,17 @@ public sealed class CuttingPlanProposal
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A detached copy of a ready plate for display, in the proposed order with the proposed
|
||||
/// programs, or null. A refused plate is not previewed: its program graphs may be unsafe to
|
||||
/// copy. Neither is a plate that changed after planning, which would draw replayed programs at
|
||||
/// poses that were never checked. The copy has quantity zero, so drawing quantities stay put.
|
||||
/// A detached copy of a ready or explicitly unverified proposal for display, or null.
|
||||
/// Refused graphs are never cloned. Changed plates are not previewed at uncaptured poses.
|
||||
/// The copy has quantity zero, so drawing quantities stay put.
|
||||
/// Call it on the thread that owns the plates.
|
||||
/// </summary>
|
||||
public Plate BuildPreview(int index)
|
||||
{
|
||||
var planned = Plates[index];
|
||||
if (!planned.IsReady || planned.Result.Snapshot?.PlateState?.IsCurrent() != true)
|
||||
if (!planned.CanApplyWithWarnings || planned.Result.Snapshot?.PlateState?.IsCurrent() != true)
|
||||
return null;
|
||||
// Unchanged since capture: live poses, plate and programs are the validated, replayed ones.
|
||||
// Unchanged since capture: graphs are supported and the preview uses the exact proposal.
|
||||
var source = planned.Plate;
|
||||
var preview = new Plate(source.Size)
|
||||
{
|
||||
@@ -274,6 +303,9 @@ public sealed class CuttingPlanProposal
|
||||
lines.Add($"Ready to apply to {Count(Plates.Count, "plate")}: "
|
||||
+ $"{Count(Plates.Count(p => p.OrderChanged), "plate")} with a new part order, "
|
||||
+ $"{Count(Plates.Sum(p => p.RegeneratedCount), "part program")} regenerated.");
|
||||
else if (RequiresWarningAcceptance)
|
||||
lines.Add("Best-effort plan available. Review the warnings and accept the unverified plan to apply. "
|
||||
+ "This is not approval to cut or post CNC output.");
|
||||
else
|
||||
lines.Add($"Apply is unavailable: {Plates.Count - ready} of {Count(Plates.Count, "plate")} could not "
|
||||
+ "be planned. No plate changes until every plate is ready.");
|
||||
@@ -307,6 +339,17 @@ public sealed class CuttingPlanProposal
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (plate.CanApplyWithWarnings)
|
||||
{
|
||||
yield return heading + $"best-effort, unverified. {Count(plate.PartCount, "part")}, "
|
||||
+ $"{plate.RegeneratedCount} regenerated; review lead-ins and cutting order.";
|
||||
foreach (var line in Limit(DescribeOverlap(plate.Overlap)))
|
||||
yield return line;
|
||||
foreach (var line in Limit(result.Findings.Select(DescribeFinding)))
|
||||
yield return line;
|
||||
yield break;
|
||||
}
|
||||
|
||||
var missingLead = !plate.IsRouteReady && result.Status != CuttingPlanStatus.Cancelled
|
||||
&& result.Findings.Any(f => f.Kind == PostVerificationKind.MissingLeadIn);
|
||||
yield return heading + (plate.IsRouteReady
|
||||
|
||||
@@ -60,8 +60,9 @@ public sealed class CuttingPlanSnapshot
|
||||
int expansionBudget, CuttingPlanStatus? failure = null, IEnumerable<CuttingPlanFinding> findings = null,
|
||||
bool regeneration = false, bool preservePartOrder = false, int maxEntries = 16,
|
||||
Action<int> expansionObserver = null, PlateCuttingState plateState = null,
|
||||
CuttingParameters ownedParameters = null, CuttingDependencyGraph dependencies = null)
|
||||
CuttingParameters ownedParameters = null, CuttingDependencyGraph dependencies = null, bool bestEffort = false)
|
||||
{
|
||||
BestEffort = bestEffort;
|
||||
PlateState = plateState;
|
||||
OwnedParameters = ownedParameters;
|
||||
Placements = Array.AsReadOnly(placements.ToArray());
|
||||
@@ -82,6 +83,7 @@ public sealed class CuttingPlanSnapshot
|
||||
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 BestEffort { get; }
|
||||
internal bool Regeneration { get; }
|
||||
internal bool PreservePartOrder { get; }
|
||||
internal int MaxEntries { get; }
|
||||
@@ -159,7 +161,9 @@ public enum CuttingPlanStatus
|
||||
UnsupportedGeometry,
|
||||
InvalidInput,
|
||||
NoSolutionWithinBudget,
|
||||
Cancelled
|
||||
Cancelled,
|
||||
/// <summary>Usable owned programs, but geometric safety checks did not pass. Never Ready.</summary>
|
||||
BestEffort
|
||||
}
|
||||
|
||||
/// <summary>Ordinals are zero-based source positions, not proposed sequence positions.</summary>
|
||||
@@ -169,7 +173,8 @@ public sealed record CuttingPlanFinding(int? SourceOrdinal, Part SourcePart,
|
||||
/// <summary>
|
||||
/// A replayed direct-XY proposal, optionally with regenerated programs, that respects the
|
||||
/// captured cutoff/containment prerequisites. Not physical safety or posting consent. Apply
|
||||
/// installs it only for a plate-scoped request whose plate is unchanged. Failures contain no proposals.
|
||||
/// installs it only for a plate-scoped request whose plate is unchanged. BestEffort is explicitly
|
||||
/// unverified and requires batch warning acceptance; other failures contain no proposals.
|
||||
/// </summary>
|
||||
public sealed class CuttingPlanResult
|
||||
{
|
||||
|
||||
@@ -15,9 +15,13 @@ public static class CuttingPlanService
|
||||
/// Read stable caller-owned sources once into privately owned programs and geometry.
|
||||
/// No private Plates, settings aliases, quantity updates or source subcall rebinding.
|
||||
/// </summary>
|
||||
public static CuttingPlanSnapshot Capture(CuttingPlanRequest request, CancellationToken token = default)
|
||||
public static CuttingPlanSnapshot Capture(CuttingPlanRequest request, CancellationToken token = default) =>
|
||||
Capture(request, token, bestEffort: false);
|
||||
|
||||
internal static CuttingPlanSnapshot Capture(CuttingPlanRequest request, CancellationToken token, bool bestEffort)
|
||||
{
|
||||
var placements = new List<FixedProgramPlacement>();
|
||||
var warnings = new List<CuttingPlanFinding>();
|
||||
Part source = null;
|
||||
int? ordinal = null;
|
||||
try
|
||||
@@ -107,7 +111,11 @@ public static class CuttingPlanService
|
||||
{
|
||||
material = LeadMaterialSnapshot.Capture(ownedClean, location, token);
|
||||
if (!material.IsComplete)
|
||||
throw new NotSupportedException(material.Reason);
|
||||
{
|
||||
if (!bestEffort)
|
||||
throw new NotSupportedException(material.Reason);
|
||||
warnings.Add(new(index, source, null, null, PostVerificationKind.Incomplete, material.Reason));
|
||||
}
|
||||
if (!source.LeadInsLocked && (request.EligibleParts == null || eligible.Contains(source)))
|
||||
prepared = PreparedContours.Capture(ownedClean, request.ConfirmedParameters, token);
|
||||
}
|
||||
@@ -122,13 +130,16 @@ public static class CuttingPlanService
|
||||
placements[0].Execution.RapidDistanceFrom(request.StartPoint);
|
||||
source = null;
|
||||
ordinal = null;
|
||||
var dependencies = CuttingDependencyGraph.Build(nodes, request.Plate?.BoundingBox(includeParts: false), token);
|
||||
var dependencies = CuttingDependencyGraph.Build(nodes, request.Plate?.BoundingBox(includeParts: false), token,
|
||||
bestEffort ? exception => warnings.Add(new(exception.Ordinal, request.Parts[exception.Ordinal],
|
||||
exception.Other, exception.Other is int other ? request.Parts[other] : null,
|
||||
PostVerificationKind.Incomplete, exception.Message)) : null);
|
||||
return new(placements, request.StartPoint, request.ExpansionBudget, regeneration: request.ConfirmedParameters != null,
|
||||
preservePartOrder: request.PreservePartOrder, maxEntries: request.MaxEntries,
|
||||
expansionObserver: request.ExpansionObserver, plateState: plateState,
|
||||
ownedParameters: request.ConfirmedParameters == null ? null
|
||||
: OwnedCuttingParameters.Copy(request.ConfirmedParameters),
|
||||
dependencies: dependencies);
|
||||
dependencies: dependencies, findings: warnings, bestEffort: bestEffort);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -175,14 +186,15 @@ public static class CuttingPlanService
|
||||
|
||||
// beforeInstall is the commit's install-boundary test seam.
|
||||
internal static CuttingCommitResult Apply(IEnumerable<CuttingPlanResult> results, CancellationToken token,
|
||||
Action<Plate, Part> beforeInstall)
|
||||
Action<Plate, Part> beforeInstall, bool acceptUnverified = false)
|
||||
{
|
||||
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)
|
||||
var eligible = result != null && (result.Status == CuttingPlanStatus.Ready && result.IndependentlyReplayed
|
||||
|| acceptUnverified && result.Status == CuttingPlanStatus.BestEffort);
|
||||
if (!eligible || 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>();
|
||||
@@ -211,6 +223,8 @@ public static class CuttingPlanService
|
||||
return new(CuttingPlanStatus.InvalidInput);
|
||||
if (snapshot.Failure is { } failure)
|
||||
return new(failure, findings: snapshot.Findings);
|
||||
if (snapshot.BestEffort)
|
||||
return BestEffortCuttingPlan.Plan(snapshot, token);
|
||||
if (snapshot.Placements.Count == 0)
|
||||
return snapshot.PlateState == null ? new(CuttingPlanStatus.InvalidInput)
|
||||
: new(CuttingPlanStatus.Ready, independentlyReplayed: true); // Empty plate: unchanged no-op.
|
||||
|
||||
Reference in new issue
Block a user