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 <noreply@anthropic.com>
This commit is contained in:
1 parent
aea1fd2caa
commit
1490fe4ea9
9 files changed
+761
-26
No files matched your search
@@ -132,6 +132,11 @@ impl Scene {
|
||||
.flatten()
|
||||
.collect();
|
||||
for &h in handles {
|
||||
// Delta-undo: capture the pre-transform image before mutating.
|
||||
if self.is_recording_undo() {
|
||||
let before = self.document.get_entity(h).cloned();
|
||||
self.record_undo_before(h, before);
|
||||
}
|
||||
if let Some(entity) = self.document.get_entity_mut(h) {
|
||||
view::dispatch::apply_transform(entity, t);
|
||||
if mirror_true {
|
||||
@@ -164,7 +169,13 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
// Move the baked dimension-block sub-entities too (collected above).
|
||||
// These are entities not named in the reported change set, so a delta
|
||||
// must capture their before-images here or a dimension move won't undo.
|
||||
for h in &dim_block_subs {
|
||||
if self.is_recording_undo() {
|
||||
let before = self.document.get_entity(*h).cloned();
|
||||
self.record_undo_before(*h, before);
|
||||
}
|
||||
if let Some(entity) = self.document.get_entity_mut(*h) {
|
||||
view::dispatch::apply_transform(entity, t);
|
||||
}
|
||||
@@ -317,6 +328,10 @@ impl Scene {
|
||||
let bn = d.base().block_name.clone();
|
||||
if !bn.trim().is_empty() {
|
||||
if let Some(new_bn) = self.clone_transformed_block(&bn, t) {
|
||||
// Delta-undo: a copied dimension adds a fresh anonymous
|
||||
// *D block record — non-entity state a pure-entity delta
|
||||
// can't restore. Poison so the app keeps a full snapshot.
|
||||
self.poison_undo_recording();
|
||||
if let EntityType::Dimension(d) = &mut entity {
|
||||
d.base_mut().block_name = new_bn;
|
||||
}
|
||||
@@ -327,6 +342,10 @@ impl Scene {
|
||||
entity.common_mut().handle = Handle::NULL;
|
||||
let h = self.document.add_entity(entity).unwrap_or(Handle::NULL);
|
||||
if !h.is_null() {
|
||||
// Delta-undo: a copy's before-image is "nothing" (undo erases it).
|
||||
if self.is_recording_undo() {
|
||||
self.record_undo_before(h, None);
|
||||
}
|
||||
let new_model = match self.document.get_entity(h) {
|
||||
Some(EntityType::Hatch(dxf)) => {
|
||||
let color = convert::tess_util::aci_to_rgba(&dxf.common.color);
|
||||
|
||||
Reference in new issue
Block a user