PlateOverlapAnalyzer converted clean programs directly with
ConvertProgram.ToGeometry, which adds call offsets only to
incremental moves, so holes in absolute-mode subprograms were read at
their frame origin. A square with two absolute holes reported
"Native material contours cross or touch", and the Plan Cutting
overlap gate blocked it while the incremental twin passed. Convert an
owned incremental-mode copy instead (PreparedContours.CopyForGeometry,
now internal, the same normalization the cutting planner uses).
The analyzer also threw on a program whose Codes list was null and
cast or cloned instructions it does not know. It now refuses a
missing list and anything other than the exact built-in instruction
types as an incomplete check, before any copy or conversion runs.
Pre-post verification and the plate overlap overlay share this path.
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.
CuttingPlanBatch captures every scoped plate on the owner thread with
an owned copy of the confirmed cutting parameters, plans them on a
worker with per-plate progress and cancellation, and returns a
proposal that applies all or nothing through CuttingPlanService.Apply.
A free-order search that ends NoSolutionWithinBudget is retried once
with the current part order, captured up front so the worker never
reads live plates; the proposal reports that the order was kept. A
kept order gets 400 expansions per part (at least 20000), since it
still searches contour order and entries.
Only after Apply succeeds does each plate keep its own copy of the
settings. The proposal builds detached preview plates (quantity zero)
and describes each plate's status and findings with part numbers.
Prepare BOM rows for quantity editing:
- BomQuantity.TryParse accepts only a whole number of at least 1
(surrounding spaces allowed; no sign, decimal, exponent, separator or
overflow).
- BomPartRow keeps the BOM's own quantity (BomQty), refuses invalid
typed quantities in TrySetQuantity, and raises PropertyChanged for
Material, Thickness and Qty plus the status they affect, so a bound
grid can refresh.
- A blank BOM quantity becomes 1, as it was imported before, but is now
visible in the row and counted in the summary ("had no BOM quantity
(1 used)"). A BOM quantity below 1 is no longer imported as 0: the
row reads "Needs quantity" until the operator enters one.
BomImportFlowTests covers BOM items to rows to an edited quantity to the
group total and the created nest's required quantity. Mutation reds:
a zero floor, signed or decimal input, ignoring the quantity in the
status, not assuming 1 for blanks, setting unparsed text and dropping
the status notification each fail the Bom tests.
When two BOM rows named the same drawing file and shared a material and
thickness (the same part in two subassemblies), Create Nests imported
the file twice. Nest.Drawings is a set keyed by drawing name, so the
second drawing was dropped with its quantity: PT01 x2 plus PT01 x3
gave a nest needing 2, while the Groups tab showed 5.
Each drawing file is now imported once and needs the total of its
rows. Build_CombinesRowsThatUseTheSameDrawing required [2, 1] where
[5, 1] was expected against the previous commit.
Project Memory: 44293f97-a027-4c29-bc2d-bb789c796746
A BOM row with a file name and a matching drawing but a blank thickness
was reported "No DXF" and locked, so its thickness could not be entered
and the part was dropped: BomAnalyzer skips such items before looking
for a drawing, and the form only knew drawings the analyzer matched.
A row with a blank material read "Matched" but was silently left out
of every group.
Rows now resolve their drawing through a DrawingFileIndex shared with
BomAnalyzer (whose behavior and tests are unchanged), and a row's status
is computed from its values:
Ready | Needs material | Needs thickness | No drawing found | No file name
Rows with a drawing stay editable, and an edit updates the status cell.
Groups take only Ready rows (a zero, negative or non-finite thickness is
not Ready). The summary line counts ready rows and each problem.
Build_RowWithoutThickness_StillFindsItsDrawing failed at its DxfPath
assertion against the previous commit. Mutation reds: removing the
material check, accepting zero or non-finite thickness, skipping the
drawing for rows without thickness, and grouping without the status
each fail the Bom tests.
Project Memory: b8e3a978-2cfe-4fe7-b2a5-eb01c6c54679
A BOM row whose File Name carried its extension ("PT01.dxf") showed
"No DXF", was locked and was never imported, although BomAnalyzer had
found the file: the matched paths were stored under the raw BOM name
and looked up by the name without the extension. Both sides now use
the extension-less name.
The new BomImportRowsTests row failed at its IsEditable assertion
against the previous commit.
Project Memory: f3be7a4b-e679-470c-bb52-7057899228e4
Move the BOM import dialog's row building, grouping and per-group nest
construction out of BomImportForm into OpenNest.IO/Bom so they can be
tested on Linux:
- BomPartRow (now public) and BomImportRows.Build, a verbatim move of
the form's BuildPartRows (still via BomAnalyzer).
- BomImportGroups.Build: one grouping used by both the Groups tab and
Create Nests, which each carried their own copy. Create Nests now
creates nests in the Groups tab's order (material, then thickness).
- BomNestBuilder.Build: the nest for one group (saved defaults first,
then the group's plate size, spacing, material and thickness; one
drawing per row with the row's quantity, 1 when blank).
The form keeps the file dialogs, grids, EditNestForm windows and the
completion message. Behavior is otherwise unchanged; the two row-status
defects found while planning are fixed in the following commits.
Nest Info edits only the material name, but SaveNestInfo replaced the
nest's material with new Material(name), so pressing OK without touching
the material silently dropped its grade and density.
SaveNestInfo now goes through NestMaterialSelection.Apply in OpenNest.Data:
a name with the same SharedListNames.Key as the current material (trimmed,
whitespace collapsed, invariant upper case) keeps its grade and density
and takes the typed name; any other name gets a name-only material, as
before. It always returns a new Material. SharedListNames.Key is the
name normalizer the shared customer/material lists will use.
Behavioral reds (each restored byte-identically): restoring the old
name-only path fails both keep-grade tests; exact-string matching,
dropping the upper-casing or the whitespace collapse fails the
normalized-name test; returning the current instance fails the copy test.
EditNestInfoFormTests cover Load -> Save through the real form; they are
cross-compiled here and execute in the windows-desktop CI job.
The Details area of the Database-mode Open dialog now has a plate
preview beside the Plates/Drawings tabs, with a draggable divider. It
draws one plate of the highlighted nest; the arrow buttons below it step
through the plates ("Plate 2 of 5"), and choosing a row on the Plates tab
shows that plate, so the table and the preview stay on the same plate.
The preview is read-only (no selection or drop) and refits when resized.
NestDetails keeps the downloaded nest's plates (PlateLayouts) beside its
rows, so the preview draws the copy already read for the tables with no
second download.
Tests: NestDetails keeps every plate in nest order (fails with the
assignment removed). The Windows form test steps forward with the
button, checks the row follows, and selects a row back.
The server stores only nest-level metadata, so a browser that shows a
saved nest's plates and drawings has to read them from its archive.
NestDetails.FromNest turns a nest into one row per plate (duplicates,
size, parts and distinct drawings without cutoffs, utilization; 0 for a
zero-size plate rather than NaN) and one row per non-cutoff drawing
(required, nested across every plate's duplicates, remaining, area),
plus the nest's units.
NestDetailsSession loads the details of the highlighted nest. A new
load or Clear supersedes the previous one before cancelling it, so a
load that completes synchronously on cancellation and any late result
or failure of a superseded load are discarded; IsLoading follows only
the latest load.
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.
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.
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.
The containment prefilter used the inner part's whole clean-program
bounds, which include rapid endpoints and scribe marks. A remote rapid or
mark could push those bounds outside the host and drop a genuine
inner-before-host prerequisite. Candidate pairs now use the material
extent only (cut and display motions); containment is still proven on
native material.
Review of the atomic Apply found two commit-boundary gaps:
- The plan installer checked only root program references, so a public
caller could install a payload whose subprograms alias another live
(even locked) part's program, or share settings between parts. The
installer and its payload types are now internal; CuttingPlanService.Apply,
which installs owned copies of independently replayed proposals, is the
only public path.
- A part placed on two plates in one scope was planned once per plate; one
plate's install replaced the program the other plate verified as fixed.
A part repeated anywhere in the scope is now invalid input.
Review of the atomic Apply found freshness gaps:
- A drawing's cutoff classification decides lead, material, obstacle and
dependency treatment but was not captured; changing it after planning
still applied the old proposal.
- Part and plate cutting settings were compared by reference only, so an
in-place edit after capture applied (and a regenerated part overwrote
it); plate settings were not compared at all. Settings are now captured
as an exact public-state fingerprint.
- A live program whose instruction list was set to null made Apply throw
instead of returning Stale, and a respelled key in a case-insensitive
binding dictionary compared equal.
Caller-confirmed planning settings remain planning input: editing them
after capture does not stale the plan and does not leak into the result.
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.
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.
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.
FillPattern gathered per-angle results in a ConcurrentBag and kept the first best it
enumerated, so an exact tie between angles went to whichever worker finished first:
identical inputs could return different, equally scored layouts. Results now sit in
slots indexed by angle, V before H within an angle (the order the single-angle bag
already produced and the existing tie tests pin), and ties keep the earliest slot.
Single-angle behaviour is unchanged.
Regression: a never-prefer comparer over one 0-degree and fifteen 180-degree angles
must keep the 0-degree layout in 50 of 50 calls; it failed on the old code.
The new windows-desktop CI job surfaced several tests that had never
actually run on Windows:
- SeparatorPenLifetimeTests expected Control.Disposed to fire only
once; current WinForms re-fires it on every Dispose() call.
- EditNestFocusTraversalTests assumed the Plates tab was selected by
default, which stopped being true once Drawings became the landing
tab; select it explicitly before asserting focus.
- NestReportExportTests asserted an exact plate count that ignored
PlateManager's trailing sentinel plate, and didn't expect the nest
name in the busy-export message.
- AtomicReportFileTests only expected IOException when replacing a
directory target; Windows raises UnauthorizedAccessException instead,
which callers already treat the same way.
Also register a ThreadException handler so an exception inside a
WndProc callback fails the test normally instead of popping a modal
dialog that blocks the desktop.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New only fell back to the legacy DefaultUnit setting when the whole
defaults file was missing or invalid, so a readable file without a
usable unit produced inches. BOM import never assigned units at all,
and the Nest Defaults dialog showed inches for a missing file.
All three now load through MainForm.LoadSavedNestDefaults, which passes
DefaultUnit as the per-field unit fallback. BOM-created nests start from
NestDefaults.ApplyTo, like New, before applying each group's plate size
and spacing.
The ApplyTo test now asserts units; removing the unit assignment from
ApplyTo fails it. WinForms behaviour is compile-verified only on Linux.
Enum.TryParse accepted numeric strings ("7" loaded an undefined unit,
"1" loaded millimeters) and comma-joined names, and a JSON number for
units failed the whole file. Units now load only from a defined unit
name; anything else falls back for that field alone, while the other
valid fields still load.
Add Load(path, fallbackUnits, out status) so callers can keep their
existing unit preference when the file is missing, unusable, or lacks a
usable unit (previously a readable file without units silently became
inches). An undefined fallback is ignored in favour of the built-in
units.
The numeric/comma/JSON-number rows failed against the previous commit;
dropping the fallback assignment or its IsDefined guard fails the new
fallback tests.
NestDefaults.Load caught only JsonException and IOException, so a
defaults.json that File.Exists can see but the process cannot read
threw UnauthorizedAccessException out of New. Report such files as
Invalid and return the fallback values, like a corrupt file.
The regression mode-000s the file on Unix (skipped on Windows and when
running as root); it failed with UnauthorizedAccessException before
this change.
A request is now marked superseded before the previous one is
cancelled, so a query that completes synchronously on cancellation
returns quietly instead of clearing the page and surfacing a stale
cancellation error. NestBrowseSession.IsLoading follows only the latest
request, and the dialog's Previous/Next and Loading text use it rather
than a count that included superseded requests. Each request disposes
its own cancellation source when it ends.
SQLite LIKE stops matching at an embedded NUL, so search=Alpha%00x
behaved like 'Alpha' followed by a wildcard and returned rows that do
not contain the whole search text. The shared query validation now
rejects NUL, so the server answers 400 and the client throws before
sending.
The Database-mode Open dialog no longer downloads every record. It
browses through NestBrowseSession, which sends one bounded query per
change: the filter box (300 ms debounce) searches on the server and
returns to the first page, column headers sort every match on the
server (a second click reverses), and Previous/Next page by 100 with a
range and total in the status line. A response superseded by a newer
request is cancelled and discarded, refresh steps back when the current
page was emptied, and failures clear the rows and show the error
without a modal box per keystroke.
The query accepts sort=<column>&order=asc|desc over a fixed allowlist
(saved, name, customer, status, material, dates, thickness, plate and
part counts, made by, comments, file size). Text columns sort with
NOCASE, ties break by id in the same direction so pages partition the
matches, and unknown or numeric sort values and other orders return 400.
The client sends the sort as its camelCase name.
GET /api/nests/query filters name, customer, material, made by, comments
and status (stored and display names) with an escaped LIKE parameter,
orders newest saved first with an id tie-break, and returns one page of
at most 500 records plus the total match count, read in one database
hold. Unknown, repeated or out-of-range parameters return 400.
RemoteNestRepository.QueryAsync validates the same bounds before
sending, rejects oversized pages, and reports a 404 from an older server
as a server that needs updating. GET /api/nests stays the full
enumeration used by backup, restore checks and the container smoke.
MaximalRectangles.InRegion finds the largest axis-aligned rectangles that fit
wholly inside a Clipper region, such as a cutout shrunk by the part spacing.
It grids the region on its vertex coordinates plus even divisions, keeps a cell
only when no edge enters it and an even-odd row scan puts it inside, then runs
the shared histogram search.
Exact for regions with only horizontal and vertical edges; slanted and curved
edges are followed as a staircase that never crosses the boundary, short of the
true maximum by up to about one cell per side. Tests cover a rectangle, an
L shape, a frame with a hole, a round hole (inscribed square), a diamond and a
star; disabling the edge-crossing check fails the three slanted-edge tests.
Add DrawingAligner: bounded multi-start rigid ICP that maps a revised
drawing's geometry into the old drawing-local frame as an operator-review
seed. Material-filtered programs flatten through the shared chord-error
machinery, resample via the shared ContourSampler ring scheduler, and fit
closest bounded target segments with trimmed weighted least squares
(reflection never solved, never applied). Seeds come from outer-centroid
translation and MBR angle deltas with the 90-degree family plus identity.
The stable outer boundary is fitted first; retained hole evidence only
reorders candidates inside the outer tie band, so a moved hole cannot
drag the perimeter. Structured AlignmentResult reports convergence,
residual quantiles, bidirectional coverage, symmetry ambiguity,
reflection uncertainty, and a bounded diagnostic IoU. Refusals
(invalid input, insufficient support, unsupported topology, sample
budget, cancellation) are reported as reasons, not thrown.
Tests verify recovered poses against exact fixture geometry (true
segments/arcs/circles, not sample clouds) for known transforms,
symmetric rectangle/circle ambiguity, mirror non-selection, moved-hole
perimeter preservation, and bounded region arithmetic (identical=1,
disjoint=0).