Files
OpenNest/docs/cutting-planner.md
T

411 lines
27 KiB
Markdown

# Unified cutting planner: direct-XY proposals
`OpenNest.Engine.CuttingPlanning.CuttingPlanService` plans contiguous whole-part
programs. It can retain fixed programs or jointly choose internal contour order,
entries and whole-part order using explicitly confirmed cutting parameters. It
returns an owned proposal; `Apply` installs Ready plate-scoped proposals atomically
after an exact freshness check. The desktop opens it from `Plate > Plan Cutting...` and
`Nest > Plan Cutting (All Plates)...` (see [Desktop workflow](#desktop-workflow)); the older
automatic sequencing and lead-in assignment commands remain until they are retired.
## Capture before worker planning
Create a `CuttingPlanRequest`, then call `Capture` while source placements,
programs and settings are stable. Pass that snapshot to `Plan` on a worker;
`Plan(request)` combines these steps synchronously. The modeled starting point
defaults to `Vector.Zero`, not a discovered controller position.
```csharp
var request = CuttingPlanRequest.ForPlate(plate, startPoint: start,
confirmedParameters: parameters, expansionBudget: 20000,
maxEntries: 16, preservePartOrder: false);
var snapshot = CuttingPlanService.Capture(request, cancellationToken);
var result = CuttingPlanService.Plan(snapshot, cancellationToken);
```
- A plate-scoped request (`CuttingPlanRequest.ForPlate`) plans the plate's current
parts and records its exact state for `Apply`. A detached part list (`new CuttingPlanRequest(parts, ...)`)
plans the same way but can never be applied. An empty plate is a Ready no-op.
- Omitting `confirmedParameters` preserves the original fixed-program contract:
locked and unlocked programs stay fixed; only whole-part order may change.
- Supplying confirmed parameters enables regeneration for unlocked placements.
`eligibleParts` can restrict it to an explicit reference-based subset; an empty
subset retains all programs but still checks their leads against owned material.
Locked placements never regenerate. Foreign or duplicate eligible identities
are invalid; eligibility without confirmed parameters is invalid.
- `preservePartOrder` fixes whole-part order, not eligible internal contour choices.
- Parameters are caller-confirmed inputs. The service does not recover missing
operator settings or silently change lead styles to find a solution.
Capture owns clean geometry, placed programs and required settings. Source `Part`
references are identity handles only: workers never read their mutable state.
Clean geometry accounts for the base program's existing rotation before applying
placement rotation; placement translation is applied once. Subprogram copying
must not rotate shared programs through their property setters. No live drawings
are attached to preview plates, so capture/search do not change quantity accounting.
Planning works from this historical snapshot; Apply compares it with live state.
Original
clean and executable graphs are type/mode-checked before cloning can erase unknown
semantics. Exact placed/proposed copies preserve authored motion feed/exact-stop
flags, symbolic bindings and shared subprogram identity; unsupported graphs are
refused. The geometry-only clean transform uses per-parent copies so legacy rotation
does not visit a globally shared descendant twice; it never changes the fixed payload.
## Cutting dependencies
Capture builds whole-part prerequisites from owned values, and both the search and
the final replay enforce them:
- A cutoff precedes every part its nominal line crosses within its span, using the
same rule as automatic sequencing: nominal position and limits against the part's
placed bounds, matched by drawing reference, never by name or trimmed segments. A
cutoff whose definition is missing precedes every part. Cutoffs need a
plate-scoped request; they are always fixed programs, need no lead-in, are not
material for lead validation and, being open cuts, never become rapid obstacles.
Rapids into and out of them are still checked.
- A part whose perimeter lies strictly inside a cutout of another part precedes
that host. Material bounds (never rapids or scribe marks) only select candidate
pairs; containment is proven on native clean material. Touching or crossing boundaries, or material that cannot be
captured, refuse as `UnsupportedGeometry` naming both parts. A part in a concave
pocket outside the host's material has no dependency.
- A preserved manual order that violates a prerequisite, or a cycle, is a
`ConstraintConflict`. Replay rechecks the captured prerequisites and refuses a
violating order rather than trusting the search.
## Search and exact output
With regeneration, the bounded deterministic search plans internal contour order
and native entry candidates part by part along a whole-part order. Internal
contours precede their own perimeter; parts remain contiguous. Backtracking can
revisit an earlier entry when a later part cannot be reached safely. For multi-part
requests containing regenerated holes, it first tries one ranked hole chain for
each outside endpoint. This lets a later blocked approach change the previous
part's departure without first exhausting combinations of its earlier holes.
This preferred pass uses the same emitted-motion checks, shared expansion budget
and stall limit as the retained search; on failure the full entry/hole-order
backtracking pass remains available. Single-part and no-hole requests keep their
existing search order.
A preserved order is followed as given. Otherwise the order is an open
travelling-salesman path over part centres from the start point: nearest neighbour,
then 2-opt reversals and Or-opt moves of one to three parts, never placing a part
before a cutoff or nested-part prerequisite. If a part on that order cannot be
reached without crossing parts already cut, the search learns "cut this part
before those", backs up to just before the earliest of them and re-plans the rest
from the tool position there; parts cut before that point are kept. An attempt
stops backtracking after a stall of 8 x entries x contours expansions without
getting further, so it learns instead of retrying every entry combination of the
parts before it. A rule that would contradict an order already required is skipped.
"Cut before" rules are a heuristic (a part blocked straight after another may be
reachable via a third), so once nothing new can be learned the remaining budget
goes to a full search that tries every ready part, nearest first; only when that
also fails is the result a refusal.
Candidates use native closest points, vertices, midpoints and circle angles in
stable order, capped by `maxEntries`. Circle rounding, clamping, corner resolution
and tab trimming happen during emission. Validation uses the actual emitted
motions, never the nominal entry point alone. Existing lead styles are not
shortened, disabled or substituted as a search fallback.
Whole-circle candidates rank by distance to their next-cut target; for a hole,
that is the next contour's actual pierce. They do not reward a diagonal point as
if it were a bounding-box corner. All eight
compass options remain available, including the four polar points at 0°, 90°,
180° and 270°, and every alternative still passes the emitted-lead and rapid checks.
Without a next-cut target, the arrival-based rule is unchanged. Polygon corner
preferences are unchanged.
`Round Lead-In Angles` remains an explicit cutting setting: a 90° increment snaps
circular-hole starts to the four polar directions; 45° also permits diagonals.
Snapping can increase reuse of identical hole subprograms and reduce output for
posts that support that reuse, at the cost of a less direct departure. It is not
a clearance exemption or a guaranteed file-size reduction. The planner does not
silently enable rounding, change the increment, or bypass checks on the rounded
motions.
Every candidate rapid is checked against contours already completed, including
earlier holes in the same part. Future contours are not yet obstacles. Rapid
checks skip completed contours whose extents are more than 0.001 clear of the
rapid, ten times the widest band any native contact query allows beyond an
extent; anything closer, touching included, gets the full native check. Lead
checks examine every other part's material: native line/circle queries can report
rounding contacts for long leads far from small circles, and leads keep those
results. An arc's
extent is its whole supporting circle widened to the distance at which the native
contact query still counts it as touched (for a very small arc up to 0.0001 beyond
its radius). Nothing is skipped when either extent has a nonfinite bound or reaches
beyond 1e6, where rounding of large supports can exceed any fixed margin. Actual
lead-in and lead-out line/arc paths must stay in target scrap and avoid other
placed material; holes in other parts remain scrap. Tangent/coincident contacts
outside the genuine target contour joint and numerically uncertain queries refuse.
Material capture supports a simple closed perimeter minus disjoint, non-nested
holes; unsupported topology is not a bounding-box approximation. A line meeting a
tangent arc at a shared vertex, such as a fillet, is an ordinary joint: an exact
contact that the native query rounds away is not uncertain at a line endpoint the
other curve already touches, while a contact anywhere else on the line still refuses.
Source parts rank by modeled travel (material-centre distance at a holed-part
boundary); within a part, preferred contour order and facing-entry rank precede
travel. Ties use stable source/contour/entry ordinals. Hash values and drawing names
are not tie breakers.
When the next whole part is fixed by the tour or a part is already active, the
search generates and checks contour alternatives on demand in contour/entry rank
order. It does not emit every sibling before trying the first. Backtracking still
visits the remaining alternatives when needed; none is pruned. The free whole-part
fallback still evaluates all ready sources before ranking their travel distances.
The expansion budget counts evaluated candidates and frontier ranking, including
rejections, before emission; it is not a wall-clock timeout. An unvisited sibling
consumes no additional DFS-prefix emission work or per-sibling expansion charge;
entry-catalogue prechecks still emit isolated contours and consume budget.
Consequently a bounded search can
reach a different frontier and report different rejected approaches than eager
expansion did. All visited candidates retain their lead/rapid checks, and a selected
plan still receives fresh independent replay. Callers can cancel. Exhaustion returns
a refusal, not an unranked fallback or proof of geometric impossibility.
## Automatic outside entries and look-ahead
An unlocked part's outside entry is chosen automatically toward the NEXT cut: the ranker orders the native candidate
catalogue by the facing side(s) of the next part's placed-material centre, and
the shared lead validator certifies each emitted lead lazily until up to
`maxEntries` feasible candidates remain. At caps of four or more, a corrective
scan reconsiders memoized clear candidates for each missing side before evaluating
more of the catalogue; a later side cannot lose a usable point merely because
an earlier side's scan passed it. Replacements preserve other covered sides and
global rank, never exceed the cap, and stop evaluating the tail once coverage
settles. Smaller caps retain rank priority rather than promising all-side coverage.
The next cut is the next unfinished part in a supplied order — recomputed
after every learned-order replan — or, in the full fallback search, the nearest
dependency-ready remaining part, stable-ordinal ties; the last part has no
target and ranks by tier then distance to the tool's arrival. Between source
parts the tour stays nearest-first; the look-ahead rank only orders the entries
inside one part's contour stage, so distance sorting cannot undo the facing.
Uncertain (numerically incomplete) validator answers are not geometric refusals:
those candidates can fill remaining retained slots for emitted-prefix checking and
complete replay. The total retained entry count stays within `maxEntries`. A
part/contour with no fitting lead in its fully evaluated catalogue is reported
as "No tested lead-in fits on part N, contour M"; budget exhaustion stays a
budget finding and incomplete checks are never presented as geometric
impossibility. Lead prechecks are reported separately and their requests also consume
the shared expansion budget before native work.
For a holed part, the search chooses an outside endpoint before cutting any hole.
Each endpoint branch builds an open hole-centre route from the original arrival to
that endpoint, using bounded nearest-neighbour, 2-opt and Or-opt improvement.
Preferred hole entries are then resolved backward from the outside's actual emitted
pierce, ignoring scribe marks: each hole faces the following contour's actual pierce.
Convex corners lead the preference tiers, then straight midpoints/tangent joints,
then native fallbacks. Reflex/cusp corners remain manual-only.
This preference orders the search; it never certifies a rapid or prunes alternate
retained entries or hole orders. A different outside endpoint recomputes its hole
preference. Every standalone emitted prefix is replayed from the original part
arrival and a copy of the checker from before that part, not from the previous
prefix (which would double-consume holes and scribes). Holes are cut once, the
outside last, and scribes once. Locked programs remain exact. A crossed preferred
route must recover through checked backtracking or return a refusal, never unsafe
`Ready`.
Selected programs are replayed from the beginning with a fresh checker and fresh
lead validation, without regenerating them or trusting cached search verdicts.
Before replay, expected-emission geometry is independently built from the owned
choices/settings, not from the candidate payload. Replay checks actual selected
code against it and independently accounts for directed native boundary coverage:
no partial, duplicated, retraced or reversed cuts, except the exact selected tab.
Equivalent subdivisions and merged collinear moves remain valid. Captured source
identity and pose binding use exact scalar bits, not geometric tolerance.
Arrival positions use actual departures, including lead-outs and subprograms.
`ProposedOrder` retains source identities/poses; `CopyProgram()` returns an
independent deep copy of each exact captured/generated program. `ContourChoices`
are nominal choice metadata, not a substitute for reading actual execution.
Neither obtaining a proposal nor copying its programs installs them on live parts.
## Results and refusal
`Ready` and `IndependentlyReplayed` describe the modeled proposal only. In the
no-parameter fixed route, replay checks rapid crossings, missing leads and
incomplete retention; it does not add regeneration-mode material/lead checks.
In regeneration mode, replay also checks actual lead paths and contour accounting
against owned clean material. Neither mode certifies final NC, production cutting
readiness or physical machine safety.
Findings and source ordinals use the original zero-based source list, not proposed
sequence positions. Strict service refusals contain no proposed order. The desktop batch may
produce a separate `BestEffort` proposal as described below; it never labels it `Ready` or
sets `IndependentlyReplayed`.
- `ConstraintConflict`: fixed programs or explored fixed routing violate the
modeled constraints. Locked internal crossings cannot be repaired by regeneration.
- `UnsupportedGeometry`: unsupported motion/material semantics or an incomplete
check. Open nominal outlines, ambiguous release states or containment, cutoffs
in a detached part list and scribe-only source drawings are not silently accepted.
- `InvalidInput`: malformed/missing/duplicate placements or settings, invalid
geometry, empty input, invalid eligibility or nonpositive bounds.
- `NoSolutionWithinBudget`: the bounded/capped search found no complete proposal;
it does not prove no possible geometric route exists.
- `Cancelled`: capture, search or replay cancelled without live mutation.
Malformed original executed graphs are refused, not salvaged. Valid but incomplete
old programs can regenerate from clean geometry. Regenerated programs retain genuine
configured tab gaps; stale tab settings do not establish retention. In confirmed-
parameters mode, locked/ineligible programs must cover the complete directed clean
boundary: an open fixed program has no certified selected tab metadata and is refused,
not repaired, even if it may have been intentionally tabbed. The no-parameter route
retains its narrower compatibility contract. A lead-out that may bridge a tab, or
a malformed emitted arc, is refused, not automatically repaired. Tabbed lead-outs
leave from the trimmed cut end, but a lead-out after an open contour still needs
manual review of its retention gap, so confirmed-parameters planning refuses it.
## Apply
```csharp
var commit = CuttingPlanService.Apply(results, cancellationToken); // one result per plate
```
Call it on the thread that owns the plates, with Ready, independently replayed
results from plate-scoped requests; anything else is `InvalidInput`. Apply never
replans. Each plate is compared exactly with the state captured with its request:
part list instance and order, plate quantity/size/quadrant and settings, cutoff
definitions, and for every part its program reference and exact content (an
in-place edit counts), drawing program and cutoff classification, pose bits,
lead-in/lock flags, settings (reference and exact content) and bounds. Any
difference on any plate returns `Stale` and changes nothing, so a proposal that
changes a plate can be applied once; an unchanged (no-op) proposal stays current.
A malformed live program is also `Stale`, not an exception. A part repeated on
two plates of one scope is `InvalidInput`. Caller-confirmed planning settings are
input, not plate state: editing a separate confirmed-settings object after
capture does not stale the result (confirmed settings that are also a part's or
the plate's live settings are live state, and editing them does). Settings are
compared member by member, and only the exact built-in settings types are
supported (the same set regeneration copies): a plate-scoped request whose part
or plate settings, or any lead-in, lead-out, tab, sequencing or assignment
object inside them, is another type (a subclass included) returns
`UnsupportedGeometry` without running that type's code. A settings object
replaced by such a type after capture makes `Apply` return `Stale`. Detached
part-list requests do not capture settings and are unaffected.
The whole scope is validated and its bounds staged first; cancellation is checked
immediately before the install. Order changes through `ObservableList.Reorder`
semantics: same references, no `PartAdded`/`PartRemoved`, so drawing quantities,
sentinel plates and plate lists are untouched. Regenerated parts receive a fresh
owned copy of the replayed program and of the settings captured with the request,
keep their pose and lock, and are marked as having lead-ins. Fixed programs are
not replaced. An exception during install restores every plate exactly and returns
`Failed`. The installer itself is internal: it trusts these owned payloads and
checks only root program references, so `CuttingPlanService.Apply` is the only
public path. After the whole scope is installed, each changed plate raises
`Plate.PartsReordered` once; an observer exception is reported in `RefreshErrors`
on an `Applied` result, not as a rollback.
## Desktop workflow
`Plate > Plan Cutting...` plans the active plate and `Nest > Plan Cutting (All Plates)...`
plans every plate that has parts. Both open one dialog built on
`OpenNest.Engine.CuttingPlanning.CuttingPlanBatch`:
- The dialog starts from the plate's cutting settings (or the last-used settings) and plans
at once. `Cutting Settings...` edits them and `Keep the current part order` fixes the
whole-part order; either change replans. The settings are confirmed parameters: every
unlocked part's lead-ins are regenerated. Locked parts keep their exact programs;
the strict route checks them, while any best-effort warnings require explicit review below.
- A missing or zero-length lead-in is reported directly, rather than as a search-limit
failure. Open `Cutting Settings...`, choose a lead-in other than `None` with a nonzero
length on the affected `External`, `Internal`, or `Arc / Circle` tab, then replan.
Locked programs require manual lead editing or unlocking before regeneration.
When a lead hits another part, the finding suggests more spacing or a shorter lead;
when no tested entry fits, it suggests reducing lead-in length and, if neighbours
obstruct it, spacing the parts farther apart. These are suggestions, not guaranteed
fixes: replanning runs the same strict checks; an unverified fallback requires
separate warning acceptance and is not approval to post or cut.
- The desktop free-order batch first tries up to eight entries per contour for at most
1,000 expansions. Only a Ready proposal that passes independent replay is used;
otherwise it runs the original 16-entry search with its entire plate budget.
Cancellation stops both attempts. This short pass can choose a different valid
route, and a hard plate can take up to 1,000 extra expansions before the full
attempt. The current-order retry still uses its original 16-entry budget.
Returned expansion counts describe the retained attempt, not a discarded
short pass plus its full-cap retry.
- Every plate is captured on the UI thread and checked and planned on a worker. Clean part
material is checked for overlaps with the pre-post overlap analyzer; known overlapping parts
still block that plate whatever its route. Incomplete checks remain visible warnings, never
a clear result (see [pre-post verification](post-verification.md)). A free-order search that ends
`NoSolutionWithinBudget` is retried once with the current part order, and the summary
says the order was kept. Both are allowed 400 expansions per part (at least the
default 20000), because both still plan contour order and entries for every part.
- The summary lists every plate: ready plates with part counts and rapid travel, others
with their status and findings. Unverified proposals show every overlap and route
warning in the scrollable summary before the acceptance checkbox is used; they do
not truncate later parts' warnings. Finding part numbers are the plate's current order, as the
editor numbers them. The preview shows the active plate detached from the nest (quantity
zero, so drawing quantities do not change) in the proposed order with the proposed
programs. Ready and best-effort proposals can be previewed only while the plate still
matches capture. Best-effort previews are labelled `UNVERIFIED`. Refused program graphs
are never copied, and changed plates require replanning.
- Apply requires usable output for every plate and remains all or nothing. A ready batch
can apply immediately; an unverified batch requires the unchecked, per-proposal
`I reviewed the warnings. Apply this unverified plan.` checkbox. Replanning clears it.
Both use the same owned-program, freshness and rollback boundary. After it applies, each plate keeps its own copy of the
confirmed settings, which also become the saved defaults. `Stale` keeps the dialog open
and asks for a replan; nothing changes.
- Closing or cancelling while planning cancels the worker and keeps the dialog open until
it stops. Planning and Apply refuse to start while a nesting or plate operation runs.
The dialog plans with its own copy of the settings, and posts progress and results to
the thread it was created on rather than to whichever context is current.
- `PlateView` follows `Plate.PartsReordered`: it redraws parts in the plate's order (the
numbers it draws are the cutting order), rebuilds their graphics and marks the overlap
check out of date. Both the editor and preview build outlines and lead paths in the
drawing-local frame, then apply the part placement once. Absolute (G90) programs
therefore follow moves and clones just like incremental (G91) programs; displaying a
part does not rewrite its program or coordinate mode.
### Best-effort fallback for imperfect geometry
When strict planning reports unsupported or incomplete geometry, the desktop batch
automatically attempts a bounded, deterministic best-effort proposal. Touching, intersecting
or numerically uncertain material boundaries need not prevent lead-in generation when their
closed executable contours are readable. The fallback reuses the existing contour emitter:
internal contours first, the largest bounding perimeter last, nearest entry from the preceding
departure, with the confirmed lead styles. It retains source part order except to satisfy
proven cutoff and nested-part prerequisites. `Keep the current part order` still refuses an
order that contradicts a proven prerequisite. Uncertain containment is named as a warning.
No source contours are repaired, removed, simplified, or silently closed. Locked programs
and cutoffs remain exact. Unknown/null/recursive instruction graphs, suppressed or nonfinite
motions and contours the emitter cannot represent still refuse. This fallback is for
incomplete geometry checks, not every constraint conflict or exhausted search. Known
inter-part material overlap still blocks the entire batch.
The result retains strict refusal findings and available rapid-check findings, and is
explicitly not a certificate of lead clearance, rapid travel or material containment.
Inspect the preview and warnings before accepting it. A route with only an incomplete
overlap check may likewise be accepted with warnings. `CanApply` and parameterless batch
`Apply()` remain strict; `CanApplyWithWarnings` and `Apply(acceptWarnings: true)` are the
explicit review path. `CuttingPlanService.Apply` still rejects best-effort results directly.
Acceptance never skips the separate pre-post checks or grants posting consent.
## Remaining integration boundaries
The service does not establish clean-material non-overlap, scrap release by open
cutoff cuts or sheet edges, or physical retention strength. It does not write CNC
or set posting consent. A `Ready` proposal can still be unsuitable for cutting.
Later slices route the lead-in side panel's automatic assignment through the planner
and retire the legacy automatic paths. Windows interaction,
supplied-job coverage and actual posted order remain separate acceptance gates.
Fresh [pre-post verification](post-verification.md) is still required; it is not a
physical safety qualification.
## Portable regression gate
```sh
dotnet test OpenNest.Tests/OpenNest.Tests.csproj -c Release --filter 'FullyQualifiedName~CuttingPlanning|FullyQualifiedName~PostVerificationAnalyzerTests'
```
Synthetic fixtures exercise whole-part routing and internal-hole crossing repair,
locked/ineligible refusal, actual native lead paths, shared subprograms, ownership,
entry/whole-part backtracking, deterministic budgets, cancellation and fresh replay
rejection. Retained emission characterizations cover styles, winding, corner rules,
circle rounding/clamping and tabs; unsupported cases remain explicit refusals.