forked from 1b-agent/opencadstudio-upstream
feat(blocks): synchronize definition geometry changes to evaluated references
Update evaluated references upon saving block definition edits while preserving each reference's independent parameter values, visibility states, and undo history. Reconcile representation members by native element identity rather than positional index, preserving handles, extension dictionaries, reactors, nested attribute handles, and 3D polyline vertex handles. Prune erased targets from action dependencies, synchronize anonymous block base points, and prevent element ID collisions across undo/redo. Coalesce representation updates into editor undo deltas and composite structure snapshots for symmetric undo/redo.
This commit is contained in:
1 parent
ee846914dc
commit
5a6a7fab93
10 files changed
+1317
-191
No files matched your search
@@ -62,14 +62,22 @@ impl OpenCADStudio {
|
||||
self.tabs[self.active_tab].active_cmd = Some(Box::new(command));
|
||||
Task::none()
|
||||
}
|
||||
fn apply_action_document(&mut self, i: usize, mut staged: codec::CadDocument) {
|
||||
pub(super) fn apply_action_document(&mut self, i: usize, mut staged: codec::CadDocument) {
|
||||
let doc = &mut self.tabs[i].scene.document;
|
||||
let changes = staged
|
||||
.entities()
|
||||
.filter(|e| doc.get_entity(e.common().handle) != Some(*e))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let removed = doc
|
||||
.entities()
|
||||
.map(|e| e.common().handle)
|
||||
.filter(|h| staged.get_entity(*h).is_none())
|
||||
.collect::<Vec<_>>();
|
||||
doc.swap_structure(staged.snapshot_structure());
|
||||
for handle in removed {
|
||||
doc.remove_entity_arc(handle);
|
||||
}
|
||||
for entity in changes {
|
||||
let handle = entity.common().handle;
|
||||
if let Some(current) = doc.get_entity_mut(handle) {
|
||||
|
||||
+76
-14
@@ -161,6 +161,8 @@ impl OpenCADStudio {
|
||||
candidate_handles.insert(insert.common.handle);
|
||||
if let Some(record) = document.block_records.get(&insert.block_name) {
|
||||
candidate_handles.extend(record.entity_handles.iter().copied());
|
||||
candidate_handles.insert(record.block_entity_handle);
|
||||
candidate_handles.insert(record.block_end_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,15 +170,10 @@ impl OpenCADStudio {
|
||||
}
|
||||
let mut sub_handles = Vec::new();
|
||||
for &h in &candidate_handles {
|
||||
let entity_opt = before_doc
|
||||
.get_entity(h)
|
||||
.or_else(|| tab.scene.document.get_entity(h));
|
||||
if let Some(EntityType::Insert(ins)) = entity_opt {
|
||||
for attr in &ins.attributes {
|
||||
sub_handles.push(attr.common.handle);
|
||||
}
|
||||
if let Some(seqend) = ins.seqend_handle {
|
||||
sub_handles.push(seqend);
|
||||
for document in [before_doc, &tab.scene.document] {
|
||||
if let Some(EntityType::Insert(insert)) = document.get_entity(h) {
|
||||
sub_handles.extend(insert.attributes.iter().map(|a| a.common.handle));
|
||||
sub_handles.extend(insert.seqend_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -578,6 +575,49 @@ impl OpenCADStudio {
|
||||
true
|
||||
}
|
||||
|
||||
/// Save-time representation edits belong to the preceding editor command,
|
||||
/// so local Undo cannot leave untracked members or reuse their handles.
|
||||
fn coalesce_saved_representations(&mut self, i: usize, before: &CadDocument) {
|
||||
let tab = &mut self.tabs[i];
|
||||
let doc = &tab.scene.document;
|
||||
if before == doc {
|
||||
return;
|
||||
}
|
||||
let Some(HistorySnapshot::Delta(delta)) = tab.history.undo_stack.last_mut() else {
|
||||
return;
|
||||
};
|
||||
let handles = before
|
||||
.entities()
|
||||
.chain(doc.entities())
|
||||
.map(|e| e.common().handle)
|
||||
.collect::<FxHashSet<_>>();
|
||||
for handle in handles {
|
||||
let a = before.get_entity_arc(handle);
|
||||
let b = doc.get_entity_arc(handle);
|
||||
if a == b {
|
||||
continue;
|
||||
}
|
||||
if let Some((_, _, after)) = delta.entities.iter_mut().find(|(h, _, _)| *h == handle) {
|
||||
*after = b;
|
||||
} else {
|
||||
delta.entities.push((handle, a, b));
|
||||
}
|
||||
}
|
||||
if !delta
|
||||
.structure
|
||||
.as_ref()
|
||||
.is_some_and(StructureSnapshot::is_full)
|
||||
{
|
||||
let mut previous = before.clone();
|
||||
let broad = StructureSnapshot::Full(previous.snapshot_structure());
|
||||
delta.structure = Some(match delta.structure.take() {
|
||||
Some(specialized) => StructureSnapshot::Combined(vec![broad, specialized]),
|
||||
None => broad,
|
||||
});
|
||||
}
|
||||
delta.dirty_after = true;
|
||||
}
|
||||
|
||||
pub(super) fn save_block_edit(&mut self, i: usize) -> bool {
|
||||
let Some(session) = self.tabs[i].active_block_edit_session() else {
|
||||
self.command_line
|
||||
@@ -617,11 +657,33 @@ impl OpenCADStudio {
|
||||
// A dynamic block leaves the editor showing its first visibility
|
||||
// state, with erased objects dropped from its states.
|
||||
let definition = session.br_handle;
|
||||
crate::scene::block_visibility::finish_editing(
|
||||
&mut self.tabs[i].scene.document,
|
||||
definition,
|
||||
);
|
||||
self.tabs[i].scene.visibility_states.remove(&definition);
|
||||
if self.block_edit_delta(i, true).is_some() {
|
||||
self.reconcile_block_edit_membership(i);
|
||||
self.finish_pending_history(i);
|
||||
let before_sync = self.tabs[i].scene.document.clone();
|
||||
let mut staged = before_sync.clone();
|
||||
crate::scene::block_visibility::finish_editing(&mut staged, definition);
|
||||
crate::scene::block_actions::prune_erased_targets(&mut staged, definition);
|
||||
let before = &self.tabs[i]
|
||||
.block_edit_history
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.checkpoint
|
||||
.document;
|
||||
match crate::scene::block_actions::instance::refresh_after_edit(
|
||||
&staged, before, definition,
|
||||
) {
|
||||
Ok(document) => {
|
||||
self.apply_action_document(i, document);
|
||||
self.coalesce_saved_representations(i, &before_sync);
|
||||
}
|
||||
Err(error) => {
|
||||
self.command_line.push_error(crate::t!(error).as_ref());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.visibility_states.remove(&definition);
|
||||
}
|
||||
if let Some(delta) = self.block_edit_delta(i, true) {
|
||||
let mut history = self.tabs[i].block_edit_history.take().unwrap();
|
||||
std::mem::swap(&mut self.tabs[i].history, &mut history.main);
|
||||
|
||||
@@ -68,6 +68,338 @@ fn edit_action_set(app: &mut OpenCADStudio, action: Handle, mode: &str, targets:
|
||||
assert!(app.tabs[app.active_tab].active_cmd.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_actions_topology_save_updates_values_members_properties_and_history() {
|
||||
use crate::scene::block_actions::{self as actions, Kind};
|
||||
let (mut app, reference) = fixture();
|
||||
let i = app.active_tab;
|
||||
let second = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.add_entity(EntityType::Insert(Insert::new(
|
||||
"EDIT_ME",
|
||||
Vector3::new(40., 0., 0.),
|
||||
)))
|
||||
.unwrap();
|
||||
begin(&mut app, reference);
|
||||
circle(&mut app, 3.);
|
||||
let _ = app.run_command_line("BPARAMETER LINEAR 0,0 10,0 5,2");
|
||||
let def = app.tabs[i].active_block_edit_session().unwrap().br_handle;
|
||||
let p = actions::parameters(&app.tabs[i].scene.document, def, Kind::Move)[0].handle;
|
||||
let old_members = members(&app);
|
||||
let a = actions::create(
|
||||
&mut app.tabs[i].scene.document,
|
||||
def,
|
||||
Kind::Move,
|
||||
p,
|
||||
&old_members[..1],
|
||||
glam::DVec3::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
let _ = app.dispatch_command("BSAVE");
|
||||
let _ = app.dispatch_command("BCLOSE");
|
||||
for (insert, x) in [(reference, 12.), (second, 54.)] {
|
||||
let _ = app.handle_block_action_result(crate::command::CmdResult::EvaluateBlockParameter {
|
||||
insert: Some(insert),
|
||||
document: app.tabs[i].id,
|
||||
definition: def,
|
||||
parameter: p,
|
||||
value: actions::Value::Point(glam::DVec3::new(x, 0., 0.)),
|
||||
});
|
||||
}
|
||||
let check = |app: &OpenCADStudio, end: f64, center: f64| {
|
||||
let doc = &app.tabs[app.active_tab].scene.document;
|
||||
for (insert, delta) in [(reference, 2.), (second, 4.)] {
|
||||
let EntityType::Insert(ins) = doc.get_entity(insert).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
let handles = &doc
|
||||
.block_records
|
||||
.get(&ins.block_name)
|
||||
.unwrap()
|
||||
.entity_handles;
|
||||
assert_eq!(handles.len(), 2);
|
||||
let EntityType::Line(line) = doc.get_entity(handles[0]).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(line.start.x, delta);
|
||||
assert_eq!(line.end.x, end + delta);
|
||||
let EntityType::Circle(circle) = doc.get_entity(handles[1]).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(circle.center.x, center);
|
||||
}
|
||||
};
|
||||
check(&app, 1., 3.);
|
||||
begin(&mut app, reference);
|
||||
app.push_undo_snapshot(i, "Definition edit");
|
||||
let EntityType::Line(line) = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity_mut(old_members[0])
|
||||
.unwrap()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
line.end.x = 2.;
|
||||
line.common.color = codec::types::Color::Index(3);
|
||||
app.tabs[i].scene.erase_entities(&old_members[1..]);
|
||||
app.finish_pending_history(i);
|
||||
circle(&mut app, 7.);
|
||||
let added = members(&app)[1];
|
||||
let _ = app.dispatch_command("BSAVE");
|
||||
check(&app, 2., 7.);
|
||||
let saved = app.tabs[i].scene.document.clone();
|
||||
let depth = main_depth(&app);
|
||||
let _ = app.dispatch_command("BSAVE");
|
||||
assert_eq!(main_depth(&app), depth);
|
||||
assert_eq!(
|
||||
app.tabs[i].scene.document, saved,
|
||||
"saving unchanged authoring does not rebuild again"
|
||||
);
|
||||
let _ = app.dispatch_command("BCLOSE");
|
||||
app.undo_steps(1);
|
||||
check(&app, 1., 3.);
|
||||
assert!(app.tabs[i].scene.document.get_entity(added).is_none());
|
||||
app.redo_steps(1);
|
||||
check(&app, 2., 7.);
|
||||
for insert in [reference, second] {
|
||||
let doc = &app.tabs[i].scene.document;
|
||||
let EntityType::Insert(ins) = doc.get_entity(insert).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
let member = doc
|
||||
.block_records
|
||||
.get(&ins.block_name)
|
||||
.unwrap()
|
||||
.entity_handles[0];
|
||||
assert_eq!(
|
||||
doc.get_entity(member).unwrap().common().color,
|
||||
codec::types::Color::Index(3)
|
||||
);
|
||||
}
|
||||
let bytes = codec::DwgWriter::write_to_vec(&app.tabs[i].scene.document).unwrap();
|
||||
app.tabs[i].scene.document = codec::DwgReader::from_stream(std::io::Cursor::new(bytes))
|
||||
.read()
|
||||
.unwrap();
|
||||
check(&app, 2., 7.);
|
||||
begin(&mut app, reference);
|
||||
edit_action_set(&mut app, a, "Add", vec![added]);
|
||||
let _ = app.dispatch_command("BSAVE");
|
||||
let _ = app.dispatch_command("BCLOSE");
|
||||
// New geometry can now join an action even on previously evaluated references.
|
||||
for (insert, center) in [(reference, 9.), (second, 11.)] {
|
||||
let doc = &app.tabs[i].scene.document;
|
||||
let EntityType::Insert(ins) = doc.get_entity(insert).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
let h = doc
|
||||
.block_records
|
||||
.get(&ins.block_name)
|
||||
.unwrap()
|
||||
.entity_handles[1];
|
||||
let EntityType::Circle(circle) = doc.get_entity(h).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(circle.center.x, center);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_actions_topology_save_coalesces_bounded_layer_history_in_both_directions() {
|
||||
use crate::scene::block_actions::{self as actions, Kind};
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
let i = app.active_tab;
|
||||
let _ = app.run_command_line("BPARAMETER LINEAR 0,0 10,0 5,2");
|
||||
let def = app.tabs[i].active_block_edit_session().unwrap().br_handle;
|
||||
let p = actions::parameters(&app.tabs[i].scene.document, def, Kind::Move)[0].handle;
|
||||
let targets = members(&app);
|
||||
actions::create(
|
||||
&mut app.tabs[i].scene.document,
|
||||
def,
|
||||
Kind::Move,
|
||||
p,
|
||||
&targets,
|
||||
glam::DVec3::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
let _ = app.dispatch_command("BSAVE");
|
||||
let _ = app.dispatch_command("BCLOSE");
|
||||
let _ = app.handle_block_action_result(crate::command::CmdResult::EvaluateBlockParameter {
|
||||
insert: Some(reference),
|
||||
document: app.tabs[i].id,
|
||||
definition: def,
|
||||
parameter: p,
|
||||
value: actions::Value::Point(glam::DVec3::new(12., 0., 0.)),
|
||||
});
|
||||
begin(&mut app, reference);
|
||||
circle(&mut app, 3.);
|
||||
let original = app.tabs[i].scene.document.layers.get("0").unwrap().color;
|
||||
let pending = app.begin_layer_undo(i, "Layer color", &["0".into()]);
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
.get_mut("0")
|
||||
.unwrap()
|
||||
.color = codec::types::Color::Index(1);
|
||||
app.commit_layer_undo(i, pending);
|
||||
let _ = app.dispatch_command("BSAVE");
|
||||
assert!(
|
||||
matches!(app.tabs[i].history.undo_stack.last(),Some(super::document::HistorySnapshot::Delta(d)) if matches!(d.structure,Some(super::document::StructureSnapshot::Combined(_))))
|
||||
);
|
||||
app.undo_steps(1);
|
||||
assert_eq!(
|
||||
app.tabs[i].scene.document.layers.get("0").unwrap().color,
|
||||
original
|
||||
);
|
||||
app.undo_steps(1);
|
||||
assert_eq!(members(&app).len(), 1);
|
||||
app.redo_steps(2);
|
||||
let doc = &app.tabs[i].scene.document;
|
||||
assert_eq!(
|
||||
doc.layers.get("0").unwrap().color,
|
||||
codec::types::Color::Index(1)
|
||||
);
|
||||
let EntityType::Insert(ins) = doc.get_entity(reference).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
let record = doc.block_records.get(&ins.block_name).unwrap();
|
||||
assert_eq!(record.entity_handles.len(), 2);
|
||||
assert!(record
|
||||
.entity_handles
|
||||
.iter()
|
||||
.all(|h| doc.get_entity(*h).is_some()));
|
||||
assert_eq!(members(&app).len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_actions_topology_local_undo_after_save_resave_has_no_orphan_members() {
|
||||
use crate::scene::block_actions::{self as actions, Kind};
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
let i = app.active_tab;
|
||||
let _ = app.run_command_line("BPARAMETER LINEAR 0,0 10,0 5,2");
|
||||
let def = app.tabs[i].active_block_edit_session().unwrap().br_handle;
|
||||
let p = actions::parameters(&app.tabs[i].scene.document, def, Kind::Move)[0].handle;
|
||||
let targets = members(&app);
|
||||
actions::create(
|
||||
&mut app.tabs[i].scene.document,
|
||||
def,
|
||||
Kind::Move,
|
||||
p,
|
||||
&targets,
|
||||
glam::DVec3::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
let _ = app.dispatch_command("BSAVE");
|
||||
let _ = app.dispatch_command("BCLOSE");
|
||||
let _ = app.handle_block_action_result(crate::command::CmdResult::EvaluateBlockParameter {
|
||||
insert: Some(reference),
|
||||
document: app.tabs[i].id,
|
||||
definition: def,
|
||||
parameter: p,
|
||||
value: actions::Value::Point(glam::DVec3::new(12., 0., 0.)),
|
||||
});
|
||||
begin(&mut app, reference);
|
||||
circle(&mut app, 3.);
|
||||
let _ = app.dispatch_command("BSAVE");
|
||||
app.undo_steps(1);
|
||||
circle(&mut app, 7.);
|
||||
let _ = app.dispatch_command("BSAVE");
|
||||
let _ = app.dispatch_command("BCLOSE");
|
||||
let doc = &app.tabs[i].scene.document;
|
||||
let EntityType::Insert(ins) = doc.get_entity(reference).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
let record = doc.block_records.get(&ins.block_name).unwrap();
|
||||
assert_eq!(record.entity_handles.len(), 2);
|
||||
let owned = doc
|
||||
.entities()
|
||||
.filter(|e| {
|
||||
e.common().owner_handle == record.handle
|
||||
&& !matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_))
|
||||
})
|
||||
.count();
|
||||
assert_eq!(owned, 2);
|
||||
let EntityType::Circle(circle) = doc.get_entity(record.entity_handles[1]).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(circle.center.x, 7.);
|
||||
let bytes = codec::DwgWriter::write_to_vec(doc).unwrap();
|
||||
let reopened = codec::DwgReader::from_stream(std::io::Cursor::new(bytes))
|
||||
.read()
|
||||
.unwrap();
|
||||
let record = reopened.block_records.get(&ins.block_name).unwrap();
|
||||
assert_eq!(record.entity_handles.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_actions_topology_erase_last_target_empty_definition_discard_and_save() {
|
||||
use crate::scene::block_actions::{self as actions, Kind};
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
let i = app.active_tab;
|
||||
let _ = app.run_command_line("BPARAMETER LINEAR 0,0 10,0 5,2");
|
||||
let def = app.tabs[i].active_block_edit_session().unwrap().br_handle;
|
||||
let p = actions::parameters(&app.tabs[i].scene.document, def, Kind::Move)[0].handle;
|
||||
let targets = members(&app);
|
||||
let a = actions::create(
|
||||
&mut app.tabs[i].scene.document,
|
||||
def,
|
||||
Kind::Move,
|
||||
p,
|
||||
&targets,
|
||||
glam::DVec3::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
let _ = app.dispatch_command("BSAVE");
|
||||
let _ = app.dispatch_command("BCLOSE");
|
||||
let _ = app.handle_block_action_result(crate::command::CmdResult::EvaluateBlockParameter {
|
||||
insert: Some(reference),
|
||||
document: app.tabs[i].id,
|
||||
definition: def,
|
||||
parameter: p,
|
||||
value: actions::Value::Point(glam::DVec3::new(12., 0., 0.)),
|
||||
});
|
||||
let original = app.tabs[i].scene.document.clone();
|
||||
begin(&mut app, reference);
|
||||
circle(&mut app, 5.);
|
||||
let _ = app.dispatch_command("BEDIT_DISCARD");
|
||||
assert_eq!(app.tabs[i].scene.document, original);
|
||||
begin(&mut app, reference);
|
||||
app.push_undo_snapshot(i, "Erase targets");
|
||||
app.tabs[i].scene.erase_entities(&targets);
|
||||
app.finish_pending_history(i);
|
||||
let _ = app.dispatch_command("BSAVE");
|
||||
let _ = app.dispatch_command("BCLOSE");
|
||||
assert!(actions::actions(&app.tabs[i].scene.document, def)[0]
|
||||
.1
|
||||
.dependencies
|
||||
.is_empty());
|
||||
assert!(app.tabs[i].scene.document.objects.contains_key(&a));
|
||||
let EntityType::Insert(ins) = app.tabs[i].scene.document.get_entity(reference).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
assert!(app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get(&ins.block_name)
|
||||
.unwrap()
|
||||
.entity_handles
|
||||
.is_empty());
|
||||
app.undo_steps(1);
|
||||
assert_eq!(
|
||||
app.tabs[i].scene.document.get_entity(reference),
|
||||
original.get_entity(reference)
|
||||
);
|
||||
assert!(app.tabs[i].scene.document.get_entity(targets[0]).is_some());
|
||||
app.redo_steps(1);
|
||||
assert!(app.tabs[i].scene.document.get_entity(targets[0]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_actions_set_edit_refresh_local_history_discard_save_and_drawing_history() {
|
||||
use crate::scene::block_actions::{self as actions, Kind};
|
||||
|
||||
@@ -876,6 +876,8 @@ pub(super) struct DeltaSnapshot {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) enum StructureSnapshot {
|
||||
/// A broad save-time synchronization followed by a command's bounded edits.
|
||||
Combined(Vec<StructureSnapshot>),
|
||||
/// Compatibility fallback for genuinely broad structural commands.
|
||||
Full(CadDocument),
|
||||
/// Exact layer-table entries touched by one command.
|
||||
@@ -901,6 +903,7 @@ pub(super) enum StructureSnapshot {
|
||||
impl StructureSnapshot {
|
||||
pub(super) fn estimated_bytes(&self) -> usize {
|
||||
match self {
|
||||
Self::Combined(parts) => parts.iter().map(Self::estimated_bytes).sum(),
|
||||
Self::Full(doc) => doc.objects.len().saturating_mul(192),
|
||||
Self::Layers(entries) => entries.len().saturating_mul(256),
|
||||
Self::TextStyles(entries) => entries.len().saturating_mul(320),
|
||||
@@ -914,6 +917,7 @@ impl StructureSnapshot {
|
||||
|
||||
pub(super) fn is_full(&self) -> bool {
|
||||
matches!(self, Self::Full(_))
|
||||
|| matches!(self,Self::Combined(parts) if parts.iter().any(Self::is_full))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+142
-136
@@ -962,6 +962,143 @@ impl OpenCADStudio {
|
||||
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
|
||||
}
|
||||
|
||||
fn apply_structure_state(&mut self, i: usize, structure: &mut StructureSnapshot, undo: bool) {
|
||||
match structure {
|
||||
StructureSnapshot::Combined(parts) => {
|
||||
if undo {
|
||||
for part in parts {
|
||||
self.apply_structure_state(i, part, undo);
|
||||
}
|
||||
} else {
|
||||
for part in parts.iter_mut().rev() {
|
||||
self.apply_structure_state(i, part, undo);
|
||||
}
|
||||
}
|
||||
}
|
||||
StructureSnapshot::Full(stored) => {
|
||||
let inverse = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.swap_structure(std::mem::replace(stored, codec::CadDocument::new()));
|
||||
*stored = inverse;
|
||||
self.tabs[i].scene.invalidate_dependency_index();
|
||||
self.tabs[i].scene.bump_layout_epoch();
|
||||
self.tabs[i].scene.bump_scale_epoch();
|
||||
self.tabs[i].scene.bump_ucs_epoch();
|
||||
}
|
||||
StructureSnapshot::Layers(entries) => {
|
||||
let names: Vec<String> = entries.iter().map(|entry| entry.name.clone()).collect();
|
||||
for entry in entries {
|
||||
let value = if undo {
|
||||
entry.before.clone()
|
||||
} else {
|
||||
entry.after.clone()
|
||||
};
|
||||
if let Some(layer) = value {
|
||||
self.tabs[i].scene.document.layers.add_or_replace(layer);
|
||||
} else {
|
||||
self.tabs[i].scene.document.layers.remove(&entry.name);
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
}
|
||||
StructureSnapshot::TextStyles(entries) => {
|
||||
let names: Vec<String> = entries.iter().map(|entry| entry.name.clone()).collect();
|
||||
for entry in entries {
|
||||
let value = if undo {
|
||||
entry.before.clone()
|
||||
} else {
|
||||
entry.after.clone()
|
||||
};
|
||||
if let Some(style) = value {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.text_styles
|
||||
.add_or_replace(style);
|
||||
} else {
|
||||
self.tabs[i].scene.document.text_styles.remove(&entry.name);
|
||||
}
|
||||
}
|
||||
for name in names {
|
||||
self.tabs[i].scene.invalidate_text_style_dependencies(&name);
|
||||
}
|
||||
}
|
||||
StructureSnapshot::DimStyles(entries) => {
|
||||
let names: Vec<String> = entries.iter().map(|entry| entry.name.clone()).collect();
|
||||
for entry in entries {
|
||||
let value = if undo {
|
||||
entry.before.clone()
|
||||
} else {
|
||||
entry.after.clone()
|
||||
};
|
||||
if let Some(style) = value {
|
||||
self.tabs[i].scene.document.dim_styles.add_or_replace(style);
|
||||
} else {
|
||||
self.tabs[i].scene.document.dim_styles.remove(&entry.name);
|
||||
}
|
||||
}
|
||||
for name in names {
|
||||
self.tabs[i].scene.invalidate_dim_style_dependencies(&name);
|
||||
}
|
||||
}
|
||||
StructureSnapshot::Objects(entries) => {
|
||||
let mut layout_touched = false;
|
||||
let mut scale_touched = false;
|
||||
for entry in entries {
|
||||
let value = if undo {
|
||||
entry.before.clone()
|
||||
} else {
|
||||
entry.after.clone()
|
||||
};
|
||||
// Either direction can add/remove a layout: undoing
|
||||
// an insert removes it (`before` is `None`), redoing
|
||||
// re-inserts it — so both sides must be inspected.
|
||||
layout_touched |=
|
||||
matches!(entry.before, Some(codec::objects::ObjectType::Layout(_)))
|
||||
|| matches!(entry.after, Some(codec::objects::ObjectType::Layout(_)));
|
||||
scale_touched |=
|
||||
matches!(entry.before, Some(codec::objects::ObjectType::Scale(_)))
|
||||
|| matches!(entry.after, Some(codec::objects::ObjectType::Scale(_)));
|
||||
if let Some(object) = value {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.insert(entry.handle, object);
|
||||
} else {
|
||||
self.tabs[i].scene.document.objects.remove(&entry.handle);
|
||||
}
|
||||
}
|
||||
if layout_touched {
|
||||
self.tabs[i].scene.bump_layout_epoch();
|
||||
}
|
||||
if scale_touched {
|
||||
self.tabs[i].scene.bump_scale_epoch();
|
||||
}
|
||||
}
|
||||
StructureSnapshot::Styles {
|
||||
before,
|
||||
after,
|
||||
text_names,
|
||||
dim_names,
|
||||
object_handles,
|
||||
} => {
|
||||
let snapshot = if undo { before } else { after };
|
||||
self.restore_style_state(i, snapshot);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.invalidate_text_style_dependencies_many(text_names);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.invalidate_dim_style_dependencies_many(dim_names);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.invalidate_object_style_dependencies(object_handles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -977,138 +1114,7 @@ impl OpenCADStudio {
|
||||
// and UI invalidation are deferred until every requested undo/redo step
|
||||
// has been applied.
|
||||
if let Some(structure) = d.structure.as_mut() {
|
||||
match structure {
|
||||
StructureSnapshot::Full(stored) => {
|
||||
let inverse = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.swap_structure(std::mem::replace(stored, codec::CadDocument::new()));
|
||||
*stored = inverse;
|
||||
self.tabs[i].scene.invalidate_dependency_index();
|
||||
self.tabs[i].scene.bump_layout_epoch();
|
||||
self.tabs[i].scene.bump_scale_epoch();
|
||||
self.tabs[i].scene.bump_ucs_epoch();
|
||||
}
|
||||
StructureSnapshot::Layers(entries) => {
|
||||
let names: Vec<String> =
|
||||
entries.iter().map(|entry| entry.name.clone()).collect();
|
||||
for entry in entries {
|
||||
let value = if undo {
|
||||
entry.before.clone()
|
||||
} else {
|
||||
entry.after.clone()
|
||||
};
|
||||
if let Some(layer) = value {
|
||||
self.tabs[i].scene.document.layers.add_or_replace(layer);
|
||||
} else {
|
||||
self.tabs[i].scene.document.layers.remove(&entry.name);
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
}
|
||||
StructureSnapshot::TextStyles(entries) => {
|
||||
let names: Vec<String> =
|
||||
entries.iter().map(|entry| entry.name.clone()).collect();
|
||||
for entry in entries {
|
||||
let value = if undo {
|
||||
entry.before.clone()
|
||||
} else {
|
||||
entry.after.clone()
|
||||
};
|
||||
if let Some(style) = value {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.text_styles
|
||||
.add_or_replace(style);
|
||||
} else {
|
||||
self.tabs[i].scene.document.text_styles.remove(&entry.name);
|
||||
}
|
||||
}
|
||||
for name in names {
|
||||
self.tabs[i].scene.invalidate_text_style_dependencies(&name);
|
||||
}
|
||||
}
|
||||
StructureSnapshot::DimStyles(entries) => {
|
||||
let names: Vec<String> =
|
||||
entries.iter().map(|entry| entry.name.clone()).collect();
|
||||
for entry in entries {
|
||||
let value = if undo {
|
||||
entry.before.clone()
|
||||
} else {
|
||||
entry.after.clone()
|
||||
};
|
||||
if let Some(style) = value {
|
||||
self.tabs[i].scene.document.dim_styles.add_or_replace(style);
|
||||
} else {
|
||||
self.tabs[i].scene.document.dim_styles.remove(&entry.name);
|
||||
}
|
||||
}
|
||||
for name in names {
|
||||
self.tabs[i].scene.invalidate_dim_style_dependencies(&name);
|
||||
}
|
||||
}
|
||||
StructureSnapshot::Objects(entries) => {
|
||||
let mut layout_touched = false;
|
||||
let mut scale_touched = false;
|
||||
for entry in entries {
|
||||
let value = if undo {
|
||||
entry.before.clone()
|
||||
} else {
|
||||
entry.after.clone()
|
||||
};
|
||||
// Either direction can add/remove a layout: undoing
|
||||
// an insert removes it (`before` is `None`), redoing
|
||||
// re-inserts it — so both sides must be inspected.
|
||||
layout_touched |=
|
||||
matches!(entry.before, Some(codec::objects::ObjectType::Layout(_)))
|
||||
|| matches!(
|
||||
entry.after,
|
||||
Some(codec::objects::ObjectType::Layout(_))
|
||||
);
|
||||
scale_touched |=
|
||||
matches!(entry.before, Some(codec::objects::ObjectType::Scale(_)))
|
||||
|| matches!(
|
||||
entry.after,
|
||||
Some(codec::objects::ObjectType::Scale(_))
|
||||
);
|
||||
if let Some(object) = value {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.insert(entry.handle, object);
|
||||
} else {
|
||||
self.tabs[i].scene.document.objects.remove(&entry.handle);
|
||||
}
|
||||
}
|
||||
if layout_touched {
|
||||
self.tabs[i].scene.bump_layout_epoch();
|
||||
}
|
||||
if scale_touched {
|
||||
self.tabs[i].scene.bump_scale_epoch();
|
||||
}
|
||||
}
|
||||
StructureSnapshot::Styles {
|
||||
before,
|
||||
after,
|
||||
text_names,
|
||||
dim_names,
|
||||
object_handles,
|
||||
} => {
|
||||
let snapshot = if undo { before } else { after };
|
||||
self.restore_style_state(i, snapshot);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.invalidate_text_style_dependencies_many(text_names);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.invalidate_dim_style_dependencies_many(dim_names);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.invalidate_object_style_dependencies(object_handles);
|
||||
}
|
||||
}
|
||||
self.apply_structure_state(i, structure, undo);
|
||||
}
|
||||
let mut changes = self.tabs[i].scene.apply_entity_delta(&d.entities, undo);
|
||||
let scene = &mut self.tabs[i].scene;
|
||||
@@ -1341,8 +1347,8 @@ impl OpenCADStudio {
|
||||
layer_panel_changed |= d.structure.as_ref().is_some_and(|structure| {
|
||||
matches!(
|
||||
structure,
|
||||
StructureSnapshot::Full(_) | StructureSnapshot::Layers(_)
|
||||
)
|
||||
StructureSnapshot::Layers(_)
|
||||
) || structure.is_full()
|
||||
}) || d.active_layer.is_some()
|
||||
|| d.entities.iter().any(|(_, before, after)| {
|
||||
before
|
||||
@@ -1411,8 +1417,8 @@ impl OpenCADStudio {
|
||||
layer_panel_changed |= d.structure.as_ref().is_some_and(|structure| {
|
||||
matches!(
|
||||
structure,
|
||||
StructureSnapshot::Full(_) | StructureSnapshot::Layers(_)
|
||||
)
|
||||
StructureSnapshot::Layers(_)
|
||||
) || structure.is_full()
|
||||
}) || d.active_layer.is_some()
|
||||
|| d.entities.iter().any(|(_, before, after)| {
|
||||
before
|
||||
|
||||
Reference in new issue
Block a user