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:
Karim Jerbi committed 2026-10-17 11:35:26 +01:00
1 parent ee846914dc
commit 5a6a7fab93
10 files changed
+1317 -191

No files matched your search

+332
View File
@@ -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};