forked from 1b-agent/opencadstudio-upstream
feat(blocks): wire Rotate and Flip actions and saved reference grips
Author Rotate actions against Rotation parameters and Flip actions against Flip parameters through the existing palette, BACTION selection flow and native evaluation graph. Keep markers selectable and editable in BEDIT. Rotation grips accept a new direction at the original radius. Flip grips toggle immediately. Evaluate both from immutable definition geometry in Test Block and independent drawing references, preserving per-INSERT angle/state caches across DWG/DXF saves and COPY. Undo/redo includes the native state and representation. Accept parameter names containing spaces at the action parameter prompt. 332 block regressions pass (6 existing opt-in checks ignored), including native binding/marker roundtrips, composed rotation/flip, repeated reversible toggles, COPY isolation, viewport mouse interactions, history and saved-reference reopen. Custom base modes, constrained/chained evaluation and parameter-target actions remain unsupported; Rotate currently binds Rotation parameters.
This commit is contained in:
1 parent
f67f2cf4f5
commit
de7ed432fb
11 files changed
+922
-98
No files matched your search
+98
-25
@@ -21,9 +21,11 @@ impl OpenCADStudio {
|
||||
{
|
||||
return Some(e);
|
||||
}
|
||||
if actions::parameters(&tab.scene.document, s.br_handle, kind.unwrap_or(Kind::Move))
|
||||
.is_empty()
|
||||
{
|
||||
let candidates = kind.map_or_else(
|
||||
|| actions::all_parameters(&tab.scene.document, s.br_handle),
|
||||
|kind| actions::parameters(&tab.scene.document, s.br_handle, kind),
|
||||
);
|
||||
if candidates.is_empty() {
|
||||
return Some("Place a compatible parameter before adding this action.");
|
||||
}
|
||||
None
|
||||
@@ -36,7 +38,7 @@ impl OpenCADStudio {
|
||||
};
|
||||
if !args.is_empty() && kind.is_none() {
|
||||
self.command_line.push_error(
|
||||
crate::t!("Supported actions: Move, Scale. Other action types are not wired yet.")
|
||||
crate::t!("Supported actions: Move, Scale, Rotate, Flip. Other action types are not wired yet.")
|
||||
.as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
@@ -51,7 +53,10 @@ impl OpenCADStudio {
|
||||
tab.id,
|
||||
definition,
|
||||
kind,
|
||||
actions::parameters(&tab.scene.document, definition, kind.unwrap_or(Kind::Move)),
|
||||
kind.map_or_else(
|
||||
|| actions::all_parameters(&tab.scene.document, definition),
|
||||
|kind| actions::parameters(&tab.scene.document, definition, kind),
|
||||
),
|
||||
);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.command_line.set_step_options(command.options());
|
||||
@@ -96,7 +101,7 @@ impl OpenCADStudio {
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.finish_pending_history(i);
|
||||
self.refresh_properties();
|
||||
self.command_line.push_output(crate::tf!("{} action attached. Open Test Block and click the parameter grip to test it.",crate::t!(kind.name())).as_ref());
|
||||
self.command_line.push_output(crate::tf!("{} action attached. Select the reference and use its parameter grip, or open Test Block.",crate::t!(kind.name())).as_ref());
|
||||
}
|
||||
Err(e) => self.command_line.push_error(crate::t!(e).as_ref()),
|
||||
}
|
||||
@@ -106,14 +111,13 @@ impl OpenCADStudio {
|
||||
document,
|
||||
definition,
|
||||
parameter,
|
||||
position,
|
||||
value,
|
||||
} => {
|
||||
if self.tabs[i].id != document {
|
||||
return Task::none();
|
||||
}
|
||||
if let Some(insert) = insert {
|
||||
return self
|
||||
.evaluate_reference_parameter(insert, definition, parameter, position);
|
||||
return self.evaluate_reference_parameter(insert, definition, parameter, value);
|
||||
}
|
||||
let Some(test) = self.tabs[i].block_test.as_ref() else {
|
||||
return Task::none();
|
||||
@@ -141,10 +145,21 @@ impl OpenCADStudio {
|
||||
);
|
||||
return Task::none();
|
||||
};
|
||||
let local = actions::v(inverse.apply(actions::native(position)));
|
||||
let local = match value {
|
||||
actions::Value::Point(at) => {
|
||||
actions::Value::Point(actions::v(inverse.apply(actions::native(at))))
|
||||
}
|
||||
state => state,
|
||||
};
|
||||
let representation = insert.block_name.clone();
|
||||
let mut staged = self.tabs[i].scene.document.clone();
|
||||
match actions::evaluate(&mut staged, &test.baseline, definition, parameter, local) {
|
||||
match actions::evaluate_value(
|
||||
&mut staged,
|
||||
&test.baseline,
|
||||
definition,
|
||||
parameter,
|
||||
local,
|
||||
) {
|
||||
Ok(changed) => {
|
||||
if changed {
|
||||
if let Err(e) = actions::sync_representation(
|
||||
@@ -210,21 +225,28 @@ impl OpenCADStudio {
|
||||
let transform = crate::scene::render_graph::insert_transform(&tab.scene.document, insert);
|
||||
let mut grips = vec![];
|
||||
let all = actions::actions(&test.baseline, test.definition);
|
||||
for (index, p) in actions::parameters(&test.baseline, test.definition, Kind::Move)
|
||||
for (index, p) in actions::all_parameters(&test.baseline, test.definition)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
if !all.iter().any(|(_, _, _, id)| *id == p.id) {
|
||||
continue;
|
||||
}
|
||||
let world = actions::v(
|
||||
transform.apply(actions::native(actions::current(&tab.scene.document, &p))),
|
||||
) - glam::DVec3::from_array(wo);
|
||||
let Ok(value) = actions::value(&tab.scene.document, &p) else {
|
||||
continue;
|
||||
};
|
||||
let world =
|
||||
actions::v(transform.apply(actions::native(actions::grip_position(&p, value))))
|
||||
- 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,
|
||||
shape: if p.flip {
|
||||
crate::scene::model::object::GripShape::Square
|
||||
} else {
|
||||
crate::scene::model::object::GripShape::Triangle
|
||||
},
|
||||
dir: None,
|
||||
axis: None,
|
||||
});
|
||||
@@ -251,12 +273,18 @@ impl OpenCADStudio {
|
||||
if tab.scene.is_layer_locked(handle) {
|
||||
return true;
|
||||
}
|
||||
let Some(p) = actions::parameters(&test.baseline, test.definition, Kind::Move)
|
||||
let Some(p) = actions::all_parameters(&test.baseline, test.definition)
|
||||
.get(id - TEST_GRIP_START)
|
||||
.cloned()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if p.flip {
|
||||
let state = actions::value(&tab.scene.document, &p);
|
||||
let document = tab.id;
|
||||
let definition = test.definition;
|
||||
return self.toggle_action_grip(document, definition, p.handle, None, state);
|
||||
}
|
||||
let command = crate::modules::block_editor::action::TestParameterCommand {
|
||||
insert: None,
|
||||
document: tab.id,
|
||||
@@ -291,23 +319,28 @@ impl OpenCADStudio {
|
||||
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)
|
||||
for (index, p) in actions::all_parameters(&tab.scene.document, def)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
if !all.iter().any(|(_, _, _, id)| *id == p.id) {
|
||||
continue;
|
||||
}
|
||||
let Ok(local) = actions::instance::current(&tab.scene.document, handle, &p) else {
|
||||
let Ok(value) = actions::instance::value(&tab.scene.document, handle, &p) else {
|
||||
continue;
|
||||
};
|
||||
let world =
|
||||
actions::v(transform.apply(actions::native(local))) - glam::DVec3::from_array(wo);
|
||||
actions::v(transform.apply(actions::native(actions::grip_position(&p, value))))
|
||||
- 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,
|
||||
shape: if p.flip {
|
||||
crate::scene::model::object::GripShape::Square
|
||||
} else {
|
||||
crate::scene::model::object::GripShape::Triangle
|
||||
},
|
||||
dir: None,
|
||||
axis: None,
|
||||
});
|
||||
@@ -329,12 +362,17 @@ impl OpenCADStudio {
|
||||
if tab.scene.is_layer_locked(handle) {
|
||||
return true;
|
||||
}
|
||||
let Some(p) = actions::parameters(&tab.scene.document, def, Kind::Move)
|
||||
let Some(p) = actions::all_parameters(&tab.scene.document, def)
|
||||
.get(id - TEST_GRIP_START)
|
||||
.cloned()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if p.flip {
|
||||
let state = actions::instance::value(&tab.scene.document, handle, &p);
|
||||
let document = tab.id;
|
||||
return self.toggle_action_grip(document, def, p.handle, Some(handle), state);
|
||||
}
|
||||
let command = crate::modules::block_editor::action::TestParameterCommand {
|
||||
insert: Some(handle),
|
||||
document: tab.id,
|
||||
@@ -352,7 +390,7 @@ impl OpenCADStudio {
|
||||
handle: Handle,
|
||||
definition: Handle,
|
||||
parameter: Handle,
|
||||
position: glam::DVec3,
|
||||
value: actions::Value,
|
||||
) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let tab = &self.tabs[i];
|
||||
@@ -373,8 +411,13 @@ impl OpenCADStudio {
|
||||
) else {
|
||||
return Task::none();
|
||||
};
|
||||
let local = actions::v(inverse.apply(actions::native(position)));
|
||||
match actions::instance::evaluate(&tab.scene.document, handle, parameter, local) {
|
||||
let local = match value {
|
||||
actions::Value::Point(at) => {
|
||||
actions::Value::Point(actions::v(inverse.apply(actions::native(at))))
|
||||
}
|
||||
state => state,
|
||||
};
|
||||
match actions::instance::evaluate_value(&tab.scene.document, handle, parameter, local) {
|
||||
Ok(staged) => {
|
||||
if let Some(mut staged) = staged {
|
||||
self.push_undo_snapshot(i, "Block reference parameter");
|
||||
@@ -407,3 +450,33 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
fn toggle_action_grip(
|
||||
&mut self,
|
||||
document: u64,
|
||||
definition: Handle,
|
||||
parameter: Handle,
|
||||
insert: Option<Handle>,
|
||||
state: Result<actions::Value, &'static str>,
|
||||
) -> bool {
|
||||
match state {
|
||||
Ok(actions::Value::Flip(state)) => {
|
||||
let _ = self.handle_block_action_result(
|
||||
crate::command::CmdResult::EvaluateBlockParameter {
|
||||
document,
|
||||
definition,
|
||||
parameter,
|
||||
insert,
|
||||
value: actions::Value::Flip(!state),
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(e) => self.command_line.push_error(crate::t!(e).as_ref()),
|
||||
_ => return false,
|
||||
}
|
||||
self.grip_hover = None;
|
||||
self.grip_popup = None;
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,139 @@ fn feed(app: &mut OpenCADStudio, text: &str) {
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_actions_rotate_flip_mouse_grips_authoring_history_and_reopen() {
|
||||
use crate::scene::block_actions::{self as actions, Kind};
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
let source = app.active_tab;
|
||||
let _ = app.run_command_line("BPARAMETER ROTATION 0,0 10 0");
|
||||
let _ = app.run_command_line("BPARAMETER FLIP 0,0 10,0 -2,2");
|
||||
let def = app.tabs[source]
|
||||
.active_block_edit_session()
|
||||
.unwrap()
|
||||
.br_handle;
|
||||
let rotate =
|
||||
actions::parameters(&app.tabs[source].scene.document, def, Kind::Rotate)[0].clone();
|
||||
let flip = actions::parameters(&app.tabs[source].scene.document, def, Kind::Flip)[0].clone();
|
||||
let targets = members(&app);
|
||||
for (kind, p) in [("Rotate", &rotate), ("Flip", &flip)] {
|
||||
let depth = app.tabs[source].history.undo_stack.len();
|
||||
let _ = app.dispatch_command(&format!("BACTION {kind}"));
|
||||
feed(&mut app, &p.name);
|
||||
let _ = app.feed_command(crate::command::StepInput::SelectionComplete(
|
||||
targets.clone(),
|
||||
));
|
||||
let _ = app.feed_command(crate::command::StepInput::Enter);
|
||||
feed(&mut app, "3,4");
|
||||
assert!(app.tabs[source].active_cmd.is_none());
|
||||
assert_eq!(app.tabs[source].history.undo_stack.len(), depth + 1);
|
||||
}
|
||||
let baseline = app.tabs[source].scene.document.clone();
|
||||
let click = |app: &mut OpenCADStudio, world: glam::DVec3| {
|
||||
let scene = &app.tabs[app.active_tab].scene;
|
||||
let (w, h) = scene.selection.borrow().view.vp_size;
|
||||
let tile = scene.active_model_tile_bounds(w, h);
|
||||
let at = scene
|
||||
.camera
|
||||
.borrow()
|
||||
.project(
|
||||
world,
|
||||
iced::Rectangle {
|
||||
x: 0.,
|
||||
y: 0.,
|
||||
width: tile.width,
|
||||
height: tile.height,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let _ = app.update(Message::ViewportLeftPressAt(iced::Point::new(
|
||||
at.x + tile.x,
|
||||
at.y + tile.y,
|
||||
)));
|
||||
let _ = app.update(Message::ViewportLeftRelease);
|
||||
};
|
||||
let check = |app: &OpenCADStudio, insert: Handle, expected: Vector3| {
|
||||
let doc = &app.tabs[app.active_tab].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[0];
|
||||
let EntityType::Line(line) = doc.get_entity(h).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
assert!(
|
||||
line.end.distance(&expected) < 1e-5,
|
||||
"{:?} != {expected:?}",
|
||||
line.end
|
||||
);
|
||||
};
|
||||
// Exercise the exact viewport grip press/release path in both contexts.
|
||||
for test in [true, false] {
|
||||
let insert = if test {
|
||||
let _ = app.dispatch_command("BTESTBLOCK");
|
||||
app.tabs[app.active_tab].block_test.as_ref().unwrap().insert
|
||||
} else {
|
||||
app.active_tab = source;
|
||||
let _ = app.dispatch_command("BSAVE");
|
||||
let _ = app.dispatch_command("BEDIT_DISCARD");
|
||||
reference
|
||||
};
|
||||
let i = app.active_tab;
|
||||
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (1600., 900.);
|
||||
app.tabs[i].scene.camera.borrow_mut().fit_to_bounds_f64(
|
||||
glam::DVec3::new(-4., -3., 0.),
|
||||
glam::DVec3::new(13., 13., 0.),
|
||||
1600. / 900.,
|
||||
);
|
||||
app.tabs[i].scene.selected.insert(insert);
|
||||
app.refresh_properties();
|
||||
click(&mut app, rotate.end);
|
||||
assert!(app.tabs[i].active_cmd.is_some());
|
||||
let depth = app.tabs[i].history.undo_stack.len();
|
||||
click(&mut app, glam::DVec3::new(0., 10., 0.));
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), depth + 1);
|
||||
check(&app, insert, Vector3::UNIT_Y);
|
||||
app.tabs[i].scene.selected.insert(insert);
|
||||
app.refresh_properties();
|
||||
let depth = app.tabs[i].history.undo_stack.len();
|
||||
click(&mut app, flip.reference);
|
||||
assert!(
|
||||
app.tabs[i].active_cmd.is_none(),
|
||||
"Flip toggles immediately without point input"
|
||||
);
|
||||
check(&app, insert, -Vector3::UNIT_Y);
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), depth + 1);
|
||||
app.undo_steps(1);
|
||||
check(&app, insert, Vector3::UNIT_Y);
|
||||
app.redo_steps(1);
|
||||
check(&app, insert, -Vector3::UNIT_Y);
|
||||
if test {
|
||||
assert_eq!(app.tabs[source].scene.document, baseline);
|
||||
let _ = app.dispatch_command("BCLOSETESTBLOCK");
|
||||
} else {
|
||||
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.tabs[i].scene.selected.insert(insert);
|
||||
app.refresh_properties();
|
||||
click(&mut app, flip.reference);
|
||||
check(&app, insert, Vector3::UNIT_Y);
|
||||
let EntityType::Line(original) =
|
||||
app.tabs[i].scene.document.get_entity(targets[0]).unwrap()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(original.end, Vector3::UNIT_X);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_actions_drawing_grips_history_cancel_transform_and_reopen() {
|
||||
use crate::scene::block_actions::{self as actions, Kind};
|
||||
@@ -601,7 +734,7 @@ fn block_actions_command_selection_cancel_undo_and_test_block_isolation() {
|
||||
document: id,
|
||||
definition,
|
||||
parameter: p.handle,
|
||||
position: glam::DVec3::new(30., 0., 0.),
|
||||
value: crate::scene::block_actions::Value::Point(glam::DVec3::new(30., 0., 0.)),
|
||||
});
|
||||
assert_eq!(
|
||||
app.tabs[test].scene.document, shared_before,
|
||||
@@ -682,7 +815,7 @@ fn block_actions_test_reference_transform_visibility_and_noop_history() {
|
||||
document: id,
|
||||
definition,
|
||||
parameter: p.handle,
|
||||
position: current_world,
|
||||
value: crate::scene::block_actions::Value::Point(current_world),
|
||||
});
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), depth);
|
||||
// Existing visibility UI creates a per-reference anonymous definition.
|
||||
@@ -721,7 +854,7 @@ fn block_actions_test_reference_transform_visibility_and_noop_history() {
|
||||
document: id,
|
||||
definition,
|
||||
parameter: p.handle,
|
||||
position: world,
|
||||
value: crate::scene::block_actions::Value::Point(world),
|
||||
});
|
||||
let anon = app.tabs[i]
|
||||
.scene
|
||||
|
||||
@@ -1244,6 +1244,44 @@ fn block_actions_palette_controls_require_compatible_parameters_and_start_comman
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_actions_rotate_flip_palette_requires_matching_parameters() {
|
||||
with_stack(|| {
|
||||
use crate::ui::window::authoring_palettes::Tab;
|
||||
let mut app = authoring_app();
|
||||
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||||
let point = authoring_control(&app, Tab::Actions.widget_id());
|
||||
authoring_click(&mut app, point);
|
||||
for id in ["authoring-tool-rotate-action", "authoring-tool-flip-action"] {
|
||||
let point = authoring_control(&app, id);
|
||||
authoring_click(&mut app, point);
|
||||
assert!(app.tabs[app.active_tab].active_cmd.is_none());
|
||||
}
|
||||
let _ = app.run_command_line("BPARAMETER ROTATION 0,0 2 0");
|
||||
let point = authoring_control(&app, "authoring-tool-flip-action");
|
||||
authoring_click(&mut app, point);
|
||||
assert!(app.tabs[app.active_tab].active_cmd.is_none());
|
||||
let point = authoring_control(&app, "authoring-tool-rotate-action");
|
||||
authoring_click(&mut app, point);
|
||||
assert!(app.tabs[app.active_tab]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.prompt()
|
||||
.contains("Rotate"));
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
let _ = app.run_command_line("BPARAMETER FLIP 0,0 2,0 -1,1");
|
||||
let point = authoring_control(&app, "authoring-tool-flip-action");
|
||||
authoring_click(&mut app, point);
|
||||
assert!(app.tabs[app.active_tab]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.prompt()
|
||||
.contains("Flip"));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookup_basepoint_palette_and_table_dialog_real_controls() {
|
||||
with_stack(|| {
|
||||
|
||||
@@ -413,6 +413,8 @@ impl OpenCADStudio {
|
||||
self.lookup_table_error(),
|
||||
self.action_error(Some(crate::scene::block_actions::Kind::Move)),
|
||||
self.action_error(Some(crate::scene::block_actions::Kind::Scale)),
|
||||
self.action_error(Some(crate::scene::block_actions::Kind::Rotate)),
|
||||
self.action_error(Some(crate::scene::block_actions::Kind::Flip)),
|
||||
)
|
||||
}
|
||||
crate::ui::dock::PanelId::BlockPalette => {
|
||||
|
||||
Reference in new issue
Block a user