using System; using System.Collections.Generic; using System.Linq; using OpenNest.CNC; using OpenNest.CNC.CuttingPlanning; using OpenNest.CNC.CuttingStrategy; using OpenNest.Diagnostics; using OpenNest.Geometry; using OpenNest.Support; namespace OpenNest.Engine.CuttingPlanning; /// /// Caller-side direct-XY input. Regeneration requires explicit confirmed parameters. /// Keep sources/settings stable during Capture. Only plate-scoped requests () /// record the state that CuttingPlanService.Apply checks; a detached part list cannot be applied. /// public sealed class CuttingPlanRequest { public CuttingPlanRequest(IEnumerable parts, Vector startPoint = default, int expansionBudget = 20000, CuttingParameters confirmedParameters = null, IEnumerable eligibleParts = null, bool preservePartOrder = false, int maxEntries = 16) { Parts = parts == null ? null : Array.AsReadOnly(parts.ToArray()); StartPoint = startPoint; ExpansionBudget = expansionBudget; ConfirmedParameters = confirmedParameters; EligibleParts = eligibleParts == null ? null : Array.AsReadOnly(eligibleParts.ToArray()); PreservePartOrder = preservePartOrder; MaxEntries = maxEntries; } /// /// Plate scope: plans the plate's current parts and captures its exact state, so a Ready /// result can later be applied through CuttingPlanService.Apply. A factory rather than a /// constructor overload, so new CuttingPlanRequest(null) stays unambiguous. /// public static CuttingPlanRequest ForPlate(Plate plate, Vector startPoint = default, int expansionBudget = 20000, CuttingParameters confirmedParameters = null, IEnumerable eligibleParts = null, bool preservePartOrder = false, int maxEntries = 16) => new(plate?.Parts, startPoint, expansionBudget, confirmedParameters, eligibleParts, preservePartOrder, maxEntries) { Plate = plate, SupportBed = plate?.SupportBed, SupportQuadrant = plate?.Quadrant ?? 1 }; /// The plate scope, or null for a detached part list that cannot be applied. public Plate Plate { get; private init; } /// /// The bed the parts rest on once cut free (the plate's own for ), or /// null. With it and , each regenerated part the /// bed cannot hold level is tabbed where the tab steadies it (docs/support-tabs.md). /// public SupportBed SupportBed { get; init; } /// The sheet quadrant that places on the parts' coordinates. public int SupportQuadrant { get; init; } = 1; public CuttingParameters ConfirmedParameters { get; } public IReadOnlyList EligibleParts { get; } public bool PreservePartOrder { get; } public int MaxEntries { get; } internal Action ExpansionObserver { get; init; } public IReadOnlyList Parts { get; } public Vector StartPoint { get; } public int ExpansionBudget { get; } } /// Owned worker input. SourcePart references are identity handles only, never worker data. public sealed class CuttingPlanSnapshot { internal CuttingPlanSnapshot(IEnumerable placements, Vector startPoint, int expansionBudget, CuttingPlanStatus? failure = null, IEnumerable findings = null, bool regeneration = false, bool preservePartOrder = false, int maxEntries = 16, Action expansionObserver = null, PlateCuttingState plateState = null, CuttingParameters ownedParameters = null, CuttingDependencyGraph dependencies = null, bool bestEffort = false) { BestEffort = bestEffort; PlateState = plateState; OwnedParameters = ownedParameters; Placements = Array.AsReadOnly(placements.ToArray()); Dependencies = dependencies ?? CuttingDependencyGraph.Empty(Placements.Count); StartPoint = startPoint; ExpansionBudget = expansionBudget; Failure = failure; Findings = Array.AsReadOnly((findings ?? []).ToArray()); Regeneration = regeneration; PreservePartOrder = preservePartOrder; MaxEntries = maxEntries; ExpansionObserver = expansionObserver; tabMayNotHold = Placements .Where(p => p.Prepared is { Support.TabCanSupport: false, PerimeterTabbed: true }) .Select(p => p.SourceOrdinal + 1).ToHashSet(); } // Part numbers (source ordinal + 1, as every checker is keyed) of support-tabbed parts that no // single tab can steady. private readonly HashSet tabMayNotHold; /// /// An empty rapid checker for this capture. A support-tabbed part no single tab can steady may /// tip once cut, so its tabbed outside is an obstacle from then on, as an untabbed one is; /// rapids may still cross parts their tab holds level. /// internal ReleasedContourState NewReleasedState() => new(reportMissingLeadIns: false, tabMayNotHold.Count == 0 ? null : tabMayNotHold.Contains, CommonLineMembers.Count == 0 ? null : CommonLineMembers.Contains); /// /// Part numbers (source ordinal + 1) of the members of common-line groups cut once /// (docs/common-line-cutting.md): each member's open remainder cuts free its outside, which is a /// rapid obstacle once its program ends. /// internal IReadOnlySet CommonLineMembers { get; init; } = new HashSet(); /// /// The prerequisites that are common-line order (source ordinals: the member that cuts a shared /// line before its partner), for naming them in findings. /// internal IReadOnlySet<(int Part, int Prerequisite)> CommonLineOrder { get; init; } = new HashSet<(int, int)>(); /// Whole-part prerequisites by source ordinal (cutoffs, inner parts before hosts). internal CuttingDependencyGraph Dependencies { get; } /// Exact captured plate state for plate-scoped requests; null for detached part lists. internal PlateCuttingState PlateState { get; } /// Owned copy of the confirmed parameters taken at capture; never the caller's object. internal CuttingParameters OwnedParameters { get; } /// Every lead-in pierce keeps this from all material; 0 without a pierce diameter. internal double PierceZoneRadius => OwnedParameters?.GetPierceZoneRadius() ?? 0; internal bool BestEffort { get; } internal bool Regeneration { get; } internal bool PreservePartOrder { get; } internal int MaxEntries { get; } internal Action ExpansionObserver { get; } public IReadOnlyList Placements { get; } public Vector StartPoint { get; } public int ExpansionBudget { get; } internal CuttingPlanStatus? Failure { get; } internal IReadOnlyList Findings { get; } /// /// Support tabs are on but go by size (the request had no support bed, or its settings tab by /// size) and its settings set no size range, so capture tabbed no part; the plan carries a /// note saying so. /// internal bool SupportTabsSkipped { get; init; } // A worker-only search variant over the same owned capture. Never recapture the live plate: // both proposals retain the identical freshness record and confirmed settings. internal CuttingPlanSnapshot WithSearchLimits(int expansionBudget, int maxEntries) => new(Placements, StartPoint, expansionBudget, Failure, Findings, Regeneration, PreservePartOrder, maxEntries, ExpansionObserver, PlateState, OwnedParameters, Dependencies, BestEffort) { SupportTabsSkipped = SupportTabsSkipped, CommonLineMembers = CommonLineMembers, CommonLineOrder = CommonLineOrder, }; /// The same capture planned along its current part order. internal CuttingPlanSnapshot WithPreservedOrder() => new(Placements, StartPoint, ExpansionBudget, Failure, Findings, Regeneration, preservePartOrder: true, MaxEntries, ExpansionObserver, PlateState, OwnedParameters, Dependencies, BestEffort) { SupportTabsSkipped = SupportTabsSkipped, CommonLineMembers = CommonLineMembers, CommonLineOrder = CommonLineOrder, }; } public sealed class FixedProgramPlacement { internal FixedProgramPlacement(Part sourcePart, int sourceOrdinal, Vector location, double rotation, bool leadInsLocked, OwnedExecution execution, Program program = null, PreparedContours prepared = null, LeadMaterialSnapshot material = null, IReadOnlyList choices = null) { SourcePart = sourcePart; SourceOrdinal = sourceOrdinal; Location = location; Rotation = rotation; LeadInsLocked = leadInsLocked; Execution = execution; this.program = program; Prepared = prepared; Material = material; ContourChoices = Array.AsReadOnly((choices ?? []).ToArray()); } private readonly Program program; internal PreparedContours Prepared { get; } internal LeadMaterialSnapshot Material { get; } public IReadOnlyList ContourChoices { get; } public bool IsRegenerated => ContourChoices.Count != 0; /// Returns an independent deep copy; never an alias to captured/proposed code. public Program CopyProgram() => program == null ? null : OwnedProgramCopy.Copy(program); internal SelectedContourProgram SelectedProgram { get; private init; } /// /// The motions of (this placement's own program read with its /// incoming rapid first) that are a fixed emission, accepted as given in regeneration mode: /// everything a locked program does after the planner's rapid into it, and the lead-in, cut, /// lead-out and tab of every drawing-pinned contour in a regenerated program, whose /// (default: this proposal's) were emitted. The rapids between /// contours stay the planner's. Null when nothing is fixed. Cutoffs and unlocked programs a /// caller left ineligible keep their strict checks. /// internal Func FixedMotions(OwnedExecution execution, IReadOnlyList choices = null) { if (IsCutOff) return null; if (Prepared != null) { var pins = PinsByMotion(execution, choices); return pins == null ? null : motion => pins[motion] != null; } if (!LeadInsLocked) return null; var opensWithRapid = execution.Motions.Count != 0 && execution.Motions[0].Rapid; return motion => motion > 0 || !opensWithRapid; } /// /// For each motion of a regenerated program emitted from , the /// one-based drawing pin whose contour it cuts (lead-ins, cuts, lead-outs; never the rapid /// into the contour), or null. Null overall when no choice is pinned, or when the motions do /// not split into one group per choice after the scribes, so nothing is treated as fixed. /// internal int?[] PinsByMotion(OwnedExecution execution, IReadOnlyList choices = null) { choices ??= ContourChoices; if (Prepared == null || !choices.Any(c => Prepared.IsPinned(c.ContourOrdinal))) return null; var pins = new int?[execution.Motions.Count]; var breakCuts = BreakCutMotions.Mask(execution.Motions); var tabbedPieces = Prepared.TabbedPieceCount; var group = -1; var opening = true; // Inside the rapids that open a group. int? pin = null; for (var index = 0; index < execution.Motions.Count; index++) { var motion = execution.Motions[index]; if (motion.Rapid) { if (!opening) opening = true; continue; } // Break cuts are emitted after the scribes and before any contour. if (breakCuts[index]) { if (group >= 0) return null; opening = true; continue; } if (opening) { opening = false; // Scribes are emitted first, before any contour. if (motion.Layer == LayerType.Scribe) { pin = null; if (group >= 0) return null; continue; } // Then the tabbed cutouts' pieces, one lead-in and cut group each. if (tabbedPieces > 0) { pin = null; if (group >= 0) return null; tabbedPieces--; continue; } // A common-line perimeter, cut last, is several groups: its shared cuts, then its pieces. if (++group >= choices.Count) { if (!Prepared.IsCommonLinePerimeter || choices[^1].ContourOrdinal != Prepared.PerimeterOrdinal) return null; group = choices.Count - 1; } pin = Prepared.PinNumberOf(choices[group].ContourOrdinal); } pins[index] = pin; } return group == choices.Count - 1 ? pins : null; } internal FixedProgramPlacement Propose(Program proposed, OwnedExecution execution, IReadOnlyList choices, System.Threading.CancellationToken token = default) { SelectedContourProgram selected = null; try { // The expected program is constructed from owned choices/settings, NEVER // from proposed or its cached execution. Counterfeit payloads remain subject // to independent final checks, including callers of this internal test seam. selected = Prepared?.CaptureSelectedProgram(choices, Location, token); } catch (Exception ex) when (ex is ArgumentException or NotSupportedException or InvalidOperationException or ArithmeticException) { // Invalid metadata is a refused proposal, not an exception from final replay. } return new(SourcePart, SourceOrdinal, Location, Rotation, LeadInsLocked, execution, OwnedProgramCopy.Copy(proposed, token), Prepared, Material, choices) { SelectedProgram = selected, IsCutOff = IsCutOff, SeparatesSheet = SeparatesSheet }; } /// A cutoff: always a fixed open-cut program, never material or regenerated. public bool IsCutOff { get; internal init; } /// A cutoff that splits the whole sheet; it is cut after every other placement. public bool SeparatesSheet { get; internal init; } /// /// For a cutoff segment reaching the far sheet edge with a head-locked overcut, the same /// segment cut the other way at that edge (); the search /// may propose it in place of this placement. Null for everything else. /// internal FixedProgramPlacement Reversed { get; init; } /// Whether this proposal cuts its cutoff segment the other way from the placed program. public bool ReversesCutOff { get; internal init; } public Part SourcePart { get; } public int SourceOrdinal { get; } public Vector Location { get; } public double Rotation { get; } public bool LeadInsLocked { get; } public OwnedExecution Execution { get; } } public enum CuttingPlanStatus { Ready, ConstraintConflict, UnsupportedGeometry, InvalidInput, NoSolutionWithinBudget, Canceled, /// Usable owned programs, but geometric safety checks did not pass. Never Ready. BestEffort } /// Ordinals are zero-based source positions, not proposed sequence positions. public sealed record CuttingPlanFinding(int? SourceOrdinal, Part SourcePart, int? OtherSourceOrdinal, Part OtherSourcePart, PostVerificationKind? Kind, string Message) { /// /// The check's own finding, for its world-coordinate markers (location, rapid, contacts). /// Its part numbers are check positions, not source ordinals. Null when the check gave none. /// Not part of equality: repeated rejections of the same problem stay one finding. /// public PostVerificationFinding Witness { get; init; } /// /// For a warning, what the fixed emission is and /// what it hits; null otherwise. Not part of equality (the message already names both). /// public FixedLeadWarning FixedLead { get; init; } public bool Equals(CuttingPlanFinding other) => other is not null && SourceOrdinal == other.SourceOrdinal && Equals(SourcePart, other.SourcePart) && OtherSourceOrdinal == other.OtherSourceOrdinal && Equals(OtherSourcePart, other.OtherSourcePart) && Kind == other.Kind && Message == other.Message; public override int GetHashCode() => HashCode.Combine(SourceOrdinal, SourcePart, OtherSourceOrdinal, OtherSourcePart, Kind, Message); } /// /// A violation inside a fixed emission, accepted as given rather than refused (D1a); the part is /// the finding's source part. is one-based in the part's program /// (null when the whole program is concerned); is the one-based drawing /// pin, or null for a locked program; is the other part hit, /// or null for the part's own material, a released contour named in the message, or an /// uncertifiable gap. /// public sealed record FixedLeadWarning(int? ContourNumber, int? PinNumber, int? ObstacleSourceOrdinal); /// /// 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. BestEffort is explicitly /// unverified and requires batch warning acceptance; other failures contain no proposals. A /// BestEffort result that is also passed every strict check /// except inside fixed emissions, whose violations are its FixedLead warnings. /// public sealed class CuttingPlanResult { internal CuttingPlanResult(CuttingPlanStatus status, IEnumerable order = null, IEnumerable findings = null, int expansions = 0, double rapidDistance = 0, bool independentlyReplayed = false) { Status = status; ProposedOrder = Array.AsReadOnly((order ?? []).ToArray()); Findings = Array.AsReadOnly((findings ?? []).ToArray()); Expansions = expansions; RapidDistance = rapidDistance; IndependentlyReplayed = independentlyReplayed; } public CuttingPlanStatus Status { get; } public IReadOnlyList ProposedOrder { get; } public IReadOnlyList Findings { get; } public int Expansions { get; } public double RapidDistance { get; } public bool IndependentlyReplayed { get; } /// /// The violations of a safest-route fallback: no route passed every check within the search /// limit, so this BestEffort is the replayed route that breaks the fewest. Null otherwise. /// internal RouteSeverity? SafestRoute { get; init; } /// Whether this is a safest-route fallback rather than a route that passed every check. public bool IsSafestRouteFallback => SafestRoute != null; /// /// The rapids that raise the head over parts already cut, each listed in . /// Only a safest-route fallback marks them, when no search kept every rapid head-down. /// public int HeadRaises { get; init; } /// The captured input this result was planned from; binds Apply to its freshness record. internal CuttingPlanSnapshot Snapshot { get; set; } }