perf(undo): record entity deltas instead of cloning the whole document

Frequent entity-only edits (MOVE/ROTATE/SCALE/MIRROR/ALIGN, COPY/ARRAY,
draw-add, ERASE) captured a full ~800k-entity CadDocument clone (~46ms) on
every edit — the last big per-edit cost after the incremental-render work.

They now record only the touched entities' before/after images:

- Scene self-records through the five mutation primitives when a recording is
  open (UndoRecording + is_recording_undo guard, zero cost off-path).
- apply_entity_delta installs one side per handle (in-place / re-insert with
  the original handle / remove) plus a per-handle cache reseed, then a single
  bump_entities so the incremental caches patch instead of a full re-tess —
  undo/redo of a small edit is fast too, not just the edit.
- HistorySnapshot is now Full | Delta; a Delta is self-inverting and moves
  between the undo/redo stacks without re-capturing. Commands that touch
  non-entity state (new layer, group, *D block, viewport) fall back to the
  full snapshot via per-command predicates, and a poison flag debug-asserts
  any mis-scope.

remove_entity leaves the handle dangling in its owner block record's
entity_handles (harmless on its own, a lookup miss is skipped), so a
re-insert strips stale entries first — otherwise add_entity would list the
handle twice and the DXF/DWG writer would emit the entity twice.

Round-trip tests cover transform/erase/add/copy (do->undo->redo equality,
handle preservation, no duplication).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-15 11:11:17 +03:00
commit 1490fe4ea9
9 changed files with 761 additions and 26 deletions

View file

@ -696,7 +696,8 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(cmd));
} else {
let n = handles.len();
self.push_undo_snapshot(i, "ERASE");
let delta_safe = self.delta_erase_safe(i, &handles);
let pending = self.begin_undo(i, "ERASE", handles.len(), delta_safe);
// Stash the erased entities so OOPS can restore them.
self.oops_cache = handles
.iter()
@ -707,6 +708,9 @@ impl OpenCADStudio {
self.refresh_properties();
self.command_line
.push_output(&format!("{n} object(s) erased."));
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
}