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); }