forked from 1b-agent/opencadstudio-upstream
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:
1 parent
ddf348af22
commit
f67f2cf4f5
8 files changed
+804
-26
No files matched your search
+145
-1
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user