Commit Graph
32 Commits
Author SHA1 Message Date
aj 7e887921ea fix(cutting): expose every unverified warning before consent 2026-10-08 13:50:12 -04:00
aj 81f3c006ab merge: integrate best-effort cutting on current master 2026-10-08 13:34:05 -04:00
aj fc300705a8 fix(cutting): aim round cutouts toward the next cut 2026-10-07 20:55:40 -04:00
aj 1c923976a8 fix(cutting): explain missing leads and suggest clearance remedies 2026-10-07 17:50:31 -04:00
aj f4d5cfe98e fix(ui): translate absolute-coordinate part paths by placement
Build paths in the drawing-local frame and translate both ordinary and split cut/lead paths once, without changing CNC programs. Add native Windows regressions using the operator's synthetic G90 fixture and incremental controls. Four absolute-mode cases fail before the fix; all seven pass after.
2026-10-07 17:22:06 -04:00
aj 9196d9157f fix(cutting): reuse clear candidates across side coverage scans 2026-10-07 16:05:25 -04:00
aj 64459f7bc8 fix(cutting): try endpoint alternatives before hole combinations 2026-10-07 15:21:09 -04:00
aj 414e9b4e75 fix(cutting): prefer hole entries facing the next contour 2026-10-07 14:58:23 -04:00
aj d4544ea358 fix(cutting): aim perimeter entries toward the next part
No-hole parts now choose the outside entry through the S03-S08 pipeline
at the part boundary: rank the native catalogue toward the NEXT cut's
placed-material centre, lazily certify each emitted lead with the shared
validator, cap at maxEntries with side coverage. The next cut is the
next unfinished part on the supplied order (re-read after every
learned-order replan) or, in sequence-free fallback, the nearest
dependency-ready remaining part with stable ordinal ties; the last part
has no target and ranks by tier then arrival distance — never the plate
origin. Target and arrival are converted to prepared LOCAL coordinates
exactly once; geometry is already rotated.

The look-ahead rank survives Follow's ordering: children sort by
nearest source first (the sequence-free tour stays nearest-first), then
contour, then the selection rank — plain OrderBy(Distance) can no longer
undo the facing. Measured fixture (three squares, 0.15 leads, origin
start): legacy cut every sheet at its arrival-nearest lower-left corner
with 10.5-unit cross-sheet rapids totalling 21.0; look-ahead cuts the
+X-facing corner with the same 21.0 total but each inter-part rapid now
starts at the facing edge instead of trailing across the whole sheet.

Uncertain validator answers are never precheck-refused: they skip the
selected slots but flow to the emitted-prefix Check and complete replay,
which stay the authority — a refused-looking incomplete is reported,
not hidden. A fully evaluated catalogue with no fitting lead surfaces
'No tested lead-in fits on part N, contour M'; budget exhaustion stays a
budget finding. Lead prechecks count separately from DFS expansions; the
pipeline itself is one counted expansion per contour. Full backtracking,
dependencies, locked/fixed programs, cutoff handling, emitted-prefix
Check and complete replay are unchanged; budgets and CuttingPartOrder
untouched. Hole parts keep the legacy path until S12.
2026-10-07 11:01:08 -04:00
aj 12206ed8fd feat(cutting): offer unverified best-effort plans for imperfect geometry 2026-10-06 14:44:53 -04:00
aj 18a61717fe fix(cutting): accept tangent line-arc joints in material capture
Plan Cutting refused ordinary filleted parts with "Native contact query is
numerically uncertain." Material capture checks every curve pair of a ring,
adjacent ones included. Where a line meets a tangent arc at their shared
vertex, rounding can drop the tangent root of the native line/circle
quadratic; the exact ray cast from the line's far end then reached the
vertex, which is already recorded as an endpoint contact, and was read as a
contact the native query missed. Rounded rectangles rotated off-axis were
refused 1037 times in 1080 before this change and 0 times after.

The exactness rays now stop short of a line endpoint the other curve already
contains (half-way from each end when both are contained), so together they
still cover every other point of the line and any unrecorded contact still
refuses. The native kernel and tolerances are unchanged.

Regressions: four tangent-fillet rings and a rotated filleted part planned
through CuttingPlanBatch (red before, green after); a line ending inside a
small circle's contact band stays uncertain in both directions. Mutations that
treat either endpoint as always contained, drop the start ray, or count
recorded endpoints as native contacts all fail.

Project Memory: 41880037
2026-10-06 10:37:07 -04:00
aj ea1270e1e7 fix(cutting): check every lead against all other material again
The second delta review compared the filtered checks with the previous
implementation on 5,400 generated cases. Rapid checks matched in every
case, but 114 lead checks differed: a long lead passing a small circle
or arc well over 0.001 away was reported clear, while the native
line/circle query, through rounding in its squared terms, reports a
contact there. A coordinate limit and a fixed margin cannot bound that
cancellation.

Lead checks therefore examine every other part's material again,
exactly as before the filter; LeadMaterialSnapshot no longer keeps an
extent. Rapid checks, including the pre-post review's, keep skipping
completed contours more than 0.001 clear of the rapid. The 900000-long
lead beside a radius-0.0001 circle is a regression test.

Planning a dense 144-part grid now takes about 19 s again (lead checks
dominate); a new part order is still found where the old search gave
up.
2026-10-06 00:47:46 -04:00
aj 8c21c62ec3 fix(cutting): skip checks only for well-clear, well-conditioned extents
The delta review found two more ways the extent filter could skip a
check a native query would have flagged:

- Native line intersections accept points 0.00001 outside each line's
  bounding box, so a lead 0.000003 from another part touched it while
  their extents were 0.000003 apart, beyond the 1e-6 margin.
- Around a circle of radius 5e11, rounding let the native query count
  a rapid at x = -0.00001 as touching although the circle's extent
  started at x = 0; the margin scaled only with the rapid's own size.

Rather than chase each tolerance, the filter now has one narrow rule:
Extent.IsClearOf skips only when both extents are finite, lie within
1e6 and are more than 0.001 apart on some axis. 0.001 is ten times the
widest absolute band of any native contact query (the 0.00001 box
allowance and the 0.0001 contact reach of tiny arcs), and within 1e6
rounding stays far below it. Larger geometry is always checked in
full, as before the filter. Planning speed is unchanged (144-part grid
about 0.5-0.8 s).

All three reproductions are regression tests, with a test of the rule
itself.
2026-10-06 00:20:41 -04:00
aj 1c0320d17e docs(cutting): describe the arc contact band in the extent filter
Follows c1c8d5f and c21f687: an arc's extent is its supporting circle widened to the native contact reach, and an extent with a nonfinite bound is never skipped.
2026-10-06 00:07:39 -04:00
aj 4e6419fd2c fix(cutting): fall back to a full order search when learning stops
Review found a plate the previous search planned that the tour now
refused. Left leads in on its left and right on its right, so right
straight after left crosses left and left straight after right crosses
right. Learning "right before left" then contradicted "left before
right" and the search returned ConstraintConflict, although cutting a
third part above them in between is safe.

A blocked approach only proves that one part cannot follow the parts
cut so far from that position, not a global order, so learned rules
stay a heuristic. Once nothing new can be learned, the remaining budget
now goes to a full search over every ready part, nearest first (the
search used before the tour). A rule contradicting an order already
required is still skipped rather than ending learning early.
2026-10-06 00:04:05 -04:00
aj 2419a9e2e3 fix(cutting): give a new part order the plate's expansion budget
The desktop batch searched for a new part order with the flat 20000
default and kept the current order with 400 expansions per part. A new
order now also plans contour order and entries for every part (about
260 expansions per part on a dense grid), so a 100-part plate ran out
of budget and fell back to the current order. Both attempts now get
PlateBudget: 400 per part, at least 20000. The constants are renamed
MinimumExpansionBudget and ExpansionsPerPart to match.
2026-10-05 23:41:16 -04:00
aj b8f3d3bf0a fix(cutting): order parts as a short tour and learn blocked approaches
Free-order planning was one depth-first search over parts, contours and
entries. A dead end at one part backtracked through every entry
combination of the part before it (about 1,450 for a square with two
holes) before trying another part order, so a 4 x 4 grid of such parts
ran out of its 20000 expansions (and 200000) although cutting it row by
row is safe.

The whole-part order is now 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. The existing search then plans
contour order and entries along that order. If a part cannot be
reached without crossing parts already cut, the search learns "cut it
before those", backs up to just before the earliest of them, keeps the
parts cut before that point and re-plans the rest from the tool
position there. An attempt stops backtracking after 8 x entries x
contours expansions without getting further, so it learns instead of
retrying the entries of every earlier part. When nothing new can be
learned the result is a refusal, as before. A preserved order is
planned exactly as before.

16- and 36-part grids, in row order and shuffled, are now ready within
the default budget (they were NoSolutionWithinBudget); a 144-part grid
plans in about half a second.
2026-10-05 23:41:11 -04:00
aj bfe2e518e6 perf(cutting): skip lead and rapid checks against distant material
Every lead was checked against every placed part's material and every
rapid against every completed contour, so each check cost O(parts) and
planning a plate cost O(parts^2). Lead checks were 88% of planning time
on a 144-part grid.

LeadMaterialSnapshot and each completed contour now keep a conservative
extent (an arc counts as its whole supporting circle). A lead or rapid
skips material or a contour only when the extents are farther apart
than 1e-6 x (1 + coordinate size), far above the contact tolerance, so
results are unchanged: anything touching or closer still gets the full
native check. The rapid filter also applies to pre-post verification,
which shares ReleasedContourState.

New tests cover the cases just inside the skip: an arc lead and a
completed arc whose bulge reaches past their endpoints, and a lead and
a rapid that only touch another part's extent.
2026-10-05 23:41:02 -04:00
aj 48367da820 fix(diagnostics): refuse absolute subprograms instead of normalizing
1b422ef read absolute-mode hole subprograms by converting an
incremental-mode copy of every clean program. Rebuilding absolute
endpoints from incremental deltas is not exact: after a rapid at
1e12 a 1x10 rectangle moved by about 2.4e-5 and a real 2e-5 overlap
was reported clear, in the overlap overlay and pre-post verification
as well as Plan Cutting.

Convert programs directly again, which reads absolute coordinates
exactly, and refuse an absolute-mode subprogram as an incomplete
check instead: the converter adds a call's frame offset to
incremental moves only, so it would read such a hole at its frame
origin. OpenNest writes hole subprograms in incremental mode. The
null-list and unknown-instruction refusals from 1b422ef stay, and
CopyForGeometry is private to the planner again.
2026-10-05 22:04:47 -04:00
aj 8590f21c9c fix(cutting): block overlapping plates and preview only fresh plans
Review fixes for the Plan Cutting batch and dialog:

- A plate whose clean part material overlaps, or cannot be checked
  for overlap, is no longer ready, whatever its route. The batch
  captures each plate's material with PlateOverlapAnalyzer on the
  owner thread, analyzes it on the worker, and names both parts.
  Before, two overlapping squares were Ready and Apply regenerated
  them (plan section 4.2: overlap warnings are not waived).
- BuildPreview returns null for a refused plate, whose program graphs
  may be unsafe to copy (an unsupported instruction's Clone ran, and a
  cyclic subprogram overflowed the stack), and for a plate that
  changed after planning, which drew the replayed program at the live
  pose. The dialog then shows no preview and says why.
- The dialog plans with its own copy of the caller's settings, shows
  a failure message if a plan cannot be presented, and has a worker
  seam so the close-while-planning test holds the worker instead of
  racing a slow search. Form tests now observe the planning task.
2026-10-05 21:39:58 -04:00
aj 187234f459 feat(ui): preview and apply unified cutting plans
Plate > Plan Cutting... and Nest > Plan Cutting (All Plates)... open
one dialog over CuttingPlanBatch. It starts from the plate's (or the
last-used) cutting settings and plans at once; Cutting Settings and
Keep the current part order replan. The summary lists every plate
with its status and findings, and a read-only preview shows the
active plate in the proposed order with its proposed programs.

Apply is enabled only when every plate is ready and installs all of
them or none; a stale plan keeps the dialog open and asks for a
replan. Closing while planning cancels the worker and waits for it.
The menu commands share the busy guard of the other plate tools, and
after a successful Apply the confirmed settings become the saved
defaults. The older automatic sequencing and lead-in assignment
commands stay until they are migrated and retired.
2026-10-05 21:21:38 -04:00
aj 20f8b046de fix(cutting): fingerprint only the built-in settings types
The second delta review found four more false-equal classes in the
general reflective fingerprint, all reachable only through custom
settings subclasses: cycle markers that dropped the target ancestor,
display-formatted DateTime/DateTimeOffset, ignored dictionary and set
comparers, and arrays flattened without their dimensions. Safe arrays of
OpenNest elements were also newly refused. Every repair of the generic
traversal opened another such case.

Settings capture now supports exactly the types regeneration already
accepts (OwnedCuttingParameters): CuttingParameters, SequenceParameters,
AssignmentParameters and the built-in lead-in, lead-out and tab types.
Each member is written explicitly, doubles by bit pattern with invariant
numerals and text length-prefixed. Every object's runtime type is checked
before any member is read, so no other type's code runs. A plate-scoped
request whose part or plate settings contain any other type, subclasses
included, is UnsupportedGeometry at capture instead of a Ready plan that
can never apply. A nested settings object replaced by such a type after
capture makes Apply Stale. Detached part-list requests are unaffected.

Coverage tests fail when a supported type gains a property or field the
fingerprint does not write, or when a new built-in lead or tab type is
added without fingerprint support.
2026-10-05 03:16:27 -04:00
aj bb104a07bb fix(cutting): fingerprint settings state without executing foreign code
Delta review found three Important defects and a Minor one in the
settings fingerprint that closed the first freshness gap:

- Accepted settings state was silently omitted: dictionary entries render
  as KeyValuePair structs whose Key/Value are properties, property-backed
  custom structs contribute no public fields, and graphs past the depth
  limit wrote a constant marker, so all three edits fingerprinted equal
  and a changed plate still applied.
- Reading public properties executed arbitrary getters, so a capture
  documented as read-only could mutate live settings (Bump => ++Kerf).
- An enumerable settings member was enumerated at Apply, where its
  enumerator could throw out of the public commit call.
- Fingerprint text used ambient-culture interpolation, so an invariant
  capture compared unequal under a digit-substituting culture.

Traversal is now a closed boundary. OpenNest types render their public
readable properties and fields. Foreign types render only declared
instance fields, which include auto-property backing fields, because a
field read executes no code. Only arrays and List/Dictionary/HashSet/
KeyValuePair are enumerated, with insertion-ordered containers sorted;
other enumerables, delegates and unrepresentable shapes refuse to an
Invalid marker that never compares equal, so refused state is Stale
rather than silently equal. Doubles fingerprint by bit pattern rendered
with invariant formatting, and depth or budget overflow refuses instead
of truncating. A reference already on the path renders as a cycle
marker; built-in lead and tab objects reference settings back.
Capture stores refusals as-is, so a plate with uncaptureable settings
stays plannable and every commit against it reports Stale without
re-reading live state, and a fingerprint that fails on re-read is
likewise Stale, never an exception.
2026-10-05 02:49:23 -04:00
aj ea36c47a30 docs(cutting): describe Apply freshness, installer and nested-part bounds
Document the review hardening: classification, settings content and
malformed programs count as changes, a no-op proposal stays current,
parts repeated across plates are refused, the installer is internal, and
nested-part candidates use material bounds only.
2026-10-05 00:29:47 -04:00
aj 1d8c5fe274 fix(cutting): create plate-scoped requests through a factory
A CuttingPlanRequest constructor overload taking a Plate made the existing
detached call new CuttingPlanRequest(null) ambiguous (CS0121). Plate scope
is now requested with CuttingPlanRequest.ForPlate, and the result summary
describes dependencies and Apply as they now behave.
2026-10-05 00:28:59 -04:00
aj 4bbd8454b5 feat(cutting): order cutoffs and nested parts before their hosts
The cutting planner now accepts cutoffs on plate-scoped requests and
plans whole-part prerequisites captured from owned values:

- A cutoff precedes every part its nominal span crosses, using the same
  rule and drawing-reference matching as automatic sequencing; a cutoff
  without a definition precedes every part. Cutoffs stay fixed programs,
  need no lead-in and never become rapid obstacles; rapids into and out
  of them are still checked.
- A part proven, on native clean material, to lie inside a cutout of
  another part precedes that host. Touching or crossing boundaries are
  ambiguous and refuse; a part in a concave pocket has no dependency.
- Both searches only expand ready parts, a preserved order that breaks a
  prerequisite is a constraint conflict, and final replay rechecks the
  captured prerequisites instead of trusting the search.
2026-10-05 00:01:29 -04:00
aj 9c530ca7d2 feat(cutting): apply verified plans atomically
Plate-scoped cutting plan requests now record the plate's exact state at
capture, and CuttingPlanService.Apply installs Ready, replayed proposals
for a whole scope at once:

- Any change after capture (order, pose bits, program reference or
  in-place content, drawing program, lock/lead-in flags, settings,
  quantity, size, quadrant or cutoff definitions) returns Stale with
  nothing changed.
- Order changes without PartAdded/PartRemoved, so drawing quantities and
  sentinel plates are untouched; ObservableList.Reorder exposes the same
  operation and Plate.PartsReordered is raised once per changed plate.
- Regenerated parts receive owned copies of the replayed program and of
  the settings captured with the request; fixed programs stay in place.
- An install failure restores every plate exactly; an observer failure
  after publication is reported as a refresh error, not a rollback.
2026-10-04 23:48:28 -04:00
aj 7127884584 fix(cutting): start tabbed lead-outs at the trimmed cut end
A tab trims the perimeter short of its entry, but the lead-out was still
generated from the nominal entry point. An arc lead-out therefore started
off its own radius (ExecutionMotionReader rejected it as inconsistent), and
a line lead-out ran diagonally back toward the entry.

Every lead-out style on a tabbed perimeter now leaves from the trimmed
cut's actual end, on that entity's normal, so arcs are tangent and the tab
gap stays uncut. Untabbed contours and the corner run-out rules are
unchanged. Malformed legacy output is still refused, never refit.

Red before the fix: the three tabbed arc cases threw "Arc has zero or
inconsistent radius" and the line case ended at y=5 instead of 4.8. Keeping
the entry's normal at the actual end fails the curved-perimeter case.
2026-10-04 23:13:41 -04:00
aj 6f7652ee83 fix(cutting): preserve clean geometry through shared graph rotation 2026-10-04 22:26:22 -04:00
aj cf615dcef8 docs(cutting): document exact replay and fixed-program refusals 2026-10-04 22:02:56 -04:00
aj 085edeea3b docs(cutting): describe joint planning and refusal boundaries 2026-10-04 21:19:45 -04:00
aj 761cee7c1f feat(cutting): plan verified part order from fixed programs 2026-10-02 21:56:14 -04:00