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:
Karim Jerbi committed 2026-10-17 09:15:37 +01:00
1 parent f67f2cf4f5
commit de7ed432fb
11 files changed
+922 -98

No files matched your search

+98 -25
View File
@@ -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
}
}
+136 -3
View File
@@ -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
+38
View File
@@ -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(|| {
+2
View File
@@ -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 => {
+1 -1
View File
@@ -1534,7 +1534,7 @@ pub enum CmdResult {
document: u64,
definition: Handle,
parameter: Handle,
position: DVec3,
value: crate::scene::block_actions::Value,
},
/// Place or relocate a Basepoint, or place a Lookup parameter.
PlaceBlockSingleParameter {
+22 -10
View File
@@ -1,6 +1,6 @@
//! Reuse the command host's point picking and object-set selection.
use crate::command::{CadCommand, CmdOption, CmdResult, PointPickContext};
use crate::scene::block_actions::{Kind, Parameter};
use crate::scene::block_actions::{self as actions, Kind, Parameter};
use codec::Handle;
use glam::DVec3;
@@ -37,9 +37,9 @@ impl ActionCommand {
}
}
fn choose(&mut self, p: Parameter) -> CmdResult {
if self.kind == Some(Kind::Scale) && (p.point || p.xy) {
if self.kind.is_some_and(|kind| !actions::compatible(&p, kind)) {
return CmdResult::ReportError(
crate::t!("Scale requires a Linear or Polar parameter.").into_owned(),
crate::t!("Select a compatible parameter for this action.").into_owned(),
);
}
self.parameter = Some(p.handle);
@@ -51,12 +51,18 @@ impl CadCommand for ActionCommand {
fn name(&self) -> &'static str {
"BACTION"
}
fn wants_text_input(&self) -> bool {
self.parameter.is_none()
fn input_kind(&self) -> crate::command::InputKind {
if self.parameter.is_some() {
crate::command::InputKind::Point
} else if self.kind.is_some() {
crate::command::InputKind::FreeText
} else {
crate::command::InputKind::SingleToken
}
}
fn prompt(&self) -> String {
if self.kind.is_none() {
return crate::t!("BACTION Enter action type [Move/Scale]:").into_owned();
return crate::t!("BACTION Enter action type [Move/Scale/Rotate/Flip]:").into_owned();
}
if self.parameter.is_none() {
return crate::tf!(
@@ -77,6 +83,8 @@ impl CadCommand for ActionCommand {
vec![
CmdOption::new("Move", "Move"),
CmdOption::new("Scale", "Scale"),
CmdOption::new("Rotate", "Rotate"),
CmdOption::new("Flip", "Flip"),
]
} else {
vec![]
@@ -88,14 +96,18 @@ impl CadCommand for ActionCommand {
self.kind = Some(k);
CmdResult::NeedPoint
} else {
CmdResult::ReportError(crate::t!("Choose Move or Scale.").into_owned())
CmdResult::ReportError(
crate::t!("Choose Move, Scale, Rotate or Flip.").into_owned(),
)
});
}
if self.parameter.is_none() {
let matches = self
.candidates
.iter()
.filter(|p| p.name.eq_ignore_ascii_case(s))
.filter(|p| {
p.name.eq_ignore_ascii_case(s) && actions::compatible(p, self.kind.unwrap())
})
.cloned()
.collect::<Vec<_>>();
return Some(if matches.len() == 1 {
@@ -126,7 +138,7 @@ impl CadCommand for ActionCommand {
let candidate = self
.candidates
.iter()
.filter(|p| self.kind != Some(Kind::Scale) || (!p.point && !p.xy))
.filter(|p| actions::compatible(p, self.kind.unwrap()))
.map(|p| (screen(p.end).distance(click), p))
.filter(|(d, _)| d.is_finite() && *d <= c.aperture_px.max(10.))
.min_by(|a, b| a.0.total_cmp(&b.0))
@@ -200,7 +212,7 @@ impl CadCommand for TestParameterCommand {
document: self.document,
definition: self.definition,
parameter: self.parameter,
position,
value: actions::Value::Point(position),
}
}
fn on_enter(&mut self) -> CmdResult {
+322 -37
View File
@@ -1,8 +1,9 @@
//! Native Move/Scale authoring and deterministic dynamic-block evaluation.
//! Native Move/Scale/Rotate/Flip authoring and deterministic block evaluation.
use super::block_authoring::{self as authoring, *};
use codec::objects::{
BlockAction, BlockActionOffsets, BlockActionWithBasePoint, BlockBasePointAction,
BlockConnection, BlockElement, BlockEvalValue, BlockMoveAction, DynamicBlockData, ObjectType,
BlockConnection, BlockElement, BlockEvalValue, BlockMoveAction, BlockFlipAction,
DynamicBlockData, ObjectType,
};
use codec::types::{Transform, Vector3};
use codec::{CadDocument, EntityType, Handle};
@@ -15,12 +16,16 @@ mod tests;
pub enum Kind {
Move,
Scale,
Rotate,
Flip,
}
impl Kind {
pub(crate) fn parse(s: &str) -> Option<Self> {
match s.to_ascii_uppercase().as_str() {
"MOVE" | "M" => Some(Self::Move),
"SCALE" | "S" => Some(Self::Scale),
"ROTATE" | "R" => Some(Self::Rotate),
"FLIP" | "F" => Some(Self::Flip),
_ => None,
}
}
@@ -28,6 +33,8 @@ impl Kind {
match self {
Self::Move => "Move",
Self::Scale => "Scale",
Self::Rotate => "Rotate",
Self::Flip => "Flip",
}
}
}
@@ -43,6 +50,9 @@ pub(crate) struct Parameter {
pub linear: bool,
pub point: bool,
pub xy: bool,
pub rotation: bool,
pub flip: bool,
pub reference: DVec3,
}
pub(crate) fn v(p: Vector3) -> DVec3 {
DVec3::new(p.x, p.y, p.z)
@@ -51,6 +61,117 @@ pub(crate) fn native(p: DVec3) -> Vector3 {
Vector3::new(p.x, p.y, p.z)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Value {
Point(DVec3),
Flip(bool),
}
pub(crate) fn all_parameters(doc: &CadDocument, definition: Handle) -> Vec<Parameter> {
let mut result = parameters(doc, definition, Kind::Move);
result.extend(parameters(doc, definition, Kind::Rotate));
result.extend(parameters(doc, definition, Kind::Flip));
result.sort_by_key(|p| p.id);
result
}
pub(crate) fn compatible(p: &Parameter, kind: Kind) -> bool {
match kind {
Kind::Move => !p.rotation && !p.flip,
Kind::Scale => !p.rotation && !p.flip && !p.point && !p.xy,
Kind::Rotate => p.rotation,
Kind::Flip => p.flip,
}
}
fn flip_component(doc: &CadDocument, p: &Parameter) -> Result<Handle, &'static str> {
let Some(ObjectType::DynamicBlock(o)) = doc.objects.get(&p.grip) else {
return Err("Missing Flip grip.");
};
let DynamicBlockData::FlipGrip(g) = &o.data else {
return Err("Missing Flip grip.");
};
let candidates = doc
.objects
.iter()
.filter_map(|(h, x)| match x {
ObjectType::DynamicBlock(x) if x.owner == o.owner => match &x.data {
DynamicBlockData::GripLocationComponent(c)
if c.eval.node_id == g.combined_state && c.expression == "UpdatedFlip" =>
{
Some(*h)
}
_ => None,
},
_ => None,
})
.collect::<Vec<_>>();
if candidates.len() != 1 {
return Err("The Flip state binding is unavailable.");
}
Ok(candidates[0])
}
pub(crate) fn value(doc: &CadDocument, p: &Parameter) -> Result<Value, &'static str> {
if !p.flip {
return Ok(Value::Point(current(doc, p)));
}
let h = flip_component(doc, p)?;
let ObjectType::DynamicBlock(o) = &doc.objects[&h] else {
unreachable!()
};
let DynamicBlockData::GripLocationComponent(c) = &o.data else {
unreachable!()
};
match c.eval.value {
BlockEvalValue::Short(0) => Ok(Value::Flip(false)),
BlockEvalValue::Short(1) => Ok(Value::Flip(true)),
_ => Err("The Flip state is not zero or one."),
}
}
pub(crate) fn set_value(
doc: &mut CadDocument,
p: &Parameter,
value: Value,
) -> Result<(), &'static str> {
if let Value::Flip(state) = value {
if !p.flip {
return Err("Select a Flip parameter.");
}
let h = flip_component(doc, p)?;
let Some(ObjectType::DynamicBlock(o)) = doc.objects.get_mut(&h) else {
unreachable!()
};
let DynamicBlockData::GripLocationComponent(c) = &mut o.data else {
unreachable!()
};
c.eval.value = BlockEvalValue::Short(i16::from(state));
return Ok(());
}
let Value::Point(position) = value else {
unreachable!()
};
let Some(ObjectType::DynamicBlock(o)) = doc.objects.get_mut(&p.grip) else {
return Err("Missing parameter grip.");
};
match &mut o.data {
DynamicBlockData::XYGrip(g)
| DynamicBlockData::PolarGrip(g)
| DynamicBlockData::RotationGrip(g) => g.location = native(position),
DynamicBlockData::LinearGrip(g) => g.grip.location = native(position),
_ => return Err("Unsupported parameter grip."),
}
Ok(())
}
pub(crate) fn grip_position(p: &Parameter, value: Value) -> DVec3 {
match value {
Value::Point(at) => at,
Value::Flip(_) => p.reference,
}
}
pub(crate) fn parameters(doc: &CadDocument, definition: Handle, kind: Kind) -> Vec<Parameter> {
let Ok(Some(graph)) = authoring::graph(doc, definition) else {
return vec![];
@@ -68,10 +189,16 @@ pub(crate) fn parameters(doc: &CadDocument, definition: Handle, kind: Kind) -> V
linear: false,
point: true,
xy: false,
rotation: false,
flip: false,
reference: DVec3::ZERO,
});
}
}
for p in super::block_distance_parameter::parameters(doc, definition) {
if matches!(kind, Kind::Rotate | Kind::Flip) {
continue;
}
let xy = p.kind == super::block_distance_parameter::Kind::XY;
if kind == Kind::Scale && xy {
continue;
@@ -86,11 +213,46 @@ pub(crate) fn parameters(doc: &CadDocument, definition: Handle, kind: Kind) -> V
linear: p.kind == super::block_distance_parameter::Kind::Linear,
point: false,
xy,
rotation: false,
flip: false,
reference: DVec3::ZERO,
});
}
for p in super::block_orientation_parameter::parameters(doc, definition) {
use super::block_orientation_parameter::Kind as O;
if !matches!(
(kind, p.kind),
(Kind::Rotate, O::Rotation) | (Kind::Flip, O::Flip)
) {
continue;
}
result.push(Parameter {
handle: p.handle,
id: p.native.parameter.element.eval.node_id,
grip: Handle::NULL,
name: p.label,
base: v(p.native.definition_base_point),
end: v(p.native.definition_end_point),
linear: false,
point: false,
xy: false,
rotation: kind == Kind::Rotate,
flip: kind == Kind::Flip,
reference: v(p.reference),
});
}
// Resolve the actual linked grip, rather than assuming expression IDs are contiguous.
result.retain_mut(|p| {
let grip_id = if p.point {
let grip_id = if p.rotation || p.flip {
let Some(ObjectType::DynamicBlock(o)) = doc.objects.get(&p.handle) else {
return false;
};
match &o.data {
DynamicBlockData::RotationParameter(p) => p.parameter.property_states[1],
DynamicBlockData::FlipParameter(p) => p.flags_96,
_ => return false,
}
} else if p.point {
super::block_point_parameter::parameters(doc, definition)
.into_iter()
.find(|(h, _)| *h == p.handle)
@@ -121,7 +283,10 @@ pub(crate) fn parameters(doc: &CadDocument, definition: Handle, kind: Kind) -> V
return None;
}
let grip = match &o.data {
DynamicBlockData::XYGrip(g) | DynamicBlockData::PolarGrip(g) => g,
DynamicBlockData::XYGrip(g)
| DynamicBlockData::PolarGrip(g)
| DynamicBlockData::RotationGrip(g) => g,
DynamicBlockData::FlipGrip(g) => &g.grip,
DynamicBlockData::LinearGrip(g) => &g.grip,
_ => return None,
};
@@ -136,6 +301,19 @@ pub(crate) fn parameters(doc: &CadDocument, definition: Handle, kind: Kind) -> V
return false;
}
p.grip = grips[0];
if p.flip {
if let Some(ObjectType::DynamicBlock(o)) = doc.objects.get(&p.grip) {
if let DynamicBlockData::FlipGrip(g) = &o.data {
let at = v(g.grip.location);
if at.distance(p.base) > 1e-8 {
p.reference = at;
}
}
}
if p.reference.distance(p.base) < 1e-8 {
p.reference = p.end;
}
}
true
});
result.sort_by_key(|p| p.id);
@@ -192,6 +370,8 @@ pub(crate) fn create(
let (dxf, cpp) = match kind {
Kind::Move => ("BLOCKMOVEACTION", "AcDbBlockMoveAction"),
Kind::Scale => ("BLOCKSCALEACTION", "AcDbBlockScaleAction"),
Kind::Rotate => ("BLOCKROTATEACTION", "AcDbBlockRotationAction"),
Kind::Flip => ("BLOCKFLIPACTION", "AcDbBlockFlipAction"),
};
register_classes(&mut staged, &[(dxf, cpp, 55)]);
let handle = staged.allocate_handle();
@@ -228,6 +408,26 @@ pub(crate) fn create(
flags: 0,
},
}),
Kind::Flip => DynamicBlockData::FlipAction(BlockFlipAction {
action,
connections: [
connection("Flip"),
connection("UpdatedFlip"),
connection("UpdatedBase"),
connection("UpdatedEnd"),
],
}),
Kind::Rotate => DynamicBlockData::RotateAction(BlockBasePointAction {
action: BlockActionWithBasePoint {
action,
offset: Vector3::ZERO,
connections: vec![connection("UpdatedBaseX"), connection("UpdatedBaseY")],
dependent: true,
base_point: Vector3::ZERO,
},
connections: vec![connection("AngleDelta")],
dwg_tail_byte: 0,
}),
Kind::Scale => DynamicBlockData::ScaleAction(BlockBasePointAction {
action: BlockActionWithBasePoint {
action,
@@ -280,6 +480,13 @@ pub(crate) fn actions(
DynamicBlockData::MoveAction(a) => {
Some((o.handle, &a.action, Kind::Move, a.connections[0].code))
}
DynamicBlockData::FlipAction(a) => {
Some((o.handle, &a.action, Kind::Flip, a.connections[0].code))
}
DynamicBlockData::RotateAction(a) => a
.connections
.first()
.map(|c| (o.handle, &a.action.action, Kind::Rotate, c.code)),
DynamicBlockData::ScaleAction(a) => a
.connections
.first()
@@ -302,7 +509,10 @@ pub(crate) fn reframe(doc: &mut CadDocument, definition: Handle, t: &Transform)
DynamicBlockData::MoveAction(a) => {
a.action.display_location = t.apply(a.action.display_location)
}
DynamicBlockData::ScaleAction(a) => {
DynamicBlockData::FlipAction(a) => {
a.action.display_location = t.apply(a.action.display_location)
}
DynamicBlockData::ScaleAction(a) | DynamicBlockData::RotateAction(a) => {
a.action.action.display_location = t.apply(a.action.action.display_location);
a.action.base_point = t.apply(a.action.base_point);
a.action.offset = t.apply_rotation(a.action.offset);
@@ -318,7 +528,9 @@ pub(crate) fn current(doc: &CadDocument, p: &Parameter) -> DVec3 {
return p.end;
};
match &o.data {
DynamicBlockData::XYGrip(g) | DynamicBlockData::PolarGrip(g) => v(g.location),
DynamicBlockData::XYGrip(g)
| DynamicBlockData::PolarGrip(g)
| DynamicBlockData::RotationGrip(g) => v(g.location),
DynamicBlockData::LinearGrip(g) => v(g.grip.location),
_ => p.end,
}
@@ -372,54 +584,71 @@ pub(crate) fn sync_representation(
/// Re-evaluate from the scratch document's immutable starting geometry. This
/// avoids incremental drift and makes repeated changes/order independent.
#[cfg(test)]
pub(crate) fn evaluate(
doc: &mut CadDocument,
baseline: &CadDocument,
definition: Handle,
parameter: Handle,
position: DVec3,
) -> Result<bool, &'static str> {
evaluate_value(doc, baseline, definition, parameter, Value::Point(position))
}
pub(crate) fn evaluate_value(
doc: &mut CadDocument,
baseline: &CadDocument,
definition: Handle,
parameter: Handle,
input: Value,
) -> Result<bool, &'static str> {
let graph = authoring::graph(baseline, definition)?
.ok_or("The block authoring graph is unavailable.")?;
if baseline.objects.values().any(|o|matches!(o,ObjectType::DynamicBlock(o) if o.owner==graph && matches!(&o.data,
DynamicBlockData::StretchAction(_)|DynamicBlockData::PolarStretchAction(_)|DynamicBlockData::RotateAction(_)|DynamicBlockData::FlipAction(_)|DynamicBlockData::ArrayAction(_)))) {
DynamicBlockData::StretchAction(_)|DynamicBlockData::PolarStretchAction(_)|DynamicBlockData::ArrayAction(_)))) {
return Err("This block contains action types not supported yet.");
}
let p = parameters(baseline, definition, Kind::Move)
let p = all_parameters(baseline, definition)
.into_iter()
.find(|p| p.handle == parameter)
.ok_or("The block parameter is unavailable.")?;
if !position.is_finite() {
return Err("Specify a finite parameter position.");
}
let mut position = position;
position.z = p.end.z;
if p.linear {
let axis = (p.end - p.base).normalize();
let length = (position - p.base).dot(axis);
if length < 1e-9 {
return Err("Specify a positive parameter length.");
let input = match input {
Value::Flip(_) if !p.flip => return Err("Select a Flip parameter."),
Value::Point(_) if p.flip => return Err("Use the Flip grip to toggle its state."),
Value::Point(mut position) => {
if !position.is_finite() {
return Err("Specify a finite parameter position.");
}
position.z = p.end.z;
if p.linear {
let axis = (p.end - p.base).normalize();
let length = (position - p.base).dot(axis);
if length < 1e-9 {
return Err("Specify a positive parameter length.");
}
position = p.base + axis * length;
}
if p.rotation {
let delta = position - p.base;
if delta.length() < 1e-9 {
return Err("Specify a rotation direction away from the parameter base.");
}
position = p.base + delta.normalize() * p.end.distance(p.base);
}
Value::Point(position)
}
position = p.base + axis * length;
}
if current(doc, &p).distance(position) < 1e-10 {
value => value,
};
let previous = value(doc, &p)?;
if match (previous, input) {
(Value::Flip(a), Value::Flip(b)) => a == b,
(Value::Point(a), Value::Point(b)) => a.distance(b) < 1e-10,
_ => false,
} {
return Ok(false);
}
let mut staged = doc.clone();
let ObjectType::DynamicBlock(o) = staged
.objects
.get_mut(&p.grip)
.ok_or("The parameter grip is unavailable.")?
else {
return Err("The parameter grip is unavailable.");
};
match &mut o.data {
DynamicBlockData::XYGrip(g) | DynamicBlockData::PolarGrip(g) => {
g.location = native(position)
}
DynamicBlockData::LinearGrip(g) => g.grip.location = native(position),
_ => return Err("The parameter grip is unavailable."),
}
set_value(&mut staged, &p, input)?;
let all = actions(baseline, definition);
let mut affected = std::collections::BTreeSet::new();
for (_, a, _, _) in &all {
@@ -445,12 +674,32 @@ pub(crate) fn evaluate(
.into_iter()
.find(|p| p.id == *id)
.ok_or("An action has an unsupported parameter binding.")?;
if !test_parameter_supported(baseline, q.handle) {
if !supported_parameter(baseline, q.handle) {
return Err("This parameter uses value constraints or chaining not supported yet.");
}
let end = current(&staged, &q);
let transform = match kind {
Kind::Move => crate::command::EntityTransform::Translate(end - q.end),
Kind::Rotate => {
let a = end - q.base;
let b = q.end - q.base;
crate::command::EntityTransform::Rotate {
center: q.base,
axis: DVec3::Z,
angle_rad: a.y.atan2(a.x) - b.y.atan2(b.x),
}
}
Kind::Flip => {
if value(&staged, &q)? == Value::Flip(false) {
crate::command::EntityTransform::Translate(DVec3::ZERO)
} else {
crate::command::EntityTransform::Mirror {
p1: q.base,
p2: q.end,
working_normal: DVec3::Z,
}
}
}
Kind::Scale => {
let factor = end.distance(q.base) / q.end.distance(q.base);
if !factor.is_finite() || factor < 1e-9 {
@@ -502,6 +751,33 @@ pub(crate) fn evaluate(
{
return Err("This Move action uses options not supported yet.")
}
DynamicBlockData::FlipAction(f)
if f.connections
!= ["Flip", "UpdatedFlip", "UpdatedBase", "UpdatedEnd"].map(|name| {
BlockConnection {
code: q.id,
name: name.into(),
}
}) =>
{
return Err("This Flip action uses options not supported yet.")
}
DynamicBlockData::RotateAction(r)
if !r.action.dependent
|| r.action.offset != Vector3::ZERO
|| r.connections
!= vec![BlockConnection {
code: q.id,
name: "AngleDelta".into(),
}]
|| r.action.connections
!= ["UpdatedBaseX", "UpdatedBaseY"].map(|name| BlockConnection {
code: q.id,
name: name.into(),
}) =>
{
return Err("This Rotate action uses options not supported yet.")
}
DynamicBlockData::ScaleAction(s)
if !s.action.dependent
|| s.action.offset != Vector3::ZERO
@@ -534,7 +810,7 @@ pub(crate) fn evaluate(
Ok(true)
}
fn test_parameter_supported(doc: &CadDocument, handle: Handle) -> bool {
fn supported_parameter(doc: &CadDocument, handle: Handle) -> bool {
let Some(ObjectType::DynamicBlock(o)) = doc.objects.get(&handle) else {
return false;
};
@@ -546,6 +822,15 @@ fn test_parameter_supported(doc: &CadDocument, handle: Handle) -> bool {
&& s.values.is_empty()
};
match &o.data {
DynamicBlockData::FlipParameter(p) => !p.parameter.parameter.chain_actions,
DynamicBlockData::RotationParameter(p) => {
!p.parameter.parameter.chain_actions
&& p.value_set.flags == 3
&& p.value_set.minimum == 0.
&& p.value_set.maximum == std::f64::consts::TAU
&& p.value_set.increment == 0.
&& p.value_set.values.is_empty()
}
DynamicBlockData::PointParameter(p) => !p.parameter.parameter.chain_actions,
DynamicBlockData::LinearParameter(p) => {
!p.parameter.parameter.chain_actions && distance(&p.value_set)
+87 -16
View File
@@ -50,6 +50,13 @@ pub(crate) fn current(
.filter_map(|e| e.value.as_point3d())
.map(|(x, y, z)| DVec3::new(x, y, z))
.collect::<Vec<_>>();
if p.rotation
&& points
.get(3)
.is_none_or(|r| !r.is_finite() || r.distance(p.reference) > 1e-8)
{
return Err("The rotation reference point is unavailable or has been relocated.");
}
if p.xy {
let (Some(a), Some(b)) = (points.first(), points.get(1)) else {
return Err("The XY parameter cache has no finite endpoint.");
@@ -76,6 +83,48 @@ pub(crate) fn current(
.ok_or("The reference parameter cache has no finite endpoint.")
}
pub(crate) fn value(
doc: &CadDocument,
insert: Handle,
p: &Parameter,
) -> Result<Value, &'static str> {
if !p.flip {
return current(doc, insert, p).map(Value::Point);
}
let Some(h) = cache(doc, insert, p.id)? else {
return Ok(Value::Flip(false));
};
let Some(ObjectType::XRecord(r)) = doc.objects.get(&h) else {
return Err("Invalid Flip parameter record.");
};
if !r.entries_complete {
return Err("The Flip parameter cache is incomplete.");
}
let points = r
.entries
.iter()
.filter_map(|e| e.value.as_point3d())
.map(|(x, y, z)| DVec3::new(x, y, z))
.collect::<Vec<_>>();
if points.len() < 2
|| !points[0].is_finite()
|| !points[1].is_finite()
|| points[0].distance(p.base) > 1e-8
|| points[1].distance(p.end) > 1e-8
{
return Err("This reference uses an unsupported Flip reflection line.");
}
let state = r.entries.iter().skip(4).rev().find_map(|e| match e.value {
XRecordValue::Int16(s) => Some(s),
_ => None,
});
match state {
Some(0) => Ok(Value::Flip(false)),
Some(1) => Ok(Value::Flip(true)),
_ => Err("The Flip state is not zero or one."),
}
}
fn ensure_dictionary(
doc: &mut CadDocument,
owner: Handle,
@@ -103,7 +152,7 @@ fn save(
doc: &mut CadDocument,
insert: Handle,
p: &Parameter,
position: DVec3,
value: Value,
) -> Result<(), &'static str> {
let mut owner = doc.ensure_extension_dictionary(insert);
for key in [
@@ -137,7 +186,9 @@ fn save(
DVec3::new(p.end.x, p.base.y, p.end.z),
-DVec3::Z,
]
} else if !p.linear {
} else if p.rotation {
vec![p.base, p.end, -DVec3::Z, p.reference]
} else if !p.linear && !p.flip {
vec![p.base, p.end, -DVec3::Z, p.end]
} else {
vec![p.base, p.end, -DVec3::Z]
@@ -146,6 +197,9 @@ fn save(
r.entries
.push(XRecordEntry::new(10, XRecordValue::Point3D(v.x, v.y, v.z)));
}
if p.flip {
r.entries.push(XRecordEntry::int16(70, 0));
}
doc.objects.insert(h, ObjectType::XRecord(r));
if let Some(ObjectType::Dictionary(d)) = doc.objects.get_mut(&owner) {
d.add_entry(&key, h);
@@ -155,6 +209,22 @@ fn save(
let Some(ObjectType::XRecord(r)) = doc.objects.get_mut(&h) else {
return Err("Invalid parameter record.");
};
if let Value::Flip(state) = value {
let entry = r
.entries
.iter_mut()
.skip(4)
.rev()
.find(|e| matches!(e.value, XRecordValue::Int16(_)))
.ok_or("The Flip cache has no state.")?;
entry.value = XRecordValue::Int16(i16::from(state));
r.raw_data.clear();
r.raw_dwg_data = None;
return Ok(());
}
let Value::Point(position) = value else {
unreachable!()
};
if p.xy {
let mut points = r
.entries
@@ -199,37 +269,38 @@ pub(crate) fn definition(doc: &CadDocument, insert: Handle) -> Option<Handle> {
}
/// Stage and validate completely before exposing any changes to the drawing.
#[cfg(test)]
pub(crate) fn evaluate(
doc: &CadDocument,
insert: Handle,
parameter: Handle,
local: DVec3,
) -> Result<Option<CadDocument>, &'static str> {
evaluate_value(doc, insert, parameter, Value::Point(local))
}
pub(crate) fn evaluate_value(
doc: &CadDocument,
insert: Handle,
parameter: Handle,
input: Value,
) -> Result<Option<CadDocument>, &'static str> {
let def =
definition(doc, insert).ok_or("Select a uniformly scaled dynamic block reference.")?;
let parameters = parameters(doc, def, Kind::Move);
let parameters = all_parameters(doc, def);
let selected = parameters
.iter()
.find(|p| p.handle == parameter)
.ok_or("The reference parameter is unavailable.")?;
let mut evaluated = doc.clone();
for p in &parameters {
let value = current(doc, insert, p)?;
let Some(ObjectType::DynamicBlock(o)) = evaluated.objects.get_mut(&p.grip) else {
return Err("Missing parameter grip.");
};
match &mut o.data {
DynamicBlockData::XYGrip(g) | DynamicBlockData::PolarGrip(g) => {
g.location = native(value)
}
DynamicBlockData::LinearGrip(g) => g.grip.location = native(value),
_ => return Err("Unsupported parameter grip."),
}
let value = value(doc, insert, p)?;
super::set_value(&mut evaluated, p, value)?;
}
if !super::evaluate(&mut evaluated, doc, def, parameter, local)? {
if !super::evaluate_value(&mut evaluated, doc, def, parameter, input)? {
return Ok(None);
}
let value = super::current(&evaluated, selected);
let value = super::value(&evaluated, selected)?;
let mut scene = crate::scene::Scene::new();
scene.document = doc.clone();
scene.dynamic_representation(insert);
+201
View File
@@ -23,6 +23,207 @@ fn fixture() -> (CadDocument, Handle, Vec<Handle>) {
let (definition, handles) = (block.handle, block.entity_handles.clone());
(scene.document, definition, handles)
}
fn orientation(
doc: &mut CadDocument,
def: Handle,
kind: super::super::block_orientation_parameter::Kind,
) -> Handle {
super::super::block_orientation_parameter::create(
doc,
def,
kind,
Vector3::ZERO,
Vector3::new(10., 0., 0.),
if kind == super::super::block_orientation_parameter::Kind::Rotation {
Vector3::new(10., 0., 0.)
} else {
Vector3::new(-2., 2., 0.)
},
)
.unwrap()
}
#[test]
fn block_actions_rotate_flip_native_bindings_and_marker_edits_roundtrip() {
use super::super::block_orientation_parameter::Kind as O;
let (mut doc, def, targets) = fixture();
let r = orientation(&mut doc, def, O::Rotation);
let f = orientation(&mut doc, def, O::Flip);
assert!(parameters(&doc, def, Kind::Move).is_empty());
assert!(parameters(&doc, def, Kind::Scale).is_empty());
assert_eq!(all_parameters(&doc, def).len(), 2);
let rotate = create(
&mut doc,
def,
Kind::Rotate,
r,
&targets,
DVec3::new(3., 4., 0.),
)
.unwrap();
let flip = create(
&mut doc,
def,
Kind::Flip,
f,
&targets,
DVec3::new(5., 6., 0.),
)
.unwrap();
assert!(create(&mut doc, def, Kind::Rotate, f, &targets, DVec3::ZERO).is_err());
for handle in [rotate, flip] {
super::super::block_authoring::edit::move_part(
&mut doc,
def,
handle,
super::super::block_authoring::edit::Part::Point,
Vector3::new(7., 8., 0.),
)
.unwrap();
}
for saved in [
DwgReader::from_stream(Cursor::new(DwgWriter::write_to_vec(&doc).unwrap()))
.read()
.unwrap(),
DxfReader::from_reader(Cursor::new(DxfWriter::new(&doc).write_to_vec().unwrap()))
.unwrap()
.read()
.unwrap(),
] {
assert_eq!(actions(&saved, def).len(), 2);
for handle in [rotate, flip] {
assert_eq!(saved.objects[&handle], doc.objects[&handle]);
}
for (_, action, _, _) in actions(&saved, def) {
assert_eq!(action.display_location, Vector3::new(7., 8., 0.));
assert_eq!(action.dependencies, targets);
}
}
}
#[test]
fn block_actions_rotate_flip_saved_values_compose_copy_and_roundtrip() {
use super::super::block_orientation_parameter::Kind as O;
let (mut doc, def, targets) = fixture();
let r = orientation(&mut doc, def, O::Rotation);
let f = orientation(&mut doc, def, O::Flip);
create(&mut doc, def, Kind::Rotate, r, &[targets[0]], DVec3::ZERO).unwrap();
create(&mut doc, def, Kind::Flip, f, &[targets[0]], DVec3::ZERO).unwrap();
let a = doc
.add_entity(EntityType::Insert(codec::entities::Insert::new(
"ACTIONS",
Vector3::ZERO,
)))
.unwrap();
let b = doc
.add_entity(EntityType::Insert(codec::entities::Insert::new(
"ACTIONS",
Vector3::new(30., 0., 0.),
)))
.unwrap();
let baseline = doc.clone();
doc = instance::evaluate(&doc, a, r, DVec3::new(0., 50., 0.))
.unwrap()
.unwrap();
assert!(instance_line(&doc, a).distance(&Vector3::UNIT_Y) < 1e-8);
doc = instance::evaluate_value(&doc, a, f, Value::Flip(true))
.unwrap()
.unwrap();
assert!(instance_line(&doc, a).distance(&-Vector3::UNIT_Y) < 1e-8);
assert!(instance::evaluate_value(&doc, a, f, Value::Flip(true))
.unwrap()
.is_none());
let props = doc.dynamic_block_properties(def, Some(a)).unwrap();
assert!(props
.iter()
.any(|p| p.parameter_handle == f && p.value == BlockEvalValue::Short(1)));
assert!(props.iter().any(|p|p.parameter_handle==r&&matches!(p.value,BlockEvalValue::Real(v) if (v-std::f64::consts::FRAC_PI_2).abs()<1e-8)));
let variants = [
doc.clone(),
DwgReader::from_stream(Cursor::new(DwgWriter::write_to_vec(&doc).unwrap()))
.read()
.unwrap(),
DxfReader::from_reader(Cursor::new(DxfWriter::new(&doc).write_to_vec().unwrap()))
.unwrap()
.read()
.unwrap(),
];
for saved in variants {
let mut next = instance::evaluate(&saved, a, r, DVec3::new(0., -50., 0.))
.unwrap()
.unwrap();
assert!(instance_line(&next, a).distance(&Vector3::UNIT_Y) < 1e-8);
for _ in 0..6 {
next = instance::evaluate_value(&next, a, f, Value::Flip(false))
.unwrap()
.unwrap();
assert!(instance_line(&next, a).distance(&-Vector3::UNIT_Y) < 1e-8);
next = instance::evaluate_value(&next, a, f, Value::Flip(true))
.unwrap()
.unwrap();
assert!(instance_line(&next, a).distance(&Vector3::UNIT_Y) < 1e-8);
}
assert_eq!(instance_line(&next, b), Vector3::UNIT_X);
assert_eq!(start(&next, targets[0]), start(&baseline, targets[0]));
let mut scene = Scene::new();
scene.document = next;
let c = scene.copy_entities(
&[a],
&crate::command::EntityTransform::Translate(DVec3::new(40., 0., 0.)),
)[0];
let next = instance::evaluate_value(&scene.document, c, f, Value::Flip(false))
.unwrap()
.unwrap();
assert!(instance_line(&next, c).distance(&-Vector3::UNIT_Y) < 1e-8);
assert!(instance_line(&next, a).distance(&Vector3::UNIT_Y) < 1e-8);
let resaved = DwgReader::from_stream(Cursor::new(DwgWriter::write_to_vec(&next).unwrap()))
.read()
.unwrap();
assert!(instance_line(&resaved, c).distance(&-Vector3::UNIT_Y) < 1e-8);
let fp = parameters(&resaved, def, Kind::Flip).remove(0);
assert_eq!(
instance::value(&resaved, c, &fp).unwrap(),
Value::Flip(false)
);
assert_eq!(
instance::value(&resaved, a, &fp).unwrap(),
Value::Flip(true)
);
}
}
#[test]
fn block_actions_rotate_flip_test_baseline_reversible_and_invalid_atomic() {
use super::super::block_orientation_parameter::Kind as O;
let (mut doc, def, targets) = fixture();
let r = orientation(&mut doc, def, O::Rotation);
let f = orientation(&mut doc, def, O::Flip);
let action = create(&mut doc, def, Kind::Rotate, r, &targets, DVec3::ZERO).unwrap();
create(&mut doc, def, Kind::Flip, f, &targets, DVec3::ZERO).unwrap();
let baseline = doc.clone();
assert!(evaluate(&mut doc, &baseline, def, r, DVec3::ZERO).is_err());
assert_eq!(doc, baseline);
assert!(evaluate_value(&mut doc, &baseline, def, r, Value::Flip(true)).is_err());
assert!(evaluate_value(&mut doc, &baseline, def, f, Value::Point(DVec3::ONE)).is_err());
assert_eq!(doc, baseline);
evaluate(&mut doc, &baseline, def, r, DVec3::new(0., 10., 0.)).unwrap();
evaluate_value(&mut doc, &baseline, def, f, Value::Flip(true)).unwrap();
assert!(start(&doc, targets[0]).distance(&-Vector3::UNIT_Y) < 1e-8);
evaluate_value(&mut doc, &baseline, def, f, Value::Flip(false)).unwrap();
evaluate(&mut doc, &baseline, def, r, DVec3::new(10., 0., 0.)).unwrap();
assert!(start(&doc, targets[0]).distance(&Vector3::UNIT_X) < 1e-8);
let ObjectType::DynamicBlock(o) = doc.objects.get_mut(&action).unwrap() else {
panic!()
};
let DynamicBlockData::RotateAction(a) = &mut o.data else {
panic!()
};
a.action.dependent = false;
let unsupported = doc.clone();
assert!(evaluate(&mut doc, &unsupported, def, r, DVec3::new(0., 10., 0.)).is_err());
assert_eq!(doc, unsupported);
}
fn linear(doc: &mut CadDocument, def: Handle) -> Handle {
distance::create(
doc,
+4 -1
View File
@@ -277,7 +277,10 @@ pub fn move_part(
}
}
DynamicBlockData::MoveAction(p) => p.action.display_location = at,
DynamicBlockData::ScaleAction(p) => p.action.action.display_location = at,
DynamicBlockData::FlipAction(p) => p.action.display_location = at,
DynamicBlockData::ScaleAction(p) | DynamicBlockData::RotateAction(p) => {
p.action.action.display_location = at
}
DynamicBlockData::AlignmentParameter(_) => {}
_ => return Err("This authoring object cannot be edited yet."),
}
+11 -5
View File
@@ -196,6 +196,8 @@ fn catalog(
lookup_error: Option<&'static str>,
move_error: Option<&'static str>,
scale_error: Option<&'static str>,
rotate_error: Option<&'static str>,
flip_action_error: Option<&'static str>,
) -> Element<'static, Message> {
let (entries, unavailable) = match tab {
Tab::Parameters => (
@@ -217,8 +219,8 @@ fn catalog(
let orientation_error = if *label=="Flip" {flip_error} else {point_error};
let single=tab==Tab::Parameters&&matches!(*label,"Lookup"|"Basepoint");
let lookup_action=tab==Tab::Actions&&*label=="Lookup";
let geometry_action=tab==Tab::Actions&&matches!(*label,"Move"|"Scale");
let action_error=if *label=="Scale" {scale_error}else{move_error};
let geometry_action=tab==Tab::Actions&&matches!(*label,"Move"|"Scale"|"Rotate"|"Flip");
let action_error=match *label {"Scale"=>scale_error,"Rotate"=>rotate_error,"Flip"=>flip_action_error,_=>move_error};
let enabled = (geometry_action&&action_error.is_none())||(single&&point_error.is_none())||(lookup_action&&lookup_error.is_none())||(orientation && orientation_error.is_none()) || (visibility && visibility_error.is_none()) || ((point || distance) && point_error.is_none());
let base = if enabled {
icons::semantic(icon, 28.0)
@@ -264,7 +266,7 @@ fn catalog(
} else if single {
crate::t!(point_error.unwrap_or(if *label=="Basepoint" {"Place or relocate the dynamic block insertion anchor."}else{"Place a Lookup parameter, then use the Lookup action to edit its property table."})).into_owned()
} else if geometry_action {
crate::t!(action_error.unwrap_or("Select a parameter endpoint, select the objects it controls, then place the action marker. Test it using the parameter grip in Test Block.")).into_owned()
crate::t!(action_error.unwrap_or("Select a parameter endpoint, select the objects it controls, then place the action marker. Use the reference parameter grip in the drawing or Test Block.")).into_owned()
} else if lookup_action {
crate::t!(lookup_error.unwrap_or("Create or edit property Lookup tables for this block.")).into_owned()
} else {
@@ -282,7 +284,7 @@ fn catalog(
} else if single {
container(entry).id(if *label=="Lookup" {"authoring-tool-lookup"}else{"authoring-tool-basepoint"}).into()
} else if geometry_action {
container(entry).id(if *label=="Move" {"authoring-tool-move-action"}else{"authoring-tool-scale-action"}).into()
container(entry).id(match *label {"Move"=>"authoring-tool-move-action","Scale"=>"authoring-tool-scale-action","Rotate"=>"authoring-tool-rotate-action",_=>"authoring-tool-flip-action"}).into()
} else if lookup_action {
container(entry).id("authoring-tool-lookup-table").into()
} else {
@@ -401,6 +403,8 @@ pub fn view(
lookup_error: Option<&'static str>,
move_error: Option<&'static str>,
scale_error: Option<&'static str>,
rotate_error: Option<&'static str>,
flip_action_error: Option<&'static str>,
) -> Element<'static, Message> {
let tabs = column(Tab::ALL.into_iter().map(|category| {
let label = category.label();
@@ -457,7 +461,9 @@ pub fn view(
flip_error,
lookup_error,
move_error,
scale_error
scale_error,
rotate_error,
flip_action_error
))
.width(Fill)
.height(Fill),