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
@@ -230,17 +230,27 @@ impl OpenCADStudio {
|
||||
self.update_cont_anchor(&entity);
|
||||
}
|
||||
let label = self.history_label_from_active_cmd(i, "ENTITY");
|
||||
self.push_undo_snapshot(i, label);
|
||||
// A plain drawable added on an existing layer touches only the
|
||||
// one new entity; a block/image/dimension/viewport add also
|
||||
// mutates layers/objects/blocks → keep the full snapshot.
|
||||
let delta_safe = self.delta_add_safe(i, &entity);
|
||||
let pending = self.begin_undo(i, label, 1, delta_safe);
|
||||
self.commit_entity(entity);
|
||||
self.tabs[i].dirty = true;
|
||||
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
||||
if let Some(p) = prompt {
|
||||
self.command_line.push_info(&p);
|
||||
}
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
CmdResult::TransformSelected(handles, transform) => {
|
||||
let label = self.history_label_from_active_cmd(i, "MOVE");
|
||||
self.push_undo_snapshot(i, label);
|
||||
// A move/rotate/scale/mirror mutates only the selected entities
|
||||
// (and their baked dimension sub-entities) through
|
||||
// transform_entities — always delta-safe.
|
||||
let pending = self.begin_undo(i, label, handles.len(), true);
|
||||
self.tabs[i].scene.transform_entities(&handles, &transform);
|
||||
// ACIS solids render from a cached mesh, so a move/rotate/
|
||||
// scale/mirror needs the mesh re-tessellated from the now-moved
|
||||
@@ -255,10 +265,16 @@ impl OpenCADStudio {
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.refresh_properties();
|
||||
if let Some(p) = pending {
|
||||
self.commit_undo_delta(i, p);
|
||||
}
|
||||
}
|
||||
CmdResult::CopySelected(handles, transform) => {
|
||||
let label = self.history_label_from_active_cmd(i, "COPY");
|
||||
self.push_undo_snapshot(i, label);
|
||||
// Copying a dimension clones a *D block record, so gate delta on
|
||||
// the selection being dimension-free.
|
||||
let delta_safe = self.delta_copy_safe(i, &handles);
|
||||
let pending = self.begin_undo(i, label, handles.len(), delta_safe);
|
||||
let new_handles = self.tabs[i].scene.copy_entities(&handles, &transform);
|
||||
if self.tabs[i].scene.any_solid(&new_handles) {
|
||||
self.tabs[i].scene.populate_meshes_from_document();
|
||||
@@ -274,6 +290,9 @@ impl OpenCADStudio {
|
||||
self.command_line.push_info(&p);
|
||||
}
|
||||
self.refresh_properties();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
CmdResult::CommitAndExit(entity) => {
|
||||
// For XATTACH: ensure the xref block definition exists before
|
||||
@@ -301,13 +320,17 @@ impl OpenCADStudio {
|
||||
// (before `entity` is moved into commit_entity).
|
||||
self.update_cont_anchor(&entity);
|
||||
let label = self.history_label_from_active_cmd(i, "ENTITY");
|
||||
self.push_undo_snapshot(i, label);
|
||||
let delta_safe = self.delta_add_safe(i, &entity);
|
||||
let pending = self.begin_undo(i, label, 1, delta_safe);
|
||||
self.commit_entity(entity);
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
CmdResult::CommitSolid { entity, solid } => {
|
||||
let label = self.history_label_from_active_cmd(i, "SOLID");
|
||||
@@ -321,7 +344,8 @@ impl OpenCADStudio {
|
||||
}
|
||||
CmdResult::CommitAndEditText(entity) => {
|
||||
let label = self.history_label_from_active_cmd(i, "ENTITY");
|
||||
self.push_undo_snapshot(i, label);
|
||||
let delta_safe = self.delta_add_safe(i, &entity);
|
||||
let pending = self.begin_undo(i, label, 1, delta_safe);
|
||||
let handle = self.commit_entity_handle(entity);
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
@@ -329,6 +353,9 @@ impl OpenCADStudio {
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.ribbon.deactivate_tool();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
if let Some(h) = handle {
|
||||
return self.begin_text_edit(h);
|
||||
}
|
||||
@@ -419,8 +446,12 @@ impl OpenCADStudio {
|
||||
}
|
||||
CmdResult::BatchCopy(handles, transforms) => {
|
||||
let label = self.history_label_from_active_cmd(i, "ARRAY");
|
||||
self.push_undo_snapshot(i, label.clone());
|
||||
let count = transforms.len();
|
||||
// Same gate as COPY (dimension-free), sized by the total number
|
||||
// of copies the array will add.
|
||||
let delta_safe = self.delta_copy_safe(i, &handles);
|
||||
let pending =
|
||||
self.begin_undo(i, label.clone(), handles.len() * count, delta_safe);
|
||||
for t in &transforms {
|
||||
self.tabs[i].scene.copy_entities(&handles, t);
|
||||
}
|
||||
@@ -433,6 +464,9 @@ impl OpenCADStudio {
|
||||
self.command_line
|
||||
.push_output(&format!("{label}: {count} {noun} created."));
|
||||
self.refresh_properties();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
CmdResult::ReplaceMany(replacements, additions) => {
|
||||
let label = self.history_label_from_active_cmd(i, "FILLET");
|
||||
@@ -962,7 +996,10 @@ impl OpenCADStudio {
|
||||
self.restore_pre_cmd_tangent();
|
||||
} else {
|
||||
let label = self.history_label_from_active_cmd(i, "ALIGN");
|
||||
self.push_undo_snapshot(i, label);
|
||||
// A chain of transform_entities on the same handles — the
|
||||
// recording keeps each entity's first (pre-align) image, so
|
||||
// it's delta-safe like a single transform.
|
||||
let pending = self.begin_undo(i, label, handles.len(), true);
|
||||
// Step 1: translate so src1 is at origin
|
||||
self.tabs[i].scene.transform_entities(
|
||||
&handles,
|
||||
@@ -1004,6 +1041,9 @@ impl OpenCADStudio {
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.command_line.push_output("ALIGN: applied.");
|
||||
self.refresh_properties();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
}
|
||||
CmdResult::LengthenEntity {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+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>,
|
||||
|
||||
+213
-15
@@ -1,6 +1,26 @@
|
||||
use super::{document::HistorySnapshot, OpenCADStudio};
|
||||
use super::{
|
||||
document::{DeltaSnapshot, FullSnapshot, HistorySnapshot},
|
||||
OpenCADStudio,
|
||||
};
|
||||
use acadrust::{EntityType, Handle};
|
||||
use rustc_hash::FxHashSet as HashSet;
|
||||
|
||||
/// Above this many touched entities a "delta-safe" command falls back to a full
|
||||
/// snapshot: deep-cloning that many before-images would cost more than the (now
|
||||
/// Arc-cheap) document clone, and huge bulk edits are rare. Below it, the delta
|
||||
/// keeps a per-edit undo at roughly zero cost.
|
||||
const DELTA_UNDO_MAX_ENTITIES: usize = 5000;
|
||||
|
||||
/// Pre-command state captured by [`OpenCADStudio::begin_undo`] and handed back to
|
||||
/// [`OpenCADStudio::commit_undo_delta`] to close a delta entry. Lives on the
|
||||
/// stack across the (synchronous) command body — no per-tab field needed.
|
||||
pub(super) struct PendingDelta {
|
||||
label: String,
|
||||
current_layout: String,
|
||||
selected_before: Vec<Handle>,
|
||||
dirty_before: bool,
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn history_label_from_active_cmd(&self, i: usize, fallback: &'static str) -> String {
|
||||
self.tabs[i]
|
||||
@@ -15,13 +35,13 @@ impl OpenCADStudio {
|
||||
i: usize,
|
||||
label: impl Into<String>,
|
||||
) -> HistorySnapshot {
|
||||
HistorySnapshot {
|
||||
HistorySnapshot::Full(FullSnapshot {
|
||||
document: self.tabs[i].scene.document.clone(),
|
||||
current_layout: self.tabs[i].scene.current_layout.clone(),
|
||||
selected: self.tabs[i].scene.selected.iter().copied().collect(),
|
||||
dirty: self.tabs[i].dirty,
|
||||
label: label.into(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn push_undo_snapshot(&mut self, i: usize, label: impl Into<String>) {
|
||||
@@ -30,7 +50,168 @@ impl OpenCADStudio {
|
||||
self.tabs[i].history.redo_stack.clear();
|
||||
}
|
||||
|
||||
pub(super) fn restore_history_snapshot(&mut self, i: usize, snapshot: HistorySnapshot) {
|
||||
/// Begin undo capture for an entity edit that will touch `touched` entities.
|
||||
/// When `delta_safe` (the caller's per-command predicate guarantees the edit
|
||||
/// mutates only entities, through the five Scene primitives) and the edit is
|
||||
/// small enough, starts a cheap Scene delta recording and returns the
|
||||
/// pre-command state to pass to [`OpenCADStudio::commit_undo_delta`] after the
|
||||
/// mutation. Otherwise pushes a full snapshot and returns `None`.
|
||||
pub(super) fn begin_undo(
|
||||
&mut self,
|
||||
i: usize,
|
||||
label: impl Into<String>,
|
||||
touched: usize,
|
||||
delta_safe: bool,
|
||||
) -> Option<PendingDelta> {
|
||||
let label = label.into();
|
||||
if delta_safe && touched <= DELTA_UNDO_MAX_ENTITIES {
|
||||
self.tabs[i].scene.begin_undo_recording();
|
||||
Some(PendingDelta {
|
||||
label,
|
||||
current_layout: self.tabs[i].scene.current_layout.clone(),
|
||||
selected_before: self.tabs[i].scene.selected.iter().copied().collect(),
|
||||
dirty_before: self.tabs[i].dirty,
|
||||
})
|
||||
} else {
|
||||
self.push_undo_snapshot(i, label);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy is delta-safe only when no target is a Dimension: copying a
|
||||
/// dimension clones a fresh anonymous `*D` block record (non-entity state a
|
||||
/// pure-entity delta can't restore).
|
||||
pub(super) fn delta_copy_safe(&self, i: usize, handles: &[Handle]) -> bool {
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
!handles
|
||||
.iter()
|
||||
.any(|h| matches!(doc.get_entity(*h), Some(EntityType::Dimension(_))))
|
||||
}
|
||||
|
||||
/// Erase is delta-safe only when no target belongs to a group: erasing a
|
||||
/// grouped entity rewrites the group's membership in `document.objects`.
|
||||
pub(super) fn delta_erase_safe(&self, i: usize, handles: &[Handle]) -> bool {
|
||||
use acadrust::objects::ObjectType;
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
!doc.objects.values().any(|o| match o {
|
||||
ObjectType::Group(g) => g.entities.iter().any(|h| handles.contains(h)),
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Add is delta-safe only for a plain drawable on an already-existing layer:
|
||||
/// an insert / block / image / dimension add also creates block records,
|
||||
/// image definitions or layers (non-entity state).
|
||||
pub(super) fn delta_add_safe(&self, i: usize, entity: &EntityType) -> bool {
|
||||
if matches!(
|
||||
entity,
|
||||
EntityType::Insert(_)
|
||||
| EntityType::Block(_)
|
||||
| EntityType::BlockEnd(_)
|
||||
| EntityType::RasterImage(_)
|
||||
| EntityType::Dimension(_)
|
||||
// A viewport commit routes through add_entity_to_layout +
|
||||
// bump_geometry_no_blocks, bypassing the recorded Scene::add_entity
|
||||
// — nothing would be captured, so keep the full snapshot.
|
||||
| EntityType::Viewport(_)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
let layer = entity.common().layer.clone();
|
||||
layer.trim().is_empty() || self.tabs[i].scene.document.layers.contains(&layer)
|
||||
}
|
||||
|
||||
/// Close the delta transaction opened by [`OpenCADStudio::begin_undo`]:
|
||||
/// harvests the recorded before-images, pairs each with the entity's current
|
||||
/// (after) state, and pushes a symmetric [`DeltaSnapshot`] onto the undo
|
||||
/// stack. Called after the command's mutations (and after `dirty`/selection
|
||||
/// reach their final values).
|
||||
pub(super) fn commit_undo_delta(&mut self, i: usize, pending: PendingDelta) {
|
||||
let Some(rec) = self.tabs[i].scene.take_undo_recording() else {
|
||||
return;
|
||||
};
|
||||
// The predicate should have kept the command entity-only; if a primitive
|
||||
// still poisoned the recording we've already mutated and the pre-command
|
||||
// full state is gone, so the entity delta is the best we can do (its
|
||||
// entity part stays correct; a leaked layer/group/block just won't
|
||||
// revert). Loud in debug, a warning in release — never a silent wrong.
|
||||
debug_assert!(
|
||||
!rec.is_poisoned(),
|
||||
"delta command '{}' mutated non-entity state",
|
||||
pending.label
|
||||
);
|
||||
if rec.is_empty() {
|
||||
// Nothing actually changed (e.g. every target was on a locked
|
||||
// layer). Leave the undo/redo stacks untouched.
|
||||
return;
|
||||
}
|
||||
if rec.is_poisoned() {
|
||||
eprintln!(
|
||||
"[undo] delta '{}' touched non-entity state; undo may be incomplete",
|
||||
pending.label
|
||||
);
|
||||
}
|
||||
let entities: Vec<(Handle, Option<EntityType>, Option<EntityType>)> = rec
|
||||
.into_before_images()
|
||||
.into_iter()
|
||||
.map(|(h, before)| {
|
||||
let after = self.tabs[i].scene.document.get_entity(h).cloned();
|
||||
(h, before, after)
|
||||
})
|
||||
.collect();
|
||||
let selected_after = self.tabs[i].scene.selected.iter().copied().collect();
|
||||
let dirty_after = self.tabs[i].dirty;
|
||||
let delta = DeltaSnapshot {
|
||||
entities,
|
||||
current_layout: pending.current_layout,
|
||||
selected_before: pending.selected_before,
|
||||
selected_after,
|
||||
dirty_before: pending.dirty_before,
|
||||
dirty_after,
|
||||
label: pending.label,
|
||||
};
|
||||
self.tabs[i]
|
||||
.history
|
||||
.undo_stack
|
||||
.push(HistorySnapshot::Delta(delta));
|
||||
self.tabs[i].history.redo_stack.clear();
|
||||
}
|
||||
|
||||
/// Apply one side of a delta entry in place: `undo` restores each entity's
|
||||
/// before-image, `!undo` (redo) its after-image. `None` on the chosen side
|
||||
/// means the entity is absent there (erase it). Reports the exact touched
|
||||
/// handles through `bump_entities` so the incremental caches patch per-handle
|
||||
/// — no full re-tessellation, no `populate_*` document walk.
|
||||
fn apply_delta(&mut self, i: usize, d: &DeltaSnapshot, undo: bool) {
|
||||
// Install the chosen side of every entity image (Scene handles the
|
||||
// in-place / re-insert / remove, the derived-cache reseed and the
|
||||
// block-record dedup); we report the touched handles incrementally.
|
||||
let changes = self.tabs[i].scene.apply_entity_delta(&d.entities, undo);
|
||||
let scene = &mut self.tabs[i].scene;
|
||||
scene.bump_entities(&changes);
|
||||
let (sel, dirty) = if undo {
|
||||
(&d.selected_before, d.dirty_before)
|
||||
} else {
|
||||
(&d.selected_after, d.dirty_after)
|
||||
};
|
||||
let restored: HashSet<Handle> = sel
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|h| scene.document.get_entity(*h).is_some())
|
||||
.collect();
|
||||
scene.selected = restored;
|
||||
// Delta commands never change the layout; this is a no-op unless a
|
||||
// future caller widens the delta scope, in which case it stays correct.
|
||||
scene.set_current_layout(d.current_layout.clone());
|
||||
scene.clear_preview_wire();
|
||||
self.tabs[i].dirty = dirty;
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].active_grip = None;
|
||||
self.refresh_properties();
|
||||
}
|
||||
|
||||
pub(super) fn restore_history_snapshot(&mut self, i: usize, snapshot: FullSnapshot) {
|
||||
self.tabs[i].scene.document = snapshot.document;
|
||||
self.tabs[i].scene.set_current_layout(snapshot.current_layout);
|
||||
// Force a re-tessellation: the cached wires were keyed against the
|
||||
@@ -85,11 +266,21 @@ impl OpenCADStudio {
|
||||
let Some(snapshot) = self.tabs[i].history.undo_stack.pop() else {
|
||||
break;
|
||||
};
|
||||
let label = snapshot.label.clone();
|
||||
let current = self.capture_history_snapshot(i, label.clone());
|
||||
self.tabs[i].history.redo_stack.push(current);
|
||||
self.restore_history_snapshot(i, snapshot);
|
||||
last_label = label;
|
||||
last_label = snapshot.label().to_string();
|
||||
match snapshot {
|
||||
HistorySnapshot::Full(f) => {
|
||||
let current = self.capture_history_snapshot(i, f.label.clone());
|
||||
self.tabs[i].history.redo_stack.push(current);
|
||||
self.restore_history_snapshot(i, f);
|
||||
}
|
||||
HistorySnapshot::Delta(d) => {
|
||||
// Symmetric: undo applies the before side, then the same
|
||||
// delta rides to the redo stack (it still holds the after
|
||||
// side) — no current-state capture needed.
|
||||
self.apply_delta(i, &d, true);
|
||||
self.tabs[i].history.redo_stack.push(HistorySnapshot::Delta(d));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.command_line
|
||||
.push_output(&format!("Undo: {last_label}"));
|
||||
@@ -109,11 +300,18 @@ impl OpenCADStudio {
|
||||
let Some(snapshot) = self.tabs[i].history.redo_stack.pop() else {
|
||||
break;
|
||||
};
|
||||
let label = snapshot.label.clone();
|
||||
let current = self.capture_history_snapshot(i, label.clone());
|
||||
self.tabs[i].history.undo_stack.push(current);
|
||||
self.restore_history_snapshot(i, snapshot);
|
||||
last_label = label;
|
||||
last_label = snapshot.label().to_string();
|
||||
match snapshot {
|
||||
HistorySnapshot::Full(f) => {
|
||||
let current = self.capture_history_snapshot(i, f.label.clone());
|
||||
self.tabs[i].history.undo_stack.push(current);
|
||||
self.restore_history_snapshot(i, f);
|
||||
}
|
||||
HistorySnapshot::Delta(d) => {
|
||||
self.apply_delta(i, &d, false);
|
||||
self.tabs[i].history.undo_stack.push(HistorySnapshot::Delta(d));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.command_line
|
||||
.push_output(&format!("Redo: {last_label}"));
|
||||
@@ -124,6 +322,6 @@ pub(super) fn history_dropdown_labels(stack: &[HistorySnapshot]) -> Vec<String>
|
||||
stack
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|snapshot| snapshot.label.clone())
|
||||
.map(|snapshot| snapshot.label().to_string())
|
||||
.collect()
|
||||
}
|
||||
@@ -2047,7 +2047,10 @@ impl OpenCADStudio {
|
||||
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
|
||||
let handles: Vec<_> = self.tabs[i].scene.selected.iter().cloned().collect();
|
||||
if !handles.is_empty() {
|
||||
self.push_undo_snapshot(i, "ERASE");
|
||||
// Erase is delta-safe unless a target is in a group (group
|
||||
// cleanup rewrites document.objects).
|
||||
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()
|
||||
@@ -2056,6 +2059,9 @@ impl OpenCADStudio {
|
||||
self.tabs[i].scene.erase_entities(&handles);
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user