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