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
+41
-2
@@ -8,7 +8,7 @@ use crate::scene::Scene;
|
||||
use crate::snap::SnapResult;
|
||||
use crate::ui::{LayerPanel, PropertiesPanel};
|
||||
use acadrust::tables::Ucs;
|
||||
use acadrust::{CadDocument, Handle};
|
||||
use acadrust::{CadDocument, EntityType, Handle};
|
||||
use iced;
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
@@ -472,8 +472,30 @@ impl DocumentTab {
|
||||
}
|
||||
}
|
||||
|
||||
/// One undo/redo entry. Most edits touch the whole drawing's state and store a
|
||||
/// [`FullSnapshot`] (a document clone); frequent entity-only edits (move / copy /
|
||||
/// draw / erase) instead store a cheap [`DeltaSnapshot`] naming just the entities
|
||||
/// they changed, so an 800k-entity drawing doesn't pay a ~46 ms document clone
|
||||
/// per edit.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct HistorySnapshot {
|
||||
pub(super) enum HistorySnapshot {
|
||||
Full(FullSnapshot),
|
||||
Delta(DeltaSnapshot),
|
||||
}
|
||||
|
||||
impl HistorySnapshot {
|
||||
pub(super) fn label(&self) -> &str {
|
||||
match self {
|
||||
HistorySnapshot::Full(f) => &f.label,
|
||||
HistorySnapshot::Delta(d) => &d.label,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A whole-document undo entry: the safe fallback for any command that may touch
|
||||
/// layers, objects, block records, styles or tables.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct FullSnapshot {
|
||||
pub(super) document: CadDocument,
|
||||
pub(super) current_layout: String,
|
||||
pub(super) selected: Vec<Handle>,
|
||||
@@ -481,6 +503,23 @@ pub(super) struct HistorySnapshot {
|
||||
pub(super) label: String,
|
||||
}
|
||||
|
||||
/// An entity-only undo entry: for each touched handle, its before-image and
|
||||
/// after-image (`None` = the entity was absent on that side, i.e. an add or an
|
||||
/// erase). Symmetric — undo applies the before side, redo the after side — so
|
||||
/// one delta moves between the undo and redo stacks without re-capturing. The
|
||||
/// command by construction changes no layers / objects / blocks, so only the
|
||||
/// cheap scalars (`current_layout`, selection, `dirty`) ride alongside.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct DeltaSnapshot {
|
||||
pub(super) entities: Vec<(Handle, Option<EntityType>, Option<EntityType>)>,
|
||||
pub(super) current_layout: String,
|
||||
pub(super) selected_before: Vec<Handle>,
|
||||
pub(super) selected_after: Vec<Handle>,
|
||||
pub(super) dirty_before: bool,
|
||||
pub(super) dirty_after: bool,
|
||||
pub(super) label: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct HistoryState {
|
||||
pub(super) undo_stack: Vec<HistorySnapshot>,
|
||||
|
||||
Reference in new issue
Block a user