using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.CNC;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.Diagnostics;
using OpenNest.Geometry;
using OpenNest.Support;
namespace OpenNest.Engine.CuttingPlanning;
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.
///
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();
var warnings = new List();
Part source = null;
int? ordinal = null;
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(ReferenceEqualityComparer.Instance);
if (request.EligibleParts != null)
{
if (request.ConfirmedParameters == null)
throw new ArgumentException("Eligibility requires confirmed cutting parameters.");
foreach (var part in request.EligibleParts)
if (part == null || !request.Parts.Any(p => ReferenceEquals(p, part)) || !eligible.Add(part))
throw new ArgumentException("Foreign or duplicate eligible placement.");
}
// Common lines cut once: each member regenerates as its holes, shared cuts and remainder.
var commonLines = CommonLineCapture.Of(request);
var deferredMembers = new List<(int Placement, Program Clean)>();
// Cutoff definitions by drawing reference, as automatic sequencing matches them.
var definitions = new Dictionary(ReferenceEqualityComparer.Instance);
foreach (var cutOff in request.Plate?.CutOffs ?? [])
if (cutOff?.Drawing != null)
definitions[cutOff.Drawing] = cutOff;
// Each cut-off part cuts one trimmed segment of its line and precedes only the parts
// in that segment's span (docs/automatic-scrap-cutoffs.md), not every part the line passes.
var spans = new Dictionary>(ReferenceEqualityComparer.Instance);
// Lines splitting off a piece of the sheet, alone or with the separators they stop on.
IReadOnlyDictionary separators = null;
var nodes = new List();
// Reuse the reader's coordinate validation without publishing its native kernel.
var identities = new HashSet(ReferenceEqualityComparer.Instance);
// Every part's box, read once, for slug slices to keep clear of parts nested in a cutout.
Box[] partBoxes = null;
for (var index = 0; index < request.Parts.Count; index++)
{
token.ThrowIfCancellationRequested();
source = request.Parts[index];
ordinal = index;
if (source?.BaseDrawing == null || source.Program == null || !double.IsFinite(source.Rotation)
|| !identities.Add(source))
throw new ArgumentException("Missing/duplicate source placement or invalid pose.");
// Validate both original graphs before Clone or any virtual transform can
// erase unsupported runtime semantics, including fixed/ineligible targets.
OwnedProgramCopy.Validate(source.BaseDrawing.Program, token);
OwnedProgramCopy.Validate(source.Program, token);
if (source.BaseDrawing.IsCutOff)
{
// A cutoff is a fixed open cut: never material, never regenerated.
if (request.Plate == null)
throw new NotSupportedException("Cutoff dependencies require a plate-scoped request.");
if (eligible.Contains(source))
throw new ArgumentException("A cutoff cannot be eligible for regeneration.");
var cut = ExecutionMotionReader.ReadSupported(source.Program, source.Location, request.StartPoint, token);
if (!cut.HasCuttingContour)
throw new ArgumentException("Placed cutoff program has no nonzero cutting motions.");
var defined = definitions.TryGetValue(source.BaseDrawing, out var definition);
var sheet = request.Plate.BoundingBox(includeParts: false);
separators ??= CutOff.Separators(request.Parts, definitions.Values, sheet);
int? round = defined && separators.TryGetValue(source, out var found) ? found : null;
var separates = round != null;
CutOffDefinition nominal = null;
if (defined)
{
if (!spans.TryGetValue(definition, out var segments))
{
spans[definition] = segments = new(ReferenceEqualityComparer.Instance);
foreach (var segment in CutOffSegments.Of(definition, request.Parts, sheet))
segments[segment.Part] = (segment.SpanStart, segment.SpanEnd);
}
nominal = segments.TryGetValue(source, out var span)
? new(definition.Axis, definition.Position, span.Start, span.End, definition.Gaps)
: new(definition.Axis, definition.Position, definition.StartLimit, definition.EndLimit,
definition.Gaps);
}
nodes.Add(new(true, nominal, source.BoundingBox, null, null, round));
// A head-locked far-edge segment may be cut from either end (docs/automatic-scrap-cutoffs.md).
FixedProgramPlacement reversed = null;
if (defined && CutOff.ReverseAtFarEdge(source.Program) is { } flipped)
reversed = new(source, index, source.Location, source.Rotation, source.LeadInsLocked,
ExecutionMotionReader.ReadSupported(flipped, source.Location, request.StartPoint, token), flipped)
{ IsCutOff = true, SeparatesSheet = separates, ReversesCutOff = true };
placements.Add(new(source, index, source.Location, source.Rotation, source.LeadInsLocked,
cut, OwnedProgramCopy.Copy(source.Program, token))
{ IsCutOff = true, SeparatesSheet = separates, Reversed = reversed });
continue;
}
var clean = ExecutionMotionReader.ReadSupported(source.BaseDrawing.Program, Vector.Zero, null, token);
if (!clean.HasCuttingContour)
throw new NotSupportedException(clean.Motions.Any(m => m.Layer == LayerType.DisplayPerimeter)
? "A display perimeter with no cutouts has nothing to cut."
: "Scribe-only or noncutting source drawings are outside this route slice.");
var execution = ExecutionMotionReader.ReadSupported(source.Program, source.Location, request.StartPoint, token);
if (!execution.HasCuttingContour)
throw new ArgumentException("Placed program has no nonzero cutting contour motions.");
var ownedProgram = OwnedProgramCopy.Copy(source.Program, token);
PreparedContours prepared = null;
LeadMaterialSnapshot material = null;
// Geometry-only transform: original graphs were strictly validated and
// clone expansion bounded above. Legacy Rotate visits per parent, so
// retain Clone's per-parent sharing here instead of restoring diamonds.
// Exact placed/proposed payloads still use lossless OwnedProgramCopy.
var ownedClean = (Program)source.BaseDrawing.Program.Clone();
ownedClean.Rotate(source.Rotation - source.BaseDrawing.Program.Rotation);
// Incremental rounding can leave a converted contour just short of its seam.
IncrementalClosureDrift.Close(ownedClean);
// A baked-in seam overcut encloses no area; left in, the ring touches itself.
SeamOvercut.Trim(ownedClean);
// A common-line member never cuts the spans earlier members cut: they are its material's tabs.
var member = request.ConfirmedParameters != null && commonLines?.IsMember(source) == true;
if (member)
{
if (source.LeadInsLocked || request.EligibleParts != null && !eligible.Contains(source))
throw new NotSupportedException($"Part {index + 1} shares a common line that is cut once, "
+ "so Plan Cutting must regenerate its program: "
+ (source.LeadInsLocked ? "unlock its lead-ins" : "include it in the plan")
+ ", or cut the plate's common lines twice.");
ownedClean = commonLines.Mark(source, ownedClean, source.Location, index);
}
// Material extent only: remote rapids and scribe marks must not widen it, or a
// genuine insert could fail the containment prefilter.
var cleanBounds = NestedPartContainment.CleanBounds(ownedClean, source.Location, token);
var location = source.Location;
if (request.ConfirmedParameters != null)
{
material = LeadMaterialSnapshot.Capture(ownedClean, location, token, sharedSpans: member);
if (!material.IsComplete)
{
if (!bestEffort)
throw new NotSupportedException(material.Reason);
warnings.Add(new(index, source, null, null, PostVerificationKind.Incomplete, material.Reason));
}
if (member)
deferredMembers.Add((placements.Count, ownedClean));
else if (!source.LeadInsLocked && (request.EligibleParts == null || eligible.Contains(source)))
{
prepared = DrawingPinCapture.Prepare(source, index, ownedClean, request.ConfirmedParameters,
warnings, token);
prepared = SupportTabCapture.Prepare(prepared, ownedClean, location, request, token);
if (request.ConfirmedParameters.SliceTippingSlugs)
{
partBoxes ??= request.Parts.Select(part => part?.BoundingBox).ToArray();
prepared = SlugSliceCapture.Prepare(prepared, location, request, partBoxes, token);
}
}
else if (source.HasActiveDrawingPins)
warnings.Add(DrawingPinCapture.NotApplied(source, index));
}
var captured = material;
// Fixed-route material is captured only for a pair whose bounds admit containment.
nodes.Add(new(false, null, source.BoundingBox, cleanBounds,
() => captured ?? LeadMaterialSnapshot.Capture(ownedClean, location, token),
cutFirst: source.BaseDrawing.CutFirst));
placements.Add(new(source, index, source.Location, source.Rotation, source.LeadInsLocked,
execution, ownedProgram, prepared, material));
}
// A member's shared cuts need every part's material: each must fit its leads at one end.
var materials = placements.Where(p => !p.IsCutOff && p.Material != null).Select(p => p.Material).ToArray();
foreach (var (at, clean) in deferredMembers)
{
token.ThrowIfCancellationRequested();
var placed = placements[at];
source = placed.SourcePart;
ordinal = placed.SourceOrdinal;
placements[at] = CaptureMember(placed, clean, commonLines, request, materials, warnings, bestEffort, token);
}
// The start can be invalid even when the first rapid has no cutting geometry.
placements[0].Execution.RapidDistanceFrom(request.StartPoint);
source = null;
ordinal = null;
var commonLineOrder = commonLines?.Order(request.Parts) ?? [];
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, commonLineOrder);
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, findings: warnings, bestEffort: bestEffort)
{
SupportTabsSkipped = SupportTabSettings.SkipsForNoRange(request.ConfirmedParameters, request.SupportBed),
CommonLineMembers = commonLines?.MemberNumbers(request.Parts) ?? new HashSet(),
CommonLineOrder = commonLineOrder.ToHashSet(),
};
}
catch (OperationCanceledException)
{
return Failure(CuttingPlanStatus.Canceled, "Capture canceled.");
}
catch (CuttingDependencyException exception)
{
var other = exception.Other is { } index ? request.Parts[index] : null;
return new(placements, request.StartPoint, request.ExpansionBudget, exception.Status,
[new(exception.Ordinal, request.Parts[exception.Ordinal], exception.Other, other, null, exception.Message)]);
}
catch (NotSupportedException exception)
{
return Failure(CuttingPlanStatus.UnsupportedGeometry, exception.Message);
}
catch (Exception exception) when (exception is ArgumentException or InvalidOperationException or ArithmeticException)
{
return Failure(CuttingPlanStatus.InvalidInput, exception.Message);
}
CuttingPlanSnapshot Failure(CuttingPlanStatus status, string message) =>
new(placements, request?.StartPoint ?? Vector.Zero, request?.ExpansionBudget ?? 0, status,
[new(ordinal, source, null, null, null, message)]);
}
// A common-line member regenerates from its marked clean program: holes as usual, then its
// whole perimeter from its common line, or its remainder, from one fixed entry. Its leads are checked here, so a lead that
// fits nowhere refuses with the span named rather than as a search that ran out.
private static FixedProgramPlacement CaptureMember(FixedProgramPlacement placed, Program clean,
CommonLineCapture commonLines, CuttingPlanRequest request, IReadOnlyList materials,
List warnings, bool bestEffort, CancellationToken token)
{
var source = placed.SourcePart;
var index = placed.SourceOrdinal;
var parameters = request.ConfirmedParameters;
var pierceRadius = parameters.GetPierceZoneRadius();
var material = placed.Material;
PreparedContours prepared;
try
{
var perimeter = commonLines.Perimeter(source, index, request.Parts, clean, material, materials,
parameters, pierceRadius, token);
prepared = DrawingPinCapture.Prepare(source, index, clean, parameters, warnings, token, perimeter);
if (!bestEffort)
{
var entry = prepared.Entries(prepared.PerimeterOrdinal, Vector.Zero)[0];
var probe = ExecutionMotionReader.ReadSupported(prepared.EmitCandidateForValidation(entry),
source.Location, null, token);
var lead = LeadPathValidator.Check(probe, material, materials, pierceRadius, token);
if (!lead.IsComplete || !lead.IsClear)
throw new NotSupportedException((perimeter.StartsOnCommonLine
? "no lead-in fits the start of its common line: "
: "no lead-in fits its common-line remainder in the shared kerf: ") + lead.Reason);
}
}
catch (NotSupportedException exception) when (!exception.Message.StartsWith("Part ", StringComparison.Ordinal)
&& !exception.Message.StartsWith("No lead fits", StringComparison.Ordinal))
{
throw new NotSupportedException($"Part {index + 1}: {exception.Message}", exception);
}
return new(source, index, placed.Location, placed.Rotation, placed.LeadInsLocked, placed.Execution,
placed.CopyProgram(), prepared, material);
}
public static CuttingPlanResult Plan(CuttingPlanRequest request, CancellationToken token = default) =>
Plan(Capture(request, token), token);
/// Worker-only planning uses owned values; live identities are never dereferenced.
public static CuttingPlanResult Plan(CuttingPlanSnapshot snapshot, CancellationToken token = default)
{
var result = PlanCaptured(snapshot, token);
result.Snapshot = snapshot;
return result;
}
///
/// 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.
///
public static CuttingCommitResult Apply(IEnumerable results, CancellationToken token = default) =>
Apply(results, token, null);
// beforeInstall is the commit's install-boundary test seam.
internal static CuttingCommitResult Apply(IEnumerable results, CancellationToken token,
Action beforeInstall, bool acceptUnverified = false)
{
var plans = new List();
foreach (var result in results ?? [])
{
var snapshot = result?.Snapshot;
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();
var cutOffs = new List();
foreach (var proposal in result.ProposedOrder)
{
if (proposal.ReversesCutOff)
cutOffs.Add(new(proposal.SourcePart, proposal.CopyProgram()));
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, cutOffs));
}
return CuttingPlanCommit.Apply(plans, token, beforeInstall);
}
private static CuttingPlanResult PlanCaptured(CuttingPlanSnapshot snapshot, CancellationToken token)
{
if (token.IsCancellationRequested)
return new(CuttingPlanStatus.Canceled);
if (snapshot == null)
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.
if (snapshot.PreservePartOrder
&& snapshot.Dependencies.FirstViolation(Enumerable.Range(0, snapshot.Placements.Count).ToArray()) is { } manual)
return new(CuttingPlanStatus.ConstraintConflict,
findings: [DependencyFinding(snapshot, manual, "The preserved part order")]);
try
{
var fixedFindings = new List();
foreach (var placement in snapshot.Placements)
{
token.ThrowIfCancellationRequested();
if (placement.Prepared != null)
continue; // An eligible old crossing is precisely what regeneration may repair.
// With confirmed parameters a locked program is accepted as given: replay reports
// its internal violations as warnings (D1a). The no-parameter route and unlocked
// programs left ineligible keep refusing them.
if (snapshot.Regeneration && placement.FixedMotions(placement.Execution) != null)
continue;
// Ignore only the unknown incoming rapid. Every fixed internal motion is checked.
var findings = new ReleasedContourState(reportMissingLeadIns: false).Check(placement.Execution, null,
placement.SourceOrdinal + 1, placement.IsCutOff, token);
fixedFindings.AddRange(JointCuttingPlanSearch.Map(snapshot, findings));
}
if (fixedFindings.Count != 0)
return new(fixedFindings.Any(f => f.Kind == PostVerificationKind.Incomplete)
? CuttingPlanStatus.UnsupportedGeometry : CuttingPlanStatus.ConstraintConflict,
findings: fixedFindings);
if (snapshot.Regeneration)
{
var joint = JointCuttingPlanSearch.Run(snapshot, token);
// Capture warnings (drawing pins that could not be honored) stay visible on a refusal.
if (joint.Status != CuttingPlanStatus.Ready)
return new(joint.Status, findings: snapshot.Findings.Concat(joint.Findings), expansions: joint.Expansions);
return ReplayPrograms(snapshot, joint.Order, joint.Expansions, token);
}
var search = FixedProgramSearch.Run(snapshot, token);
if (search.Status != CuttingPlanStatus.Ready)
return new(search.Status, findings: JointCuttingPlanSearch.Map(snapshot, search.Findings), expansions: search.Expansions);
return Replay(snapshot, search.Order, search.Expansions, token);
}
catch (OperationCanceledException)
{
return new(CuttingPlanStatus.Canceled);
}
}
// Fresh checker and full sequence replay: no cached branch state is accepted as evidence.
internal static CuttingPlanResult Replay(CuttingPlanSnapshot snapshot, IReadOnlyList order,
int expansions, CancellationToken token)
{
if (order.Count != snapshot.Placements.Count || order.Distinct().Count() != order.Count
|| order.Any(index => index < 0 || index >= snapshot.Placements.Count))
return new(CuttingPlanStatus.InvalidInput, expansions: expansions);
// Dependencies are rechecked from the captured graph, not trusted from the search.
if (snapshot.Dependencies.FirstViolation(order) is { } violation)
return new(CuttingPlanStatus.InvalidInput,
findings: [DependencyFinding(snapshot, violation, "The proposed order")], expansions: expansions);
var checker = snapshot.NewReleasedState();
var position = snapshot.StartPoint;
var distance = 0.0;
var findings = new List();
foreach (var index in order)
{
token.ThrowIfCancellationRequested();
var placement = snapshot.Placements[index];
findings.AddRange(checker.Check(placement.Execution, position, placement.SourceOrdinal + 1,
placement.IsCutOff, token));
distance += placement.Execution.RapidDistanceFrom(position);
position = placement.Execution.DeparturePoint;
}
token.ThrowIfCancellationRequested();
if (findings.Count != 0)
return new(findings.Any(f => f.Kind == PostVerificationKind.Incomplete)
? CuttingPlanStatus.UnsupportedGeometry : CuttingPlanStatus.ConstraintConflict,
findings: JointCuttingPlanSearch.Map(snapshot, findings), expansions: expansions);
return new(CuttingPlanStatus.Ready, order.Select(index => snapshot.Placements[index]),
expansions: expansions, rapidDistance: distance, independentlyReplayed: true);
}
///
/// The safest route the greedy walk finds, independently replayed; null when there is none.
/// Its planner-chosen rapids that still cross parts already cut raise the head
/// (): only here, after the strict search found no route avoiding
/// them. A route that then passes every check is Ready, listing its raises. One that does not
/// is an UNVERIFIED BestEffort carrying every violation, applicable only with explicit
/// acceptance. A route the replay cannot check completely, or with any motion into material
/// (a lead that does not fit, a cut-order or nested-lead conflict), is never offered: the
/// strict refusal stands.
///
internal static CuttingPlanResult PlanSafestRoute(CuttingPlanSnapshot snapshot, CancellationToken token)
{
if (snapshot == null || snapshot.Failure != null || !snapshot.Regeneration || snapshot.BestEffort
|| snapshot.Placements.Count == 0)
return null;
// The walk weighs every part it cuts rather than stopping at the first refusal: it gets
// twice the strict search's budget. Replay still checks it against the captured input.
var budget = (int)System.Math.Min(int.MaxValue, 2L * snapshot.ExpansionBudget);
var order = JointCuttingPlanSearch.SafestRoute(snapshot.WithSearchLimits(budget, snapshot.MaxEntries), token);
if (order == null)
return null;
try
{
order = RaisedRapids.Mark(snapshot, order, token);
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException)
{
// A route that cannot be read for marking keeps its crossings as findings.
}
var result = ReplayPrograms(snapshot, order, 0, token, safestRoute: true);
if (result.Status is not (CuttingPlanStatus.Ready or CuttingPlanStatus.BestEffort)
|| result.SafestRoute is { Severe: > 0 })
return null;
result.Snapshot = snapshot;
return result;
}
// Re-read EXACT selected programs with a fresh checker and native lead validation.
// No emission/regeneration or cached branch verdict is used here.
///
/// Return a route whose planner-chosen motions break checks as an UNVERIFIED BestEffort with
/// those findings rather than refusing it; incomplete checks are still refused.
///
internal static CuttingPlanResult ReplayPrograms(CuttingPlanSnapshot snapshot,
IReadOnlyList order, int expansions, CancellationToken token, bool safestRoute = false)
{
if (order == null || order.Count != snapshot.Placements.Count
|| order.Any(p => p == null || p.SourceOrdinal < 0 || p.SourceOrdinal >= snapshot.Placements.Count)
|| order.Select(p => p.SourceOrdinal).Distinct().Count() != order.Count)
return new(CuttingPlanStatus.InvalidInput, expansions: expansions);
if (snapshot.Dependencies.FirstViolation(order.Select(p => p.SourceOrdinal).ToArray()) is { } violation)
return new(CuttingPlanStatus.InvalidInput,
findings: [DependencyFinding(snapshot, violation, "The proposed order")], expansions: expansions);
var checker = snapshot.NewReleasedState();
var position = snapshot.StartPoint;
var distance = 0.0;
var findings = new List();
// Violations inside fixed emissions: accepted as given, reported, never refused (D1a).
// Capture adds the drawing pins it could not honor; their contours were planned normally.
var warnings = new List(snapshot.Findings);
// Rapids that raise the head over cut parts are listed, never refused.
var raises = new List();
var raisedRapids = 0;
var materialOwners = snapshot.Placements.Where(p => !p.IsCutOff).ToArray();
var materials = materialOwners.Select(p => p.Material).ToArray();
foreach (var proposal in order)
{
token.ThrowIfCancellationRequested();
var source = snapshot.Placements[proposal.SourceOrdinal];
if (!ReferenceEquals(source.SourcePart, proposal.SourcePart)
|| !SameBits(source.Location.X, proposal.Location.X) || !SameBits(source.Location.Y, proposal.Location.Y)
|| !SameBits(source.Rotation, proposal.Rotation) || source.LeadInsLocked != proposal.LeadInsLocked
|| !ReferenceEquals(source.Prepared, proposal.Prepared) || !ReferenceEquals(source.Material, proposal.Material)
|| source.IsCutOff != proposal.IsCutOff
|| snapshot.PreservePartOrder && proposal.SourceOrdinal != order.TakeWhile(p => !ReferenceEquals(p, proposal)).Count())
return new(CuttingPlanStatus.InvalidInput, expansions: expansions);
if (source.Prepared == null)
{
if (!ReferenceEquals(source, proposal) && !ReferenceEquals(source.Reversed, proposal))
return new(CuttingPlanStatus.InvalidInput, expansions: expansions);
}
else if (proposal.SelectedProgram == null
|| !proposal.SelectedProgram.Matches(source.Prepared, proposal.ContourChoices))
return new(CuttingPlanStatus.InvalidInput, expansions: expansions);
OwnedExecution execution;
bool complete;
try
{
execution = ExecutionMotionReader.Read(proposal.CopyProgram(), proposal.Location, position, token);
// A cutoff is an open cut with no material; it is retained exactly (checked above).
complete = source.IsCutOff
|| ContourProgramVerifier.Verify(execution, source.Material, proposal.SelectedProgram, token);
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException)
{
return new(CuttingPlanStatus.UnsupportedGeometry,
findings: [JointCuttingPlanSearch.Finding(source, PostVerificationKind.Incomplete, ex.Message)], expansions: expansions);
}
var isFixed = proposal.FixedMotions(execution);
var (strict, fixedFindings, raised) = checker.Check(execution, position, source.SourceOrdinal + 1,
source.IsCutOff, isFixed, token);
raises.AddRange(RaisedRapids.Notes(snapshot, source, raised));
raisedRapids += RaisedRapids.Count(raised);
findings.AddRange(JointCuttingPlanSearch.Map(snapshot, strict));
warnings.AddRange(fixedFindings.Select(f => FixedLeadWarnings.FromChecker(snapshot, source, f)));
var fixedLeads = new List();
var lead = source.IsCutOff ? new LeadPathValidationResult(true, true, null)
: LeadPathValidator.Check(execution, source.Material, materials, isFixed, fixedLeads, token,
pierceRadius: snapshot.PierceZoneRadius);
if (!lead.IsComplete || !lead.IsClear)
findings.Add(JointCuttingPlanSearch.Finding(source,
lead.IsComplete ? null : PostVerificationKind.Incomplete, lead.Reason));
var pins = proposal.PinsByMotion(execution);
warnings.AddRange(fixedLeads.Select(f => FixedLeadWarnings.FromLead(source, execution, f, materialOwners,
pins?[f.Motion])));
// A locked program's coverage (a tab gap, a partial or reversed cut) has no certified
// selection to compare with; a regenerated program's never escapes the strict rule.
if (!complete && source.Prepared == null && isFixed != null)
warnings.Add(FixedLeadWarnings.Uncertified(source));
else if (!complete && (findings.Count == 0 || safestRoute))
return new(CuttingPlanStatus.InvalidInput,
findings: [JointCuttingPlanSearch.Finding(source, null,
"Selected program does not preserve complete directed contours, certified tabs or selected entry/style geometry.")],
expansions: expansions);
// A part the support bed cannot hold level must be tabbed where the tab steadies it.
if (SupportTabCapture.Review(source, proposal.ContourChoices) is { } unsteady)
warnings.Add(unsteady);
distance += execution.RapidDistanceFrom(position);
position = execution.DeparturePoint;
}
token.ThrowIfCancellationRequested();
// Support tabs that cannot hold their part level are noted, never refused.
var notes = raises.Concat(order.SelectMany(SupportTabCapture.Notes))
.Concat(SupportTabCapture.Skipped(snapshot)).ToList();
if (safestRoute && findings.Count != 0 && findings.All(f => f.Kind != PostVerificationKind.Incomplete))
return new(CuttingPlanStatus.BestEffort, order, findings.Concat(warnings).Concat(notes).Distinct(),
expansions, distance)
{
SafestRoute = RouteSeverity.Of(findings.Distinct().Select(f => f.Kind)),
HeadRaises = raisedRapids,
};
if (findings.Count != 0)
return new(findings.Any(f => f.Kind == PostVerificationKind.Incomplete)
? CuttingPlanStatus.UnsupportedGeometry : CuttingPlanStatus.ConstraintConflict,
findings: findings, expansions: expansions);
// Every planner-chosen motion passed. Fixed-emission warnings make the proposal
// UNVERIFIED: never Ready, applicable only through explicit warning acceptance.
if (warnings.Count != 0)
return new(CuttingPlanStatus.BestEffort, order, warnings.Concat(notes).Distinct(), expansions,
distance, independentlyReplayed: true)
{ HeadRaises = raisedRapids };
return new(CuttingPlanStatus.Ready, order, notes, expansions: expansions,
rapidDistance: distance, independentlyReplayed: true)
{ HeadRaises = raisedRapids };
}
private static CuttingPlanFinding DependencyFinding(CuttingPlanSnapshot snapshot,
(int Part, int Prerequisite) violation, string subject)
{
var part = snapshot.Placements[violation.Part];
var prerequisite = snapshot.Placements[violation.Prerequisite];
if (part.SeparatesSheet)
return new(part.SourceOrdinal, part.SourcePart, prerequisite.SourceOrdinal, prerequisite.SourcePart, null,
$"{subject} cuts the sheet-separating cutoff (part {part.SourceOrdinal + 1}) before part "
+ $"{prerequisite.SourceOrdinal + 1}; every part and other cutoff must be cut before the sheet is separated.");
var reason = prerequisite.IsCutOff ? "the cutoff crossing it"
: snapshot.CommonLineOrder.Contains((violation.Part, violation.Prerequisite))
? "the common-line partner that cuts their shared line"
: prerequisite.SourcePart.BaseDrawing.CutFirst && !part.SourcePart.BaseDrawing.CutFirst
? "the cut-first part" : "the part nested in its cutout";
return new(part.SourceOrdinal, part.SourcePart, prerequisite.SourceOrdinal, prerequisite.SourcePart, null,
$"{subject} cuts part {part.SourceOrdinal + 1} before {reason} (part {prerequisite.SourceOrdinal + 1}).");
}
private static bool SameBits(double source, double proposed) =>
BitConverter.DoubleToInt64Bits(source) == BitConverter.DoubleToInt64Bits(proposed);
}