A disposed PlateView stayed subscribed to its plate, so a plate that
outlived the view kept rebuilding the disposed view's layout and
raising its events; Dispose now detaches the part added, removed and
reordered handlers.
ObservableList.Reorder keeps repeated references, but the reorder
handler mapped each part to one layout, so [a, a, b] collapsed to one
shared layout for both occurrences of a. It now reuses each existing
layout once and creates one only when none is left.
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.
The dialog relied on Progress<T> and an await continuation capturing
SynchronizationContext.Current when planning started. WinForms
uninstalls its ambient context when the outermost DoEvents loop ends,
so a plan started outside a message loop reported progress from a
thread-pool thread and crashed the test host with "Error creating
window handle" (windows-desktop job of run 37398775280,
CuttingPlanFormTests.ApplyAfterALiveEdit_ChangesNothingAndReplanRecovers).
Capture the dialog's context once when it is built and post progress
and the finished plan to it explicitly. The busy-editor test now
starts planning after a DoEvents loop to cover that path.
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.
PlateView now follows Plate.PartsReordered, which an applied cutting
plan raises once per changed plate: it puts its layout parts in the
plate's order (the drawn part numbers are the cutting order), marks
them dirty so new programs are redrawn, and raises its own
PartsReordered. The overlap overlay treats a reorder like any other
part change and marks its report out of date.
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.
The first windows-desktop run of BomImportFormTests (PR #4, run
37379965924) failed four cases:
- InvalidQty_IsRefusedAndTheOldValueKept("0" / "-1" / ""): the tests
committed with DataGridView.EndEdit(), which never raises
CellValidating, so the grid's default parser stored 0, -1 or null.
- BlankingMaterialOrThickness...: a null value from CellParsing is
ignored by the grid, so a blank material was stored as " ".
Operator commits (Enter, leaving the cell or the grid) do validate, but
a programmatic EndEdit() must not store text validation refuses either.
CellParsing now keeps the row's current quantity or thickness for text
validation would refuse, trims the material (a blank one is ""), and a
cleared thickness or quantity cell stores null through the columns'
DataSourceNullValue. A thickness of only spaces is refused like any
other invalid text.
The tests now commit by pressing Enter (ProcessEnterKey), as the
operator does, and a new ProgrammaticCommitOfInvalidText test covers
the EndEdit() path.
The BOM import dialog's Parts table now binds to the BomPartRow objects
themselves (a BindingList with Designer-defined columns) instead of a
DataTable of strings:
- Columns: Item #, File Name, Description, Material, Thickness, Qty,
Status. Only Material, Thickness and Qty are editable, and only on
rows that have a drawing; those rows are grey and locked. Rows that
still need input are tinted amber.
- Qty is editable. Anything but a whole number of 1 or more is refused
in the cell with an error icon; Esc restores the old value. A
blank-in-BOM quantity shows a tooltip saying 1 is used.
- A thickness that is not a number above 0 is refused the same way;
blanking it marks the row "Needs thickness".
- Every edit goes through the row's bound part, so the Groups tab
(part count and Total Qty) and the summary follow at once, and an
edit can never land on a different part. Rows stay in BOM order:
the headers no longer sort, which removes the sort-then-edit defect
where index mapping applied an edit to another part.
- Create Nests first finishes the cell being edited.
BomImportFormTests (OpenNest.WinForms.Tests) cover the columns, the
quantity edit and its group total, refused quantities, edits after
the rows are reloaded in another order, locked rows and blanked
material/thickness. They first run on the windows-desktop CI job; the
sort defect's red evidence is source review, not an executed test.
Project Memory: 5b0808d1-7353-4a34-87df-d1fbfc38f2b4
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 release workflow is the only hosted job that runs the Core, Engine,
IO and Server suites on Windows, and it had no hang guard: a silent test
would hold the runner until the 30-minute cap with no clue which test
hung. Apply the guard the windows-desktop CI job already uses:
--blame-hang-timeout 5m with a mini dump, and upload all of TestResults
rather than only the TRX files.
The suites have roughly doubled since the last release run (7.5 min),
so raise the job cap to 45 minutes. Validated with actionlint.
The plate preview read DataGridView.CurrentRow in SelectionChanged, but
that event runs before CurrentCell moves, so CurrentRow still named the
previous row. Choosing a row showed the plate that had been selected
before it, and the next-plate button snapped the preview back to the
old plate. Read the selected row instead.
The Windows job of run 37341610182 caught it:
PlatePreview_StepsThroughThePlates_AndFollowsThePlatesTable expected
"Plate 2 of 2" after the next-plate button and got "Plate 1 of 2". The
test now also checks that the selected row follows the button.
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.
File > Open in Database mode now shows a resizable browser instead of
the small list dialog. The upper Nests grid keeps the server-side
search, sort and paging (row numbers count through the filtered list;
the title shows the server and the range). Below it, Details tabs list
the highlighted nest's plates and drawings, read from its archive after
the highlight rests for 250 ms; moving back to a nest already shown
reuses its result, and a missing archive is reported in the details
line.
Enter opens the highlighted nest (in the Find box it runs the search at
once instead) and Esc closes. Double-click opens a nest, and a context
menu offers Open, Delete (after confirmation) and Refresh. It only
browses saved nests; new nests are still created from the main window.
Windows tests drive the real form: page load, details for two nests,
Enter opening the highlighted one, the missing-archive message and Esc.
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.
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.
The readiness filter rebuilt the finished-part list for every candidate.
Build one set per expanded node; the candidates and their order are
unchanged.
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.
Restore RepeatedCallsReturnIdenticalPoses for a 1 x 1 square and a 1.5 x 0.75 rectangle.
Both cases fail intermittently because Default Fill can return different, equally scored
lattices for identical calls (Project Memory opennest c98c21bd), so they are skipped with
that reason until the defect is fixed rather than removed.