feat(blocks): persist independent Move and Scale reference grips

Selected drawing references now expose action parameter grips outside Test
Block. Point input evaluates immutable definition geometry into an independent
anonymous representation and stores per-INSERT values in native AppDataCache
XRecords, preserving definition graphs and sibling references.

Reuse visibility representation isolation, including shared COPY blocks, and
retain cache metadata. Record geometry, INSERT links, new block records and
parameter values in one undo step. Support transformed uniform references and
Point, Linear, Polar and XY endpoints for the existing Move/Scale actions.

Validate synthetic DWG/DXF persistence, re-edit/resave, COPY isolation,
XY/Polar cache layouts, metadata, cancellation, transformed grips and undo/redo.
327 block regressions pass (6 existing opt-in checks ignored).
This commit is contained in:
Karim Jerbi committed 2026-10-17 08:48:26 +01:00
1 parent ddf348af22
commit f67f2cf4f5
8 files changed
+804 -26

No files matched your search

+145 -1
View File
@@ -102,6 +102,7 @@ impl OpenCADStudio {
}
}
crate::command::CmdResult::EvaluateBlockParameter {
insert,
document,
definition,
parameter,
@@ -110,6 +111,10 @@ impl OpenCADStudio {
if self.tabs[i].id != document {
return Task::none();
}
if let Some(insert) = insert {
return self
.evaluate_reference_parameter(insert, definition, parameter, position);
}
let Some(test) = self.tabs[i].block_test.as_ref() else {
return Task::none();
};
@@ -193,6 +198,7 @@ impl OpenCADStudio {
let i = self.active_tab;
let tab = &self.tabs[i];
let Some(test) = tab.block_test.as_ref() else {
self.refresh_reference_action_grips(wo);
return;
};
if tab.selected_handle != Some(test.insert) {
@@ -237,7 +243,7 @@ impl OpenCADStudio {
let i = self.active_tab;
let tab = &self.tabs[i];
let Some(test) = tab.block_test.as_ref() else {
return false;
return self.start_reference_action_grip(handle, id);
};
if handle != test.insert {
return false;
@@ -252,6 +258,7 @@ impl OpenCADStudio {
return false;
};
let command = crate::modules::block_editor::action::TestParameterCommand {
insert: None,
document: tab.id,
definition: test.definition,
parameter: p.handle,
@@ -263,3 +270,140 @@ impl OpenCADStudio {
true
}
}
impl OpenCADStudio {
fn refresh_reference_action_grips(&mut self, wo: [f64; 3]) {
let i = self.active_tab;
let tab = &self.tabs[i];
if tab.active_block_edit_session().is_some() || tab.scene.selected.len() != 1 {
return;
}
let Some(handle) = tab.selected_handle else {
return;
};
let Some(def) = actions::instance::definition(&tab.scene.document, handle) else {
return;
};
let Some(EntityType::Insert(reference)) = tab.scene.document.get_entity(handle) else {
return;
};
let transform =
crate::scene::render_graph::insert_transform(&tab.scene.document, reference);
let all = actions::actions(&tab.scene.document, def);
let mut grips = vec![];
for (index, p) in actions::parameters(&tab.scene.document, def, Kind::Move)
.into_iter()
.enumerate()
{
if !all.iter().any(|(_, _, _, id)| *id == p.id) {
continue;
}
let Ok(local) = actions::instance::current(&tab.scene.document, handle, &p) else {
continue;
};
let world =
actions::v(transform.apply(actions::native(local))) - glam::DVec3::from_array(wo);
grips.push(crate::scene::model::object::GripDef {
id: TEST_GRIP_START + index,
world,
is_midpoint: false,
shape: crate::scene::model::object::GripShape::Triangle,
dir: None,
axis: None,
});
}
for grip in grips {
self.tabs[i].selected_grips.push(grip);
self.tabs[i].selected_grip_handles.push(handle);
}
}
fn start_reference_action_grip(&mut self, handle: Handle, id: usize) -> bool {
let i = self.active_tab;
let tab = &self.tabs[i];
if tab.active_block_edit_session().is_some() || tab.selected_handle != Some(handle) {
return false;
}
let Some(def) = actions::instance::definition(&tab.scene.document, handle) else {
return false;
};
if tab.scene.is_layer_locked(handle) {
return true;
}
let Some(p) = actions::parameters(&tab.scene.document, def, Kind::Move)
.get(id - TEST_GRIP_START)
.cloned()
else {
return false;
};
let command = crate::modules::block_editor::action::TestParameterCommand {
insert: Some(handle),
document: tab.id,
definition: def,
parameter: p.handle,
};
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
self.grip_hover = None;
self.grip_popup = None;
true
}
fn evaluate_reference_parameter(
&mut self,
handle: Handle,
definition: Handle,
parameter: Handle,
position: glam::DVec3,
) -> Task<Message> {
let i = self.active_tab;
let tab = &self.tabs[i];
if tab.active_block_edit_session().is_some()
|| tab.block_test.is_some()
|| actions::instance::definition(&tab.scene.document, handle) != Some(definition)
{
return Task::none();
}
if tab.scene.is_layer_locked(handle) {
return Task::none();
}
let Some(EntityType::Insert(reference)) = tab.scene.document.get_entity(handle) else {
return Task::none();
};
let Some(inverse) = crate::scene::inverse_affine(
&crate::scene::render_graph::insert_transform(&tab.scene.document, reference),
) else {
return Task::none();
};
let local = actions::v(inverse.apply(actions::native(position)));
match actions::instance::evaluate(&tab.scene.document, handle, parameter, local) {
Ok(staged) => {
if let Some(mut staged) = staged {
self.push_undo_snapshot(i, "Block reference parameter");
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<_>>();
// Structure swapping retains the live store and its transaction recorder.
doc.swap_structure(staged.snapshot_structure());
for entity in changes {
let h = entity.common().handle;
if let Some(current) = doc.get_entity_mut(h) {
*current = entity
} else {
doc.restore_entity_arc(std::sync::Arc::new(entity));
}
}
self.tabs[i].scene.bump_geometry();
self.tabs[i].dirty = true;
self.finish_pending_history(i);
}
self.tabs[i].active_cmd = None;
self.tabs[i].scene.clear_preview_wire();
self.refresh_properties();
}
Err(e) => self.command_line.push_error(crate::t!(e).as_ref()),
}
Task::none()
}
}
+122
View File
@@ -59,6 +59,125 @@ fn feed(app: &mut OpenCADStudio, text: &str) {
let _ = app.update(Message::CommandSubmit);
}
#[test]
fn block_actions_drawing_grips_history_cancel_transform_and_reopen() {
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].clone();
let targets = members(&app);
actions::create(
&mut app.tabs[i].scene.document,
def,
Kind::Move,
p.handle,
&targets,
glam::DVec3::ZERO,
)
.unwrap();
let _ = app.dispatch_command("BSAVE");
let _ = app.dispatch_command("BEDIT_DISCARD");
let sibling = app.tabs[i]
.scene
.document
.add_entity(EntityType::Insert(Insert::new(
"EDIT_ME",
Vector3::new(40., 0., 0.),
)))
.unwrap();
let EntityType::Insert(ins) = app.tabs[i]
.scene
.document
.get_entity_mut(reference)
.unwrap()
else {
panic!()
};
ins.insert_point = Vector3::new(20., 30., 0.);
ins.rotation = std::f64::consts::FRAC_PI_2;
ins.set_x_scale(2.);
ins.set_y_scale(2.);
ins.set_z_scale(2.);
let original = app.tabs[i].scene.document.clone();
app.tabs[i].scene.selected.insert(reference);
app.tabs[i].selected_handle = Some(reference);
app.refresh_properties();
let grip = app.tabs[i]
.selected_grips
.iter()
.find(|g| super::block_actions::is_test_grip(g.id))
.unwrap()
.clone();
assert!(grip.world.distance(glam::DVec3::new(20., 50., 0.)) < 1e-8);
let depth = app.tabs[i].history.undo_stack.len();
assert!(app.start_test_action_grip(reference, grip.id));
let _ = app.update(Message::CommandEscape);
assert_eq!(app.tabs[i].scene.document, original);
assert_eq!(app.tabs[i].history.undo_stack.len(), depth);
// Escape also clears the ordinary drawing selection.
app.tabs[i].scene.selected.insert(reference);
app.refresh_properties();
assert!(app.start_test_action_grip(reference, grip.id));
feed(&mut app, "20,70");
assert!(app.tabs[i].active_cmd.is_none());
let changed = app.tabs[i].scene.document.clone();
assert_eq!(app.tabs[i].history.undo_stack.len(), depth + 1);
let EntityType::Insert(ins) = changed.get_entity(reference).unwrap() else {
panic!()
};
assert_ne!(ins.block_name, "EDIT_ME");
let member = changed
.block_records
.get(&ins.block_name)
.unwrap()
.entity_handles[0];
let EntityType::Line(line) = changed.get_entity(member).unwrap() else {
panic!()
};
assert!((line.start.x - 10.).abs() < 1e-8);
assert_eq!(
changed.get_entity(targets[0]),
original.get_entity(targets[0])
);
assert_eq!(changed.get_entity(sibling), original.get_entity(sibling));
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(member).is_none());
app.redo_steps(1);
assert_eq!(
app.tabs[i].scene.document.get_entity(member),
changed.get_entity(member)
);
assert_eq!(
app.tabs[i].scene.document.get_entity(reference),
changed.get_entity(reference)
);
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();
app.refresh_properties();
let grip = app.tabs[i]
.selected_grips
.iter()
.find(|g| super::block_actions::is_test_grip(g.id))
.unwrap();
assert!(grip.world.distance(glam::DVec3::new(20., 70., 0.)) < 1e-8);
let id = grip.id;
assert!(app.start_test_action_grip(reference, id));
feed(&mut app, "20,50");
let EntityType::Line(line) = app.tabs[i].scene.document.get_entity(member).unwrap() else {
panic!()
};
assert!(line.start.x.abs() < 1e-8);
}
#[test]
fn authoring_edit_basepoint_attribute_relocation_undo_and_cancel() {
let (mut app, reference) = fixture();
@@ -478,6 +597,7 @@ fn block_actions_command_selection_cancel_undo_and_test_block_isolation() {
let shared_before = app.tabs[test].scene.document.clone();
let id = app.tabs[test].id;
let _ = app.handle_block_action_result(crate::command::CmdResult::EvaluateBlockParameter {
insert: None,
document: id,
definition,
parameter: p.handle,
@@ -558,6 +678,7 @@ fn block_actions_test_reference_transform_visibility_and_noop_history() {
.apply(actions::native(p.end)),
);
let _ = app.handle_block_action_result(crate::command::CmdResult::EvaluateBlockParameter {
insert: None,
document: id,
definition,
parameter: p.handle,
@@ -596,6 +717,7 @@ fn block_actions_test_reference_transform_visibility_and_noop_history() {
);
let before = app.tabs[i].scene.document.clone();
let _ = app.handle_block_action_result(crate::command::CmdResult::EvaluateBlockParameter {
insert: None,
document: id,
definition,
parameter: p.handle,