Files
OpenNest/docs/undo-redo.md
T
ajandClaude Opus 5.5 a3b7c715d5 docs: add the Undo/Redo contract and point AGENTS.md at it
Undo/Redo landed in phases 1-3 without a written contract, so the next
person adding a desktop edit has no rule telling them to record it, and
nothing explains why a restore must close Cutting Inspection or why
snapshots keep empty plates and skip best fits. docs/undo-redo.md states
what is and is not undoable, the BeginChange/RecordChange rule, the
"Change" safety net, how states are captured and restored, the guards
and key routing, and the measured limits. AGENTS.md links it from the
OpenNest bullet and the contract list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 12:11:59 -04:00

8.9 KiB

Undo and Redo

The desktop's Undo and Redo (Edit menu; Ctrl+Z, Ctrl+Y and Ctrl+Shift+Z) work on whole-nest states. Each open nest has a history of in-memory .nest copies; Undo reads the previous copy back and swaps it into the window. There are no hand-written inverse operations: anything the nest file format saves is undoable, and a new edit becomes undoable once its entry point records it.

Piece Where
History engine (states, cursor, limits, coalescing, save point) OpenNest.Data/UndoHistory.cs
State codec OpenNest.IO/NestSnapshot.cs
Recording scope, fingerprint, history state OpenNest/Undo/
Recording, restore, safety net OpenNest/Forms/EditNestForm.Undo.cs
Menu, keys, guards OpenNest/Forms/MainForm.Undo.cs, MainForm.Guards.cs (IsUndoBusy)
Tests OpenNest.WinForms.Tests/Forms/UndoRedoTests.cs, UndoRecordingTests.cs

What is undoable

Everything stored in the nest:

  • parts: add, delete, cut, move, drag, nudge, rotate, align, push, center, expand spacing, clone;
  • placement: fill, fill area, pattern tile, best-fit placement, Auto Nest, Clear Nest;
  • plates: add, remove, remove empty plates, Edit Plate, rotate, resize, cutting parameters, clamps and clamp rail, the support grid, the nest's default plate;
  • cutting: manual and automatic cut-offs and their drags, Auto Sequence, Set Sequence, Sequence by Path, placing and removing lead-ins, Plan Cutting's Apply, clearing lead-ins and cut-offs;
  • drawings: import, Shape Library, edit in the converter, drawing properties and quantities, delete, clean unused, Drawing Library adds, a drawing's library link after Save to Drawing Library;
  • the nest: its machine and the Nest Info fields other than identity (below).

The Edit menu names the step: "Undo Rotate", "Redo Fill". UndoRecordingTests holds one test per recorded edit with its label.

What is not undoable

Deliberately excluded:

  • anything outside the nest: Save, Save As and database saves, posting and exports, releases and Undo Release, Drawing Library server writes (the library entry Save to Drawing Library creates stays), drawing-set edits, Save as Machine Defaults (clamps, support grid), machine and post configuration, nest defaults;
  • view state: zoom, pan, view toggles, selection, the active side panel;
  • the nest's identity: Name, Status, MadeBy and DateCreated. A restore carries them over from the live nest, so a Nest Info edit of only these records nothing. All Document state (save path, server id and revision) stays too: the Document is never replaced.

A save does not clear the history; it marks a save point (UndoHistory.MarkSaved, IsAtSavePoint). A database save marks it only when no edit was recorded while it ran.

Recording rule

Every desktop edit that changes a nest goes through EditNestForm.BeginChange(label) (or PlateView.BeginChange, which forwards to its window) or the RecordChange(label, edit) helper, with a short user-facing label:

using var change = BeginChange("Rotate");
change.MarkChanged();
...mutate...
change.Complete();
  • Dispose after Complete() commits one history entry. A scope that never called MarkChanged() records nothing, so a cancelled dialog or an edit that changed nothing does not add an entry.
  • A scope disposed without Complete() (an exception, a cancellation) rolls a changed nest back to the last recorded state.
  • A scope opened inside another merges into the outermost one: one entry under the outer label. A nested scope left incomplete rolls back the whole entry.
  • Record in the desktop only, at the command's entry point. Never in engines, Core, IO or dialogs that do not own the window's nest.
  • Long or asynchronous runs (Fill with progress, Auto Nest, automatic cut-offs, Plan Cutting) open the scope at their final commit and record only the result they keep. A stopped run that keeps nothing records nothing; an interactive drag records one entry on mouse-up.
  • Repeated edits with an equal coalesceKey within one second undo as one (arrow-key nudges of one selection). An Undo, Redo or save in between ends the run.

The "Change" safety net

The window keeps a fingerprint (NestFingerprint: plate sizes and parts, poses, programs, cut-offs, drawings and required quantities, objects by reference) of the last recorded state. Before a scope opens and before Undo or Redo, a changed fingerprint means an edit bypassed recording; it is committed as an entry labelled "Change", so it is never discarded. The fingerprint is a cheap hash, so this is best effort and no excuse to skip recording. In DEBUG builds an Application.Idle check writes a Debug.WriteLine warning when the nest changed outside a scope; watch the output window when adding an edit.

How a state is made

NestSnapshot.Capture(nest) writes the archive a save would, with three differences:

  • PreserveDateLastModified: a normal write stamps the live nest's modified date;
  • KeepEmptyPlates: a file write drops empty plates, which would lose an added plate and shift plate indices. Snapshots keep every plate, the trailing sentinel too;
  • no BestFitStore: best fits are a cache, never part of history.

snapshot.Restore(live) reads the copy and reuses each live Drawing whose saved content is unchanged (matched by NestWriter.GetDrawingFingerprints), so BestFitCache, FillResultCache, colours and Drawing.Id survive. A changed drawing, a quantity edit included, comes from the copy. Restore then recomputes nested quantities.

Never compare snapshots byte for byte to detect a change: two writes of one nest can differ in derived values (expectedRemnant around 1e-10). The scope's MarkChanged flag decides.

What a restore rebuilds

EditNestForm.RestoreNest(restored):

  1. Closes the Cutting Inspection panel, and refuses if it stays open (below).
  2. Copies the identity fields from the live nest.
  3. Captures the plate index, viewport and selected part indices.
  4. Ends the current action, clears preview parts, cancels the overlap check and any pending drawing-list drag, and hides the side panel: all of them hold parts, plates or the nest.
  5. Unbinds the old nest, replaces Document.Nest and binds the new one through the same path the constructor uses (PlateManager, sentinel, units, cut-off defaults, overlap check).
  6. Regenerates each plate's cut-offs with PlateView.CutOffSettings (reading uses defaults) and recolours drawings with the current scheme, as Open does.
  7. Loads the same plate index (clamped), restores the viewport instead of zooming to fit, and reselects by part index when the plate's part count is unchanged.
  8. Refreshes the plate list and header, then raises NestRestored; the main window closes a remnant viewer on that view and refreshes the status bar, overlap and Edit menus.

CuttingInspectionPanel holds the old nest's plates, its staged cut-offs and a proposal. Apply after a restore would commit to plates no longer in the nest, so it must close first. Undo is unavailable while it is open, and the main window swallows Ctrl shortcuts during inspection.

Every Nest, Plate, Part and Program is a new object after a restore. Never key state on them, or on Drawing.Id, across an Undo; a new holder of nest objects must be closed or rebuilt by RestoreNest.

Guards and keys

Undo and Redo do nothing, and the menu items are disabled, while:

  • MainForm.IsUndoBusy: a whole-job run or progress window, a background database save (it serializes the live nest), a fill on any view of the nest, or a current action whose Action.BlocksUndo is true. BlocksUndo defaults to IsBusy(); ActionSelect blocks only during a cut-off drag (a selection is not busy), and ActionLeadIn never blocks;
  • a change scope is open or a restore is running (IsChangeInProgress);
  • Cutting Inspection is open.

Keys are routed in MainForm.ProcessCmdKey; the menu items show shortcut text but have no ShortcutKeys. A focused text box, numeric box, editable combo box, grid cell in edit mode or the program editor keeps its own Ctrl+Z and Ctrl+Y (KeepsOwnUndo). Otherwise Ctrl+Z first asks the current action's TryUndoStep(): Sequence by Path removes its last line, and only when it has none does the nest undo.

Limits and cost

  • 50 undo steps and 128 MB per open nest (UndoHistory.DefaultMaxDepth, DefaultMaxBytes); the oldest states are evicted first, never the current one.
  • Typical nests take a few kilobytes and a few milliseconds per state. Measured on the heaviest production nests (E0323-02, 7000 parts; K0924-17, 101 drawings) with every part planned: capture about 350 ms and 2.2 MB, Undo about 1 s, most of it the plate reload (LayoutPart.Create per part, the same as switching to that plate). Opening a nest pays one capture for the history's first state.
  • The worst nest's 50 states stay inside the byte budget (about 110 MB). Plate-scoped states are not needed unless a heavier nest shows lag.