forked from 1b-agent/opencadstudio-upstream
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 <[email protected]>
This commit is contained in:
1 parent
aea1fd2caa
commit
1490fe4ea9
9 files changed
+761
-26
No files matched your search
@@ -335,6 +335,11 @@ impl Scene {
|
||||
if self.is_layer_locked(h) {
|
||||
continue;
|
||||
}
|
||||
// Delta-undo: capture the removed entity so an undo can re-insert it.
|
||||
if self.is_recording_undo() {
|
||||
let before = self.document.get_entity(h).cloned();
|
||||
self.record_undo_before(h, before);
|
||||
}
|
||||
self.document.remove_entity(h);
|
||||
self.selected.remove(&h);
|
||||
self.hatches.remove(&h);
|
||||
@@ -344,13 +349,18 @@ impl Scene {
|
||||
}
|
||||
// Remove erased handles from all groups; delete groups that become empty.
|
||||
let group_dict_handle = self.document.header.acad_group_dict_handle;
|
||||
let mut groups_changed = false;
|
||||
let to_remove: Vec<Handle> = self
|
||||
.document
|
||||
.objects
|
||||
.values_mut()
|
||||
.filter_map(|obj| match obj {
|
||||
ObjectType::Group(g) => {
|
||||
let before = g.entities.len();
|
||||
g.entities.retain(|h| !handles.contains(h));
|
||||
if g.entities.len() != before {
|
||||
groups_changed = true;
|
||||
}
|
||||
if g.entities.is_empty() {
|
||||
Some(g.handle)
|
||||
} else {
|
||||
@@ -360,6 +370,11 @@ impl Scene {
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
// Delta-undo: editing a group (or deleting an emptied one) mutates
|
||||
// document.objects — non-entity state a pure-entity delta can't restore.
|
||||
if groups_changed {
|
||||
self.poison_undo_recording();
|
||||
}
|
||||
for gh in &to_remove {
|
||||
if let Some(ObjectType::Dictionary(dict)) =
|
||||
self.document.objects.get_mut(&group_dict_handle)
|
||||
|
||||
Reference in new issue
Block a user