Files
OneCAD-Draft/src/app/block_actions.rs
T
Karim Jerbi 042f26a4cc feat(blocks): wire Stretch actions and independent reference evaluation
Author a native Stretch action from a Linear, Polar or XY parameter, a two-corner stretch frame, an object selection and a marker. Preview and retain its frame in BEDIT, with selectable and movable action markers.

Freeze line and planar lightweight-polyline vertex bindings, preserve curved-segment height, move wholly selected objects, and evaluate independent saved INSERTs from their definition. Retain native DWG/DXF data, copied-reference isolation and undo/redo.

Validate with synthetic frame, topology, parameter, history, Test Block, drawing-reference, COPY and roundtrip regressions: 340 block tests pass (6 existing opt-in ignored), plus 41 dock and 27 viewport tests. Other entity types, Polar Stretch, custom multipliers and chaining remain unsupported.
2026-10-17 10:03:59 +01:00

491 lines
20 KiB
Rust

use super::{Message, OpenCADStudio};
use crate::command::CadCommand;
use crate::scene::block_actions::{self as actions, Kind};
use codec::{EntityType, Handle};
use iced::Task;
// These grips start an ordinary point-input command, keeping snapping, typed
// coordinates and Escape on the command driver's existing path.
const TEST_GRIP_START: usize = usize::MAX / 2;
pub(super) fn is_test_grip(id: usize) -> bool {
(TEST_GRIP_START..usize::MAX).contains(&id)
}
impl OpenCADStudio {
pub(in crate::app) fn action_error(&self, kind: Option<Kind>) -> Option<&'static str> {
let tab = &self.tabs[self.active_tab];
let Some(s) = tab.active_block_edit_session() else {
return Some("Open a block in the Block Editor before adding actions.");
};
if let Some(e) =
crate::scene::block_point_parameter::placement_error(&tab.scene.document, s.br_handle)
{
return Some(e);
}
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
}
pub(in crate::app) fn start_block_action(&mut self, args: &str) -> Task<Message> {
let kind = if args.is_empty() {
None
} else {
Kind::parse(args)
};
if !args.is_empty() && kind.is_none() {
self.command_line.push_error(
crate::t!("Supported actions: Move, Scale, Stretch, Rotate, Flip. Other action types are not wired yet.")
.as_ref(),
);
return Task::none();
}
if let Some(e) = self.action_error(kind) {
self.command_line.push_error(crate::t!(e).as_ref());
return Task::none();
}
let tab = &self.tabs[self.active_tab];
let definition = tab.active_block_edit_session().unwrap().br_handle;
let command = crate::modules::block_editor::action::ActionCommand::new(
tab.id,
definition,
kind,
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());
self.tabs[self.active_tab].active_cmd = Some(Box::new(command));
Task::none()
}
pub(in crate::app) fn handle_block_action_result(
&mut self,
result: crate::command::CmdResult,
) -> Task<Message> {
let i = self.active_tab;
match result {
crate::command::CmdResult::PlaceBlockAction {
document,
definition,
kind,
parameter,
targets,
location,
frame,
} => {
if self.tabs[i].id != document
|| self.tabs[i]
.active_block_edit_session()
.map(|s| s.br_handle)
!= Some(definition)
{
self.command_line.push_error(
crate::t!("The action's Block Editor session is no longer active.")
.as_ref(),
);
return Task::none();
}
let mut staged = self.tabs[i].scene.document.clone();
match actions::create_with_frame(
&mut staged,
definition,
kind,
parameter,
&targets,
location,
frame,
) {
Ok(_) => {
self.push_undo_snapshot(i, &format!("{} action", kind.name()));
self.tabs[i].scene.document = staged;
self.tabs[i].scene.bump_geometry();
self.tabs[i].dirty = true;
self.tabs[i].active_cmd = None;
self.tabs[i].scene.clear_preview_wire();
self.finish_pending_history(i);
self.refresh_properties();
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()),
}
}
crate::command::CmdResult::EvaluateBlockParameter {
insert,
document,
definition,
parameter,
value,
} => {
if self.tabs[i].id != document {
return Task::none();
}
if let Some(insert) = insert {
return self.evaluate_reference_parameter(insert, definition, parameter, value);
}
let Some(test) = self.tabs[i].block_test.as_ref() else {
return Task::none();
};
if test.definition != definition {
return Task::none();
}
let Some(EntityType::Insert(insert)) =
self.tabs[i].scene.document.get_entity(test.insert)
else {
return Task::none();
};
let doc = &self.tabs[i].scene.document;
if doc.entities().any(|e|matches!(e,EntityType::Insert(other) if other.common.handle!=test.insert && (other.block_name==insert.block_name||doc.dynamic_definition_for_insert(other.common.handle)==Some(definition)))) {
self.command_line.push_error(crate::t!("Test actions with one reference in Test Block. Remove the extra references or reopen Test Block.").as_ref());return Task::none();
}
let Some(inverse) =
crate::scene::inverse_affine(&crate::scene::render_graph::insert_transform(
&self.tabs[i].scene.document,
insert,
))
else {
self.command_line.push_error(
crate::t!("The test reference has a singular transform.").as_ref(),
);
return Task::none();
};
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_value(
&mut staged,
&test.baseline,
definition,
parameter,
local,
) {
Ok(changed) => {
if changed {
if let Err(e) = actions::sync_representation(
&mut staged,
definition,
&representation,
) {
self.command_line.push_error(crate::t!(e).as_ref());
return Task::none();
}
self.finish_pending_history(i);
let before = &self.tabs[i].scene.document;
let entities = before
.entities()
.filter_map(|e| {
let h = e.common().handle;
(staged.get_entity(h) != Some(e))
.then(|| (h, before.get_entity_arc(h).unwrap()))
})
.collect();
let objects = before
.objects
.iter()
.filter(|(h, o)| staged.objects.get(h) != Some(*o))
.map(|(h, o)| (*h, o.clone()))
.collect();
let dirty_before = self.tabs[i].dirty;
self.tabs[i].scene.document = staged;
self.tabs[i].scene.bump_geometry();
self.tabs[i].dirty = true;
self.push_entity_group_history(
i,
"Test block parameter",
entities,
objects,
dirty_before,
);
}
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()),
}
}
_ => unreachable!(),
}
Task::none()
}
pub(super) fn refresh_test_action_grips(&mut self, wo: [f64; 3]) {
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) {
return;
}
let Some(EntityType::Insert(insert)) = tab.scene.document.get_entity(test.insert) else {
return;
};
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::all_parameters(&test.baseline, test.definition)
.into_iter()
.enumerate()
{
if !all.iter().any(|(_, _, _, id)| *id == p.id) {
continue;
}
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,
dir: actions::grip_direction(&p, value).and_then(|direction| {
(actions::v(transform.apply(actions::native(direction)))
- actions::v(transform.apply(codec::types::Vector3::ZERO)))
.try_normalize()
}),
axis: None,
});
}
let handle = test.insert;
let tab = &mut self.tabs[i];
for grip in grips {
tab.selected_grips.push(grip);
tab.selected_grip_handles.push(handle);
}
}
pub(super) fn start_test_action_grip(&mut self, handle: Handle, id: usize) -> bool {
if !is_test_grip(id) {
return false;
}
let i = self.active_tab;
let tab = &self.tabs[i];
let Some(test) = tab.block_test.as_ref() else {
return self.start_reference_action_grip(handle, id);
};
if handle != test.insert {
return false;
}
if tab.scene.is_layer_locked(handle) {
return true;
}
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,
definition: test.definition,
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
}
}
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::all_parameters(&tab.scene.document, def)
.into_iter()
.enumerate()
{
if !all.iter().any(|(_, _, _, id)| *id == p.id) {
continue;
}
let Ok(value) = actions::instance::value(&tab.scene.document, handle, &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,
dir: actions::grip_direction(&p, value).and_then(|direction| {
(actions::v(transform.apply(actions::native(direction)))
- actions::v(transform.apply(codec::types::Vector3::ZERO)))
.try_normalize()
}),
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::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,
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,
value: actions::Value,
) -> 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 = 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");
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()
}
}
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
}
}