feat(blocks): implement native Array action authoring and dynamic evaluation

Add the native Array action (\BARRAY\, \BACTION ARRAY\, and Authoring Palettes Actions tab \Array\) supporting Linear, Polar, and XY parameters.

Prompt for column offset (and row offset for XY), constructing native \DynamicBlockData::ArrayAction(BlockArrayAction)\ with class \AcDbBlockArrayAction\ and 4 native connections (\Base\, \End\, \UpdatedBase\, \UpdatedEnd\) bound to the parameter node ID.

Evaluate step displacement along 1D rays and 2D grids, tagging clones with deterministic element IDs to reconcile representations cleanly across incremental grip adjustments without mutating baseline geometry. Validate native DWG/DXF roundtrip, reference independence, COPY isolation, rejections, and dock palette tool state.
This commit is contained in:
Karim Jerbi committed 2026-10-17 12:49:51 +01:00
1 parent 5a6a7fab93
commit 26071f2021
14 files changed
+806 -57

No files matched your search

+4 -2
View File
@@ -95,7 +95,7 @@ impl OpenCADStudio {
}; };
if !args.is_empty() && kind.is_none() { if !args.is_empty() && kind.is_none() {
self.command_line.push_error( self.command_line.push_error(
crate::t!("Supported actions: Move, Scale, Stretch, Rotate, Flip. Other action types are not wired yet.") crate::t!("Supported actions: Move, Scale, Stretch, Rotate, Flip, Array. Other action types are not wired yet.")
.as_ref(), .as_ref(),
); );
return Task::none(); return Task::none();
@@ -181,6 +181,7 @@ impl OpenCADStudio {
targets, targets,
location, location,
frame, frame,
array_offsets,
} => { } => {
if self.tabs[i].id != document if self.tabs[i].id != document
|| self.tabs[i] || self.tabs[i]
@@ -195,7 +196,7 @@ impl OpenCADStudio {
return Task::none(); return Task::none();
} }
let mut staged = self.tabs[i].scene.document.clone(); let mut staged = self.tabs[i].scene.document.clone();
match actions::create_with_frame( match actions::create_with_options(
&mut staged, &mut staged,
definition, definition,
kind, kind,
@@ -203,6 +204,7 @@ impl OpenCADStudio {
&targets, &targets,
location, location,
frame, frame,
array_offsets,
) { ) {
Ok(_) => { Ok(_) => {
self.push_undo_snapshot(i, &format!("{} action", kind.name())); self.push_undo_snapshot(i, &format!("{} action", kind.name()));
+1
View File
@@ -1575,6 +1575,7 @@ fn block_actions_rejected_outside_editor_preserve_running_command_and_stale_resu
targets: members(&app), targets: members(&app),
location: glam::DVec3::ZERO, location: glam::DVec3::ZERO,
frame: None, frame: None,
array_offsets: None,
}); });
assert_eq!(app.tabs[i].scene.document, before); assert_eq!(app.tabs[i].scene.document, before);
let _ = app.dispatch_command("LINE"); let _ = app.dispatch_command("LINE");
+8 -2
View File
@@ -395,11 +395,17 @@ impl OpenCADStudio {
cmd if cmd == "BACTION" cmd if cmd == "BACTION"
|| cmd.starts_with("BACTION ") || cmd.starts_with("BACTION ")
|| cmd == "BACTIONTOOL" || cmd == "BACTIONTOOL"
|| cmd.starts_with("BACTIONTOOL ") => || cmd.starts_with("BACTIONTOOL ")
|| cmd == "BARRAY"
|| cmd.starts_with("BARRAY ") =>
{ {
Some( Some(
self.start_block_action( self.start_block_action(
cmd.split_once(' ').map_or("", |(_, args)| args.trim()), if cmd == "BARRAY" || cmd.starts_with("BARRAY ") {
"ARRAY"
} else {
cmd.split_once(' ').map_or("", |(_, args)| args.trim())
},
), ),
) )
} }
+7 -3
View File
@@ -181,12 +181,16 @@ impl OpenCADStudio {
return Task::none(); return Task::none();
} }
} }
if matches!(cmd.split(' ').next(), Some("BACTION" | "BACTIONTOOL")) { if matches!(cmd.split(' ').next(), Some("BACTION" | "BACTIONTOOL" | "BARRAY")) {
let args = cmd.split_once(' ').map_or("", |(_, a)| a.trim()); let args = if cmd == "BARRAY" || cmd.starts_with("BARRAY ") {
"ARRAY"
} else {
cmd.split_once(' ').map_or("", |(_, a)| a.trim())
};
if !args.is_empty() && crate::scene::block_actions::Kind::parse(args).is_none() { if !args.is_empty() && crate::scene::block_actions::Kind::parse(args).is_none() {
self.command_line.push_error( self.command_line.push_error(
crate::t!( crate::t!(
"Supported actions: Move, Scale, Stretch, Rotate, Flip. Other action types are not wired yet." "Supported actions: Move, Scale, Stretch, Rotate, Flip, Array. Other action types are not wired yet."
) )
.as_ref(), .as_ref(),
); );
+25
View File
@@ -1334,6 +1334,31 @@ fn block_actions_stretch_palette_requires_distance_parameter() {
}); });
} }
#[test]
fn block_actions_array_palette_requires_distance_parameter() {
with_stack(|| {
let mut app = authoring_app();
let _ = app.dispatch_command("BAUTHORPALETTE");
app.authoring_palette_tab = crate::ui::window::authoring_palettes::Tab::Actions;
let point = authoring_control(&app, "authoring-tool-array-action");
authoring_click(&mut app, point);
assert!(app.tabs[app.active_tab].active_cmd.is_none());
let _ = app.run_command_line("BPARAMETER POINT 0.5,0.5 0.7,0.7");
let point = authoring_control(&app, "authoring-tool-array-action");
authoring_click(&mut app, point);
assert!(app.tabs[app.active_tab].active_cmd.is_none());
let _ = app.run_command_line("BPARAMETER LINEAR 0,0 10,0 5,2");
let point = authoring_control(&app, "authoring-tool-array-action");
authoring_click(&mut app, point);
assert!(app.tabs[app.active_tab]
.active_cmd
.as_ref()
.unwrap()
.prompt()
.contains("Array"));
});
}
#[test] #[test]
fn block_actions_rotate_flip_palette_requires_matching_parameters() { fn block_actions_rotate_flip_palette_requires_matching_parameters() {
with_stack(|| { with_stack(|| {
+1
View File
@@ -416,6 +416,7 @@ impl OpenCADStudio {
self.action_error(Some(crate::scene::block_actions::Kind::Rotate)), self.action_error(Some(crate::scene::block_actions::Kind::Rotate)),
self.action_error(Some(crate::scene::block_actions::Kind::Flip)), self.action_error(Some(crate::scene::block_actions::Kind::Flip)),
self.action_error(Some(crate::scene::block_actions::Kind::Stretch)), self.action_error(Some(crate::scene::block_actions::Kind::Stretch)),
self.action_error(Some(crate::scene::block_actions::Kind::Array)),
) )
} }
crate::ui::dock::PanelId::BlockPalette => { crate::ui::dock::PanelId::BlockPalette => {
+1 -1
View File
@@ -5267,7 +5267,7 @@ mod tablestyle_buffer_tests {
let mut app = OpenCADStudio::new_for_test(); let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#); app.automation_op(r#"{"op":"new"}"#);
app.ts_cell_textstyle[0] = "STALE_SENTINEL".to_string(); app.ts_cell_textstyle[0] = "STALE_SENTINEL".to_string();
app.style_new(StyleKind::Table); let _ = app.style_new(StyleKind::Table);
assert_ne!( assert_ne!(
app.ts_cell_textstyle[0], "STALE_SENTINEL", app.ts_cell_textstyle[0], "STALE_SENTINEL",
"buffers must reload from the fresh style" "buffers must reload from the fresh style"
+1
View File
@@ -1537,6 +1537,7 @@ pub enum CmdResult {
targets: Vec<Handle>, targets: Vec<Handle>,
location: DVec3, location: DVec3,
frame: Option<[DVec3; 2]>, frame: Option<[DVec3; 2]>,
array_offsets: Option<(f64, f64)>,
}, },
EvaluateBlockParameter { EvaluateBlockParameter {
insert: Option<Handle>, insert: Option<Handle>,
+115 -14
View File
@@ -5,7 +5,7 @@ use codec::Handle;
use glam::DVec3; use glam::DVec3;
inventory::submit!(crate::command::CommandRegistration { inventory::submit!(crate::command::CommandRegistration {
names: &["BACTION", "BACTIONTOOL"] names: &["BACTION", "BACTIONTOOL", "BARRAY"]
}); });
pub(crate) struct ActionCommand { pub(crate) struct ActionCommand {
@@ -17,6 +17,8 @@ pub(crate) struct ActionCommand {
targets: Vec<Handle>, targets: Vec<Handle>,
selecting: bool, selecting: bool,
frame: Vec<DVec3>, frame: Vec<DVec3>,
column_offset: Option<f64>,
row_offset: Option<f64>,
context: Option<PointPickContext>, context: Option<PointPickContext>,
} }
impl ActionCommand { impl ActionCommand {
@@ -35,6 +37,8 @@ impl ActionCommand {
targets: vec![], targets: vec![],
selecting: false, selecting: false,
frame: vec![], frame: vec![],
column_offset: None,
row_offset: None,
context: None, context: None,
} }
} }
@@ -55,6 +59,16 @@ impl CadCommand for ActionCommand {
} }
fn input_kind(&self) -> crate::command::InputKind { fn input_kind(&self) -> crate::command::InputKind {
if self.parameter.is_some() { if self.parameter.is_some() {
if self.kind == Some(Kind::Array) && !self.selecting {
let is_xy = self
.candidates
.iter()
.find(|p| Some(p.handle) == self.parameter)
.is_some_and(|p| p.xy);
if self.column_offset.is_none() || (is_xy && self.row_offset.is_none()) {
return crate::command::InputKind::SingleToken;
}
}
crate::command::InputKind::Point crate::command::InputKind::Point
} else if self.kind.is_some() { } else if self.kind.is_some() {
crate::command::InputKind::FreeText crate::command::InputKind::FreeText
@@ -64,7 +78,7 @@ impl CadCommand for ActionCommand {
} }
fn prompt(&self) -> String { fn prompt(&self) -> String {
if self.kind.is_none() { if self.kind.is_none() {
return crate::t!("BACTION Enter action type [Move/Scale/Stretch/Rotate/Flip]:") return crate::t!("BACTION Enter action type [Move/Scale/Stretch/Rotate/Flip/Array]:")
.into_owned(); .into_owned();
} }
if self.parameter.is_none() { if self.parameter.is_none() {
@@ -74,18 +88,33 @@ impl CadCommand for ActionCommand {
) )
.into_owned(); .into_owned();
} }
crate::t!( if self.kind == Some(Kind::Stretch) && self.frame.is_empty() {
if self.kind == Some(Kind::Stretch) && self.frame.is_empty() { return crate::t!("BACTION Specify first corner of stretch frame:").into_owned();
"BACTION Specify first corner of stretch frame:" }
} else if self.kind == Some(Kind::Stretch) && self.frame.len() == 1 { if self.kind == Some(Kind::Stretch) && self.frame.len() == 1 {
"BACTION Specify opposite corner of stretch frame:" return crate::t!("BACTION Specify opposite corner of stretch frame:").into_owned();
} else if self.selecting { }
"BACTION Select objects for the action, then press Enter:" if self.selecting {
} else { return crate::t!("BACTION Select objects for the action, then press Enter:")
"BACTION Specify action marker location:" .into_owned();
}
if self.kind == Some(Kind::Array) {
let is_xy = self
.candidates
.iter()
.find(|p| Some(p.handle) == self.parameter)
.is_some_and(|p| p.xy);
if self.column_offset.is_none() {
return crate::t!(
"BACTION Specify distance between columns (or distance between items):"
)
.into_owned();
} }
) if is_xy && self.row_offset.is_none() {
.into_owned() return crate::t!("BACTION Specify distance between rows:").into_owned();
}
}
crate::t!("BACTION Specify action marker location:").into_owned()
} }
fn options(&self) -> Vec<CmdOption> { fn options(&self) -> Vec<CmdOption> {
if self.kind.is_none() { if self.kind.is_none() {
@@ -95,6 +124,7 @@ impl CadCommand for ActionCommand {
CmdOption::new("Stretch", "Stretch"), CmdOption::new("Stretch", "Stretch"),
CmdOption::new("Rotate", "Rotate"), CmdOption::new("Rotate", "Rotate"),
CmdOption::new("Flip", "Flip"), CmdOption::new("Flip", "Flip"),
CmdOption::new("Array", "Array"),
] ]
} else { } else {
vec![] vec![]
@@ -107,7 +137,7 @@ impl CadCommand for ActionCommand {
CmdResult::NeedPoint CmdResult::NeedPoint
} else { } else {
CmdResult::ReportError( CmdResult::ReportError(
crate::t!("Choose Move, Scale, Stretch, Rotate or Flip.").into_owned(), crate::t!("Choose Move, Scale, Stretch, Rotate, Flip or Array.").into_owned(),
) )
}); });
} }
@@ -129,6 +159,41 @@ impl CadCommand for ActionCommand {
) )
}); });
} }
if self.kind == Some(Kind::Array) && !self.selecting {
let is_xy = self
.candidates
.iter()
.find(|p| Some(p.handle) == self.parameter)
.is_some_and(|p| p.xy);
if self.column_offset.is_none() {
let Ok(dist) = s.trim().parse::<f64>() else {
return Some(CmdResult::ReportError(
crate::t!("Specify a positive distance.").into_owned(),
));
};
if dist <= 1e-6 || !dist.is_finite() {
return Some(CmdResult::ReportError(
crate::t!("Specify a positive distance.").into_owned(),
));
}
self.column_offset = Some(dist);
return Some(CmdResult::NeedPoint);
}
if is_xy && self.row_offset.is_none() {
let Ok(dist) = s.trim().parse::<f64>() else {
return Some(CmdResult::ReportError(
crate::t!("Specify a positive distance.").into_owned(),
));
};
if dist <= 1e-6 || !dist.is_finite() {
return Some(CmdResult::ReportError(
crate::t!("Specify a positive distance.").into_owned(),
));
}
self.row_offset = Some(dist);
return Some(CmdResult::NeedPoint);
}
}
None None
} }
fn wants_point_pick_context(&self) -> bool { fn wants_point_pick_context(&self) -> bool {
@@ -188,6 +253,33 @@ impl CadCommand for ActionCommand {
} }
return CmdResult::NeedPoint; return CmdResult::NeedPoint;
} }
if self.kind == Some(Kind::Array) && !self.selecting {
let p = self
.candidates
.iter()
.find(|p| Some(p.handle) == self.parameter)
.unwrap();
if self.column_offset.is_none() {
let dist = point.distance(p.base);
if dist <= 1e-6 || !dist.is_finite() {
return CmdResult::ReportError(
crate::t!("Specify a positive distance.").into_owned(),
);
}
self.column_offset = Some(dist);
return CmdResult::NeedPoint;
}
if p.xy && self.row_offset.is_none() {
let dist = (point.y - p.base.y).abs().max(point.distance(p.base));
if dist <= 1e-6 || !dist.is_finite() {
return CmdResult::ReportError(
crate::t!("Specify a positive distance.").into_owned(),
);
}
self.row_offset = Some(dist);
return CmdResult::NeedPoint;
}
}
if self.selecting { if self.selecting {
return CmdResult::NeedPoint; return CmdResult::NeedPoint;
} }
@@ -196,6 +288,13 @@ impl CadCommand for ActionCommand {
crate::t!("Specify a finite action location.").into_owned(), crate::t!("Specify a finite action location.").into_owned(),
); );
} }
let array_offsets = if self.kind == Some(Kind::Array) {
let col = self.column_offset.unwrap_or(1.0);
let row = self.row_offset.unwrap_or(1.0);
Some((col, row))
} else {
None
};
CmdResult::PlaceBlockAction { CmdResult::PlaceBlockAction {
document: self.document, document: self.document,
definition: self.definition, definition: self.definition,
@@ -204,8 +303,10 @@ impl CadCommand for ActionCommand {
targets: self.targets.clone(), targets: self.targets.clone(),
location: point, location: point,
frame: (self.frame.len() == 2).then(|| [self.frame[0], self.frame[1]]), frame: (self.frame.len() == 2).then(|| [self.frame[0], self.frame[1]]),
array_offsets,
} }
} }
fn on_preview_wires(&mut self, point: DVec3) -> Vec<crate::scene::WireModel> { fn on_preview_wires(&mut self, point: DVec3) -> Vec<crate::scene::WireModel> {
frame_preview(&self.frame, point) frame_preview(&self.frame, point)
} }
+243 -26
View File
@@ -1,9 +1,9 @@
//! Native Move/Scale/Stretch/Rotate/Flip authoring and deterministic block evaluation. //! Native Move/Scale/Stretch/Rotate/Flip/Array authoring and deterministic block evaluation.
use super::block_authoring::{self as authoring, *}; use super::block_authoring::{self as authoring, *};
use codec::objects::{ use codec::objects::{
BlockAction, BlockActionOffsets, BlockActionWithBasePoint, BlockBasePointAction, BlockAction, BlockActionOffsets, BlockActionWithBasePoint, BlockArrayAction,
BlockConnection, BlockElement, BlockEvalValue, BlockMoveAction, BlockFlipAction, BlockBasePointAction, BlockConnection, BlockElement, BlockEvalValue, BlockFlipAction,
BlockStretchAction, DynamicBlockData, ObjectType, BlockMoveAction, BlockStretchAction, DynamicBlockData, ObjectType,
}; };
use codec::types::{Transform, Vector3}; use codec::types::{Transform, Vector3};
use codec::{CadDocument, EntityType, Handle}; use codec::{CadDocument, EntityType, Handle};
@@ -21,6 +21,7 @@ pub enum Kind {
Rotate, Rotate,
Flip, Flip,
Stretch, Stretch,
Array,
} }
impl Kind { impl Kind {
pub(crate) fn parse(s: &str) -> Option<Self> { pub(crate) fn parse(s: &str) -> Option<Self> {
@@ -30,6 +31,7 @@ impl Kind {
"ROTATE" | "R" => Some(Self::Rotate), "ROTATE" | "R" => Some(Self::Rotate),
"FLIP" | "F" => Some(Self::Flip), "FLIP" | "F" => Some(Self::Flip),
"STRETCH" | "ST" => Some(Self::Stretch), "STRETCH" | "ST" => Some(Self::Stretch),
"ARRAY" | "A" => Some(Self::Array),
_ => None, _ => None,
} }
} }
@@ -40,6 +42,7 @@ impl Kind {
Self::Rotate => "Rotate", Self::Rotate => "Rotate",
Self::Flip => "Flip", Self::Flip => "Flip",
Self::Stretch => "Stretch", Self::Stretch => "Stretch",
Self::Array => "Array",
} }
} }
} }
@@ -87,6 +90,7 @@ pub(crate) fn compatible(p: &Parameter, kind: Kind) -> bool {
Kind::Scale => !p.rotation && !p.flip && !p.point && !p.xy, Kind::Scale => !p.rotation && !p.flip && !p.point && !p.xy,
Kind::Rotate => p.rotation, Kind::Rotate => p.rotation,
Kind::Flip => p.flip, Kind::Flip => p.flip,
Kind::Array => !p.rotation && !p.flip && !p.point,
} }
} }
@@ -357,9 +361,32 @@ pub(crate) fn create(
targets: &[Handle], targets: &[Handle],
location: DVec3, location: DVec3,
) -> Result<Handle, &'static str> { ) -> Result<Handle, &'static str> {
create_with_frame(doc, definition, kind, parameter, targets, location, None) create_with_options(doc, definition, kind, parameter, targets, location, None, None)
} }
#[cfg(test)]
pub(crate) fn create_array(
doc: &mut CadDocument,
definition: Handle,
parameter: Handle,
targets: &[Handle],
location: DVec3,
column_offset: f64,
row_offset: f64,
) -> Result<Handle, &'static str> {
create_with_options(
doc,
definition,
Kind::Array,
parameter,
targets,
location,
None,
Some((column_offset, row_offset)),
)
}
#[allow(dead_code)]
pub(crate) fn create_with_frame( pub(crate) fn create_with_frame(
doc: &mut CadDocument, doc: &mut CadDocument,
definition: Handle, definition: Handle,
@@ -368,6 +395,19 @@ pub(crate) fn create_with_frame(
targets: &[Handle], targets: &[Handle],
location: DVec3, location: DVec3,
frame: Option<[DVec3; 2]>, frame: Option<[DVec3; 2]>,
) -> Result<Handle, &'static str> {
create_with_options(doc, definition, kind, parameter, targets, location, frame, None)
}
pub(crate) fn create_with_options(
doc: &mut CadDocument,
definition: Handle,
kind: Kind,
parameter: Handle,
targets: &[Handle],
location: DVec3,
frame: Option<[DVec3; 2]>,
array_offsets: Option<(f64, f64)>,
) -> Result<Handle, &'static str> { ) -> Result<Handle, &'static str> {
if !location.is_finite() { if !location.is_finite() {
return Err("Specify a finite action location."); return Err("Specify a finite action location.");
@@ -393,6 +433,18 @@ pub(crate) fn create_with_frame(
}) { }) {
return Err("This milestone supports lines, polylines, arcs, circles, ellipses, points and text belonging to this block."); return Err("This milestone supports lines, polylines, arcs, circles, ellipses, points and text belonging to this block.");
} }
let (column_offset, row_offset) = if kind == Kind::Array {
let (col, row) = array_offsets.unwrap_or((1.0, 1.0));
if col <= 1e-6 || !col.is_finite() {
return Err("Specify a positive column offset.");
}
if p.xy && (row <= 1e-6 || !row.is_finite()) {
return Err("Specify a positive row offset.");
}
(col, row)
} else {
(0.0, 0.0)
};
let stretch_binding = if kind == Kind::Stretch { let stretch_binding = if kind == Kind::Stretch {
if (location.z - p.end.z).abs() > 1e-8 { if (location.z - p.end.z).abs() > 1e-8 {
return Err("Place the Stretch marker in the parameter's XY plane."); return Err("Place the Stretch marker in the parameter's XY plane.");
@@ -414,6 +466,7 @@ pub(crate) fn create_with_frame(
Kind::Rotate => ("BLOCKROTATEACTION", "AcDbBlockRotationAction"), Kind::Rotate => ("BLOCKROTATEACTION", "AcDbBlockRotationAction"),
Kind::Flip => ("BLOCKFLIPACTION", "AcDbBlockFlipAction"), Kind::Flip => ("BLOCKFLIPACTION", "AcDbBlockFlipAction"),
Kind::Stretch => ("BLOCKSTRETCHACTION", "AcDbBlockStretchAction"), Kind::Stretch => ("BLOCKSTRETCHACTION", "AcDbBlockStretchAction"),
Kind::Array => ("BLOCKARRAYACTION", "AcDbBlockArrayAction"),
}; };
register_classes(&mut staged, &[(dxf, cpp, 55)]); register_classes(&mut staged, &[(dxf, cpp, 55)]);
let handle = staged.allocate_handle(); let handle = staged.allocate_handle();
@@ -437,6 +490,17 @@ pub(crate) fn create_with_frame(
name: name.into(), name: name.into(),
}; };
let data = match kind { let data = match kind {
Kind::Array => DynamicBlockData::ArrayAction(BlockArrayAction {
action,
connections: [
connection("Base"),
connection("End"),
connection("UpdatedBase"),
connection("UpdatedEnd"),
],
column_offset,
row_offset,
}),
Kind::Stretch => { Kind::Stretch => {
let (points, handles) = stretch_binding.unwrap(); let (points, handles) = stretch_binding.unwrap();
DynamicBlockData::StretchAction(BlockStretchAction { DynamicBlockData::StretchAction(BlockStretchAction {
@@ -511,6 +575,7 @@ pub(crate) fn create_with_frame(
Ok(handle) Ok(handle)
} }
/// Retarget an existing native action, retaining identity, graph and options. /// Retarget an existing native action, retaining identity, graph and options.
pub(crate) fn retarget( pub(crate) fn retarget(
doc: &mut CadDocument, doc: &mut CadDocument,
@@ -582,6 +647,7 @@ pub(crate) fn retarget(
DynamicBlockData::ScaleAction(a) | DynamicBlockData::RotateAction(a) => { DynamicBlockData::ScaleAction(a) | DynamicBlockData::RotateAction(a) => {
a.action.action.dependencies = targets a.action.action.dependencies = targets
} }
DynamicBlockData::ArrayAction(a) => a.action.dependencies = targets,
_ => return Err("This action's selection cannot be edited yet."), _ => return Err("This action's selection cannot be edited yet."),
} }
if frame.is_some() && kind != Kind::Stretch { if frame.is_some() && kind != Kind::Stretch {
@@ -634,6 +700,10 @@ pub(crate) fn actions(
.connections .connections
.first() .first()
.map(|c| (o.handle, &a.action.action, Kind::Scale, c.code)), .map(|c| (o.handle, &a.action.action, Kind::Scale, c.code)),
DynamicBlockData::ArrayAction(a) => a
.connections
.first()
.map(|c| (o.handle, &a.action, Kind::Array, c.code)),
_ => None, _ => None,
} }
}) })
@@ -655,6 +725,7 @@ pub(crate) fn prune_erased_targets(doc: &mut CadDocument, definition: Handle) {
let action = match &mut o.data { let action = match &mut o.data {
DynamicBlockData::MoveAction(a) => &mut a.action, DynamicBlockData::MoveAction(a) => &mut a.action,
DynamicBlockData::FlipAction(a) => &mut a.action, DynamicBlockData::FlipAction(a) => &mut a.action,
DynamicBlockData::ArrayAction(a) => &mut a.action,
DynamicBlockData::StretchAction(a) => { DynamicBlockData::StretchAction(a) => {
a.handles.retain(|b| live.contains(&b.handle)); a.handles.retain(|b| live.contains(&b.handle));
&mut a.action &mut a.action
@@ -691,6 +762,9 @@ pub(crate) fn reframe(doc: &mut CadDocument, definition: Handle, t: &Transform)
DynamicBlockData::MoveAction(a) => { DynamicBlockData::MoveAction(a) => {
a.action.display_location = t.apply(a.action.display_location) a.action.display_location = t.apply(a.action.display_location)
} }
DynamicBlockData::ArrayAction(a) => {
a.action.display_location = t.apply(a.action.display_location)
}
DynamicBlockData::StretchAction(a) => { DynamicBlockData::StretchAction(a) => {
let plane = a.action.display_location.z; let plane = a.action.display_location.z;
a.action.display_location = t.apply(a.action.display_location); a.action.display_location = t.apply(a.action.display_location);
@@ -705,6 +779,7 @@ pub(crate) fn reframe(doc: &mut CadDocument, definition: Handle, t: &Transform)
DynamicBlockData::FlipAction(a) => { DynamicBlockData::FlipAction(a) => {
a.action.display_location = t.apply(a.action.display_location) a.action.display_location = t.apply(a.action.display_location)
} }
DynamicBlockData::ScaleAction(a) | DynamicBlockData::RotateAction(a) => { DynamicBlockData::ScaleAction(a) | DynamicBlockData::RotateAction(a) => {
a.action.action.display_location = t.apply(a.action.action.display_location); a.action.action.display_location = t.apply(a.action.action.display_location);
a.action.base_point = t.apply(a.action.base_point); a.action.base_point = t.apply(a.action.base_point);
@@ -751,27 +826,35 @@ pub(crate) fn sync_representation(
.ok_or("The test representation is unavailable.")? .ok_or("The test representation is unavailable.")?
.entity_handles .entity_handles
.clone(); .clone();
if source.len() != target.len() { let has_array = actions(doc, definition)
return Err("The test representation no longer matches its definition.");
}
let affected = actions(doc, definition)
.iter() .iter()
.flat_map(|(_, a, _, _)| a.dependencies.iter().copied()) .any(|(_, _, k, _)| *k == Kind::Array);
.collect::<std::collections::BTreeSet<_>>(); if source.len() == target.len() && !has_array {
for (from, to) in source.into_iter().zip(target) { let affected = actions(doc, definition)
if !affected.contains(&from) { .iter()
continue; .flat_map(|(_, a, _, _)| a.dependencies.iter().copied())
.collect::<std::collections::BTreeSet<_>>();
for (from, to) in source.into_iter().zip(target) {
if !affected.contains(&from) {
continue;
}
let mut entity = doc
.get_entity(from)
.cloned()
.ok_or("An action target is unavailable.")?;
let current = doc
.get_entity_mut(to)
.ok_or("An action representation target is unavailable.")?;
*entity.common_mut() = current.common().clone();
*current = entity;
} }
let mut entity = doc return Ok(());
.get_entity(from)
.cloned()
.ok_or("An action target is unavailable.")?;
let current = doc
.get_entity_mut(to)
.ok_or("An action representation target is unavailable.")?;
*entity.common_mut() = current.common().clone();
*current = entity;
} }
let evaluated = doc.clone();
let mut scene = crate::scene::Scene::new();
scene.document = doc.clone();
representation::reconcile(&mut scene, definition, name, &evaluated)?;
*doc = scene.document;
Ok(()) Ok(())
} }
@@ -822,8 +905,8 @@ fn evaluate_impl(
let graph = authoring::graph(baseline, definition)? let graph = authoring::graph(baseline, definition)?
.ok_or("The block authoring graph is unavailable.")?; .ok_or("The block authoring graph is unavailable.")?;
if baseline.objects.values().any(|o| { if baseline.objects.values().any(|o| {
matches!(o,ObjectType::DynamicBlock(o) if o.owner==graph && matches!(&o.data, matches!(o, ObjectType::DynamicBlock(o) if o.owner == graph && matches!(&o.data,
DynamicBlockData::PolarStretchAction(_)|DynamicBlockData::ArrayAction(_))) DynamicBlockData::PolarStretchAction(_)))
}) { }) {
return Err("This block contains action types not supported yet."); return Err("This block contains action types not supported yet.");
} }
@@ -870,6 +953,26 @@ fn evaluate_impl(
} }
let mut staged = doc.clone(); let mut staged = doc.clone();
set_value(&mut staged, &p, input)?; set_value(&mut staged, &p, input)?;
let baseline_members = authoring::members(baseline, definition)
.into_iter()
.collect::<std::collections::BTreeSet<_>>();
let extra_handles = staged
.entities()
.filter(|e| {
e.common().owner_handle == definition && !baseline_members.contains(&e.common().handle)
})
.map(|e| e.common().handle)
.collect::<Vec<_>>();
for h in extra_handles {
staged.remove_entity(h);
}
if let Some(record) = staged.block_records.iter_mut().find(|b| b.handle == definition) {
record.entity_handles.retain(|h| {
baseline_members.contains(h)
|| *h == record.block_entity_handle
|| *h == record.block_end_handle
});
}
let all = actions(baseline, definition); let all = actions(baseline, definition);
let mut affected = std::collections::BTreeSet::new(); let mut affected = std::collections::BTreeSet::new();
for (h, a, kind, _) in &all { for (h, a, kind, _) in &all {
@@ -897,7 +1000,7 @@ fn evaluate_impl(
entity.common_mut().invisible = e.common().invisible; entity.common_mut().invisible = e.common().invisible;
} }
for (action_handle, a, kind, id) in &all { for (action_handle, a, kind, id) in &all {
if !a.dependencies.contains(&h) { if !a.dependencies.contains(&h) || *kind == Kind::Array {
continue; continue;
} }
let q = parameters(baseline, definition, *kind) let q = parameters(baseline, definition, *kind)
@@ -942,6 +1045,7 @@ fn evaluate_impl(
factor, factor,
} }
} }
Kind::Array => unreachable!(),
}; };
// Imported actions with custom multipliers/offsets/chaining must // Imported actions with custom multipliers/offsets/chaining must
// not silently behave like the simple actions authored here. // not silently behave like the simple actions authored here.
@@ -1042,10 +1146,123 @@ fn evaluate_impl(
.get_entity_mut(h) .get_entity_mut(h)
.ok_or("An action target was removed.")? = entity; .ok_or("An action target was removed.")? = entity;
} }
for (action_handle, a, kind, id) in &all {
if *kind != Kind::Array {
continue;
}
let ObjectType::DynamicBlock(o) = &baseline.objects[action_handle] else {
unreachable!()
};
let DynamicBlockData::ArrayAction(arr) = &o.data else {
unreachable!()
};
let q = parameters(baseline, definition, Kind::Array)
.into_iter()
.find(|p| p.id == *id)
.ok_or("An action has an unsupported parameter binding.")?;
if !supported_parameter(baseline, q.handle) {
return Err("This parameter uses value constraints or chaining not supported yet.");
}
if arr.column_offset <= 1e-6
|| !arr.column_offset.is_finite()
|| (q.xy && (arr.row_offset <= 1e-6 || !arr.row_offset.is_finite()))
|| arr.connections
!= ["Base", "End", "UpdatedBase", "UpdatedEnd"].map(|name| BlockConnection {
code: q.id,
name: name.into(),
})
{
return Err("This Array action uses options not supported yet.");
}
if !a.action_ids.is_empty() {
return Err("Chained actions are not supported yet.");
}
let end = current(&staged, &q);
let displacements: Vec<(usize, DVec3)> = if q.xy {
let dx = (end.x - q.base.x).abs();
let dy = (end.y - q.base.y).abs();
let nx = ((dx + 1e-9) / arr.column_offset).floor() as usize + 1;
let ny = ((dy + 1e-9) / arr.row_offset).floor() as usize + 1;
let dir_x = if end.x >= q.base.x { 1.0 } else { -1.0 };
let dir_y = if end.y >= q.base.y { 1.0 } else { -1.0 };
let mut list = vec![];
let mut idx = 1;
for row in 0..ny {
for col in 0..nx {
if col == 0 && row == 0 {
continue;
}
let disp = DVec3::new(
(col as f64) * arr.column_offset * dir_x,
(row as f64) * arr.row_offset * dir_y,
0.0,
);
list.push((idx, disp));
idx += 1;
}
}
list
} else if q.linear {
let axis = (q.end - q.base).normalize();
let length = (end - q.base).dot(axis);
let n = ((length.max(0.0) + 1e-9) / arr.column_offset).floor() as usize + 1;
(1..n)
.map(|k| (k, (k as f64) * arr.column_offset * axis))
.collect()
} else {
let delta = end - q.base;
let dist = delta.length();
let axis = if dist > 1e-9 {
delta / dist
} else {
(q.end - q.base).normalize()
};
let n = ((dist + 1e-9) / arr.column_offset).floor() as usize + 1;
(1..n)
.map(|k| (k, (k as f64) * arr.column_offset * axis))
.collect()
};
for (k, disp) in displacements {
for target_h in &a.dependencies {
let mut clone = staged
.get_entity(*target_h)
.cloned()
.ok_or("An action target is unavailable.")?;
super::view::dispatch::apply_transform(
&mut clone,
&crate::command::EntityTransform::Translate(disp),
);
let bounds = clone.as_entity().bounding_box();
if !v(bounds.min).is_finite() || !v(bounds.max).is_finite() {
return Err("The parameter value produces non-finite geometry.");
}
let new_h = staged.allocate_handle();
clone.common_mut().handle = new_h;
clone.common_mut().owner_handle = definition;
clone.common_mut().raw_record = None;
clone.common_mut().xdictionary_handle = None;
clone.common_mut().reactors.clear();
crate::scene::Scene::reset_clone_subhandles(&mut staged, &mut clone);
let base_id = authoring::element_id(&clone);
staged
.add_entity(clone)
.map_err(|_| "Could not add array item.")?;
if let Some(base_id) = base_id {
let clone_id = base_id.wrapping_add((k as i32) * 100_000);
authoring::set_entity_tag(
&mut staged,
new_h,
authoring::element_tag(clone_id, new_h),
);
}
}
}
}
*doc = staged; *doc = staged;
Ok(true) Ok(true)
} }
fn supported_parameter(doc: &CadDocument, handle: Handle) -> bool { fn supported_parameter(doc: &CadDocument, handle: Handle) -> bool {
let Some(ObjectType::DynamicBlock(o)) = doc.objects.get(&handle) else { let Some(ObjectType::DynamicBlock(o)) = doc.objects.get(&handle) else {
return false; return false;
+2 -3
View File
@@ -16,7 +16,7 @@ pub(super) fn reconcile(
.get(name) .get(name)
.ok_or("Missing representation.")? .ok_or("Missing representation.")?
.handle; .handle;
let sources = authoring::members(&scene.document, def); let sources = authoring::members(evaluated, def);
let old = scene let old = scene
.document .document
.entities() .entities()
@@ -41,8 +41,7 @@ pub(super) fn reconcile(
let mut mapping = FxHashMap::default(); let mut mapping = FxHashMap::default();
let mut pairs = vec![]; let mut pairs = vec![];
for &from in &sources { for &from in &sources {
let source = scene let source = evaluated
.document
.get_entity(from) .get_entity(from)
.ok_or("Missing definition member.")?; .ok_or("Missing definition member.")?;
let id = let id =
+384
View File
@@ -1361,3 +1361,387 @@ fn block_actions_topology_nested_insert_attribute_handles_remain_independent() {
assert_eq!(copied.attributes[0].common.owner_handle, member); assert_eq!(copied.attributes[0].common.owner_handle, member);
assert_ne!(copied.seqend_handle, original.seqend_handle); assert_ne!(copied.seqend_handle, original.seqend_handle);
} }
#[test]
fn block_actions_array_native_creation_and_dwg_dxf_roundtrip() {
for kind in [distance::Kind::Linear, distance::Kind::Polar, distance::Kind::XY] {
let (mut doc, def, targets) = fixture();
let end_pt = if kind == distance::Kind::XY {
Vector3::new(10., 10., 0.)
} else {
Vector3::new(10., 0., 0.)
};
let p = distance::create(
&mut doc,
def,
kind,
Vector3::ZERO,
end_pt,
Vector3::new(5., 2., 0.),
)
.unwrap();
let column_offset = 2.5;
let row_offset = if kind == distance::Kind::XY { 3.5 } else { 1.0 };
let action = create_array(
&mut doc,
def,
p,
&[targets[0]],
DVec3::new(5., 5., 0.),
column_offset,
row_offset,
)
.unwrap();
let ObjectType::DynamicBlock(o) = doc.objects.get(&action).unwrap() else {
panic!()
};
let DynamicBlockData::ArrayAction(arr) = &o.data else {
panic!()
};
assert_eq!(arr.column_offset, column_offset);
assert_eq!(arr.row_offset, row_offset);
assert_eq!(arr.action.dependencies, vec![targets[0]]);
assert_eq!(arr.connections.len(), 4);
let param_id = parameters(&doc, def, Kind::Array)[0].id;
assert_eq!(
arr.connections,
[
BlockConnection {
code: param_id,
name: "Base".into(),
},
BlockConnection {
code: param_id,
name: "End".into(),
},
BlockConnection {
code: param_id,
name: "UpdatedBase".into(),
},
BlockConnection {
code: param_id,
name: "UpdatedEnd".into(),
},
]
);
// DWG roundtrip
let dwg_bytes = DwgWriter::write_to_vec(&doc).unwrap();
let dwg_doc = DwgReader::from_stream(Cursor::new(dwg_bytes)).read().unwrap();
let ObjectType::DynamicBlock(dwg_o) = dwg_doc.objects.get(&action).unwrap() else {
panic!()
};
let DynamicBlockData::ArrayAction(dwg_arr) = &dwg_o.data else {
panic!()
};
assert_eq!(dwg_arr.column_offset, column_offset);
assert_eq!(dwg_arr.row_offset, row_offset);
assert_eq!(dwg_arr.action.dependencies, vec![targets[0]]);
assert_eq!(dwg_arr.connections, arr.connections);
// DXF roundtrip
let dxf_bytes = DxfWriter::new(&doc).write_to_vec().unwrap();
let dxf_doc = DxfReader::from_reader(Cursor::new(dxf_bytes)).unwrap().read().unwrap();
let ObjectType::DynamicBlock(dxf_o) = dxf_doc.objects.get(&action).unwrap() else {
panic!()
};
let DynamicBlockData::ArrayAction(dxf_arr) = &dxf_o.data else {
panic!()
};
assert_eq!(dxf_arr.column_offset, column_offset);
assert_eq!(dxf_arr.row_offset, row_offset);
assert_eq!(dxf_arr.action.dependencies, vec![targets[0]]);
assert_eq!(dxf_arr.connections, arr.connections);
}
}
#[test]
fn block_actions_array_linear_and_polar_dynamic_evaluation() {
let (mut doc, def, targets) = fixture();
let p = distance::create(
&mut doc,
def,
distance::Kind::Linear,
Vector3::ZERO,
Vector3::new(10., 0., 0.),
Vector3::new(5., 2., 0.),
)
.unwrap();
create_array(
&mut doc,
def,
p,
&[targets[0]],
DVec3::ZERO,
2.0,
1.0,
)
.unwrap();
let baseline = doc.clone();
// At distance 1.0 (< column_offset 2.0), only base item exists
evaluate(&mut doc, &baseline, def, p, DVec3::new(1.0, 0.0, 0.0)).unwrap();
let members = authoring::members(&doc, def);
assert_eq!(members.len(), 2); // original line + circle
// At distance 2.0 (>= 2.0), 1 clone is added -> 3 members total
evaluate(&mut doc, &baseline, def, p, DVec3::new(2.0, 0.0, 0.0)).unwrap();
let members = authoring::members(&doc, def);
assert_eq!(members.len(), 3);
let lines: Vec<_> = members
.iter()
.filter_map(|&h| match doc.get_entity(h) {
Some(EntityType::Line(l)) => Some((h, l.clone())),
_ => None,
})
.collect();
assert_eq!(lines.len(), 2);
// Original line was from (1,0,0) to (2,0,0). Clone should be translated by (2,0,0): from (3,0,0) to (4,0,0)
assert!(lines.iter().any(|(_, l)| (l.start - Vector3::new(3., 0., 0.)).length() < 1e-8));
// At distance 6.5, items count = floor(6.5/2.0)+1 = 4 -> 3 clones + 1 base = 4 lines + 1 circle = 5 members
evaluate(&mut doc, &baseline, def, p, DVec3::new(6.5, 0.0, 0.0)).unwrap();
let members = authoring::members(&doc, def);
assert_eq!(members.len(), 5);
// Shrink back to 0.5: clones are removed, reverting to original 2 members
evaluate(&mut doc, &baseline, def, p, DVec3::new(0.5, 0.0, 0.0)).unwrap();
let members = authoring::members(&doc, def);
assert_eq!(members.len(), 2);
// Polar parameter at angle 45 deg
let (mut doc, def, targets) = fixture();
let p_polar = distance::create(
&mut doc,
def,
distance::Kind::Polar,
Vector3::ZERO,
Vector3::new(10., 0., 0.),
Vector3::new(5., 2., 0.),
)
.unwrap();
create_array(
&mut doc,
def,
p_polar,
&[targets[0]],
DVec3::ZERO,
2.0,
1.0,
)
.unwrap();
let baseline = doc.clone();
// Grip moved to (3.0, 4.0, 0) -> distance 5.0, axis (0.6, 0.8, 0)
// n = floor(5.0/2.0) + 1 = 3 items (1 base + 2 clones)
evaluate(&mut doc, &baseline, def, p_polar, DVec3::new(3.0, 4.0, 0.0)).unwrap();
let members = authoring::members(&doc, def);
assert_eq!(members.len(), 4); // 3 lines + 1 circle
let lines: Vec<_> = members
.iter()
.filter_map(|&h| match doc.get_entity(h) {
Some(EntityType::Line(l)) => Some((h, l.clone())),
_ => None,
})
.collect();
assert_eq!(lines.len(), 3);
assert!(lines.iter().any(|(_, l)| (l.start - Vector3::new(1.0 + 1.2, 1.6, 0.)).length() < 1e-8));
assert!(lines.iter().any(|(_, l)| (l.start - Vector3::new(1.0 + 2.4, 3.2, 0.)).length() < 1e-8));
}
#[test]
fn block_actions_array_xy_grid_evaluation() {
let (mut doc, def, targets) = fixture();
let p_xy = distance::create(
&mut doc,
def,
distance::Kind::XY,
Vector3::ZERO,
Vector3::new(10., 10., 0.),
Vector3::new(5., 5., 0.),
)
.unwrap();
create_array(
&mut doc,
def,
p_xy,
&[targets[0]],
DVec3::ZERO,
3.0,
4.0,
)
.unwrap();
let baseline = doc.clone();
// Move XY grip to (6.0, 8.0, 0.0)
// nx = floor(6/3)+1 = 3 (cols 0, 3, 6)
// ny = floor(8/4)+1 = 3 (rows 0, 4, 8)
// total items: 3 * 3 = 9 (1 base + 8 clones)
evaluate(&mut doc, &baseline, def, p_xy, DVec3::new(6.0, 8.0, 0.0)).unwrap();
let members = authoring::members(&doc, def);
assert_eq!(members.len(), 10); // 9 lines + 1 circle
let lines: Vec<_> = members
.iter()
.filter_map(|&h| match doc.get_entity(h) {
Some(EntityType::Line(l)) => Some(l.clone()),
_ => None,
})
.collect();
assert_eq!(lines.len(), 9);
// Verify all 9 grid positions exist: (col*3, row*4) + base (1, 0, 0)
for col in 0..3 {
for row in 0..3 {
let expected_start = Vector3::new(1.0 + (col as f64) * 3.0, (row as f64) * 4.0, 0.0);
assert!(
lines.iter().any(|l| (l.start - expected_start).length() < 1e-8),
"Missing line at col {col}, row {row}"
);
}
}
}
#[test]
fn block_actions_array_saved_references_independent_copy_and_roundtrip() {
let (mut doc, def, targets) = fixture();
let p = distance::create(
&mut doc,
def,
distance::Kind::Linear,
Vector3::ZERO,
Vector3::new(10., 0., 0.),
Vector3::new(5., 2., 0.),
)
.unwrap();
create_array(
&mut doc,
def,
p,
&[targets[0]],
DVec3::ZERO,
2.0,
1.0,
)
.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(100., 0., 0.),
)))
.unwrap();
// Evaluate A at distance 6.0 (3 clones + 1 base = 4 lines)
let doc = instance::evaluate(&doc, a, p, DVec3::new(6.0, 0.0, 0.0))
.unwrap()
.unwrap();
// Evaluate B at distance 2.0 (1 clone + 1 base = 2 lines)
let mut doc = instance::evaluate(&doc, b, p, DVec3::new(2.0, 0.0, 0.0))
.unwrap()
.unwrap();
let EntityType::Insert(ref_a) = doc.get_entity(a).unwrap() else {
panic!()
};
let EntityType::Insert(ref_b) = doc.get_entity(b).unwrap() else {
panic!()
};
assert_ne!(ref_a.block_name, ref_b.block_name);
let block_a = doc.block_records.get(&ref_a.block_name).unwrap();
let block_b = doc.block_records.get(&ref_b.block_name).unwrap();
let a_lines = block_a
.entity_handles
.iter()
.filter(|&&h| matches!(doc.get_entity(h), Some(EntityType::Line(_))))
.count();
let b_lines = block_b
.entity_handles
.iter()
.filter(|&&h| matches!(doc.get_entity(h), Some(EntityType::Line(_))))
.count();
assert_eq!(a_lines, 4);
assert_eq!(b_lines, 2);
// Copy reference A -> C
let mut insert_c = ref_a.clone();
let c = doc.allocate_handle();
insert_c.common.handle = c;
insert_c.insert_point = Vector3::new(200., 0., 0.);
doc.add_entity(EntityType::Insert(insert_c)).unwrap();
// Roundtrip to DWG
let reopened = DwgReader::from_stream(Cursor::new(DwgWriter::write_to_vec(&doc).unwrap()))
.read()
.unwrap();
let EntityType::Insert(reopened_a) = reopened.get_entity(a).unwrap() else {
panic!()
};
let EntityType::Insert(reopened_b) = reopened.get_entity(b).unwrap() else {
panic!()
};
let EntityType::Insert(reopened_c) = reopened.get_entity(c).unwrap() else {
panic!()
};
let rec_a = reopened.block_records.get(&reopened_a.block_name).unwrap();
let rec_b = reopened.block_records.get(&reopened_b.block_name).unwrap();
let rec_c = reopened.block_records.get(&reopened_c.block_name).unwrap();
assert_eq!(
rec_a
.entity_handles
.iter()
.filter(|&&h| matches!(reopened.get_entity(h), Some(EntityType::Line(_))))
.count(),
4
);
assert_eq!(
rec_b
.entity_handles
.iter()
.filter(|&&h| matches!(reopened.get_entity(h), Some(EntityType::Line(_))))
.count(),
2
);
assert_eq!(
rec_c
.entity_handles
.iter()
.filter(|&&h| matches!(reopened.get_entity(h), Some(EntityType::Line(_))))
.count(),
4
);
}
#[test]
fn block_actions_array_rejections() {
let (mut doc, def, targets) = fixture();
let p_point = point::create(&mut doc, def, Vector3::ZERO, Vector3::new(5., 5., 0.)).unwrap();
assert!(create_array(&mut doc, def, p_point, &targets, DVec3::ZERO, 2.0, 1.0).is_err());
let p_rot = orientation(
&mut doc,
def,
super::super::block_orientation_parameter::Kind::Rotation,
);
assert!(create_array(&mut doc, def, p_rot, &targets, DVec3::ZERO, 2.0, 1.0).is_err());
let p_lin = distance::create(
&mut doc,
def,
distance::Kind::Linear,
Vector3::ZERO,
Vector3::new(10., 0., 0.),
Vector3::new(5., 2., 0.),
)
.unwrap();
assert!(create_array(&mut doc, def, p_lin, &targets, DVec3::ZERO, 0.0, 1.0).is_err());
assert!(create_array(&mut doc, def, p_lin, &targets, DVec3::ZERO, -2.0, 1.0).is_err());
}
+6 -1
View File
@@ -287,7 +287,12 @@ pub fn move_part(
p.definition_label_point = at p.definition_label_point = at
} }
} }
DynamicBlockData::MoveAction(p) => p.action.display_location = at, DynamicBlockData::MoveAction(p) => {
p.action.display_location = at
}
DynamicBlockData::ArrayAction(p) => {
p.action.display_location = at
}
DynamicBlockData::StretchAction(p) => p.action.display_location = at, DynamicBlockData::StretchAction(p) => p.action.display_location = at,
DynamicBlockData::FlipAction(p) => p.action.display_location = at, DynamicBlockData::FlipAction(p) => p.action.display_location = at,
DynamicBlockData::ScaleAction(p) | DynamicBlockData::RotateAction(p) => { DynamicBlockData::ScaleAction(p) | DynamicBlockData::RotateAction(p) => {
+8 -5
View File
@@ -199,6 +199,7 @@ fn catalog(
rotate_error: Option<&'static str>, rotate_error: Option<&'static str>,
flip_action_error: Option<&'static str>, flip_action_error: Option<&'static str>,
stretch_action_error: Option<&'static str>, stretch_action_error: Option<&'static str>,
array_action_error: Option<&'static str>,
) -> Element<'static, Message> { ) -> Element<'static, Message> {
let (entries, unavailable) = match tab { let (entries, unavailable) = match tab {
Tab::Parameters => ( Tab::Parameters => (
@@ -220,8 +221,8 @@ fn catalog(
let orientation_error = if *label=="Flip" {flip_error} else {point_error}; let orientation_error = if *label=="Flip" {flip_error} else {point_error};
let single=tab==Tab::Parameters&&matches!(*label,"Lookup"|"Basepoint"); let single=tab==Tab::Parameters&&matches!(*label,"Lookup"|"Basepoint");
let lookup_action=tab==Tab::Actions&&*label=="Lookup"; let lookup_action=tab==Tab::Actions&&*label=="Lookup";
let geometry_action=tab==Tab::Actions&&matches!(*label,"Move"|"Scale"|"Stretch"|"Rotate"|"Flip"); let geometry_action=tab==Tab::Actions&&matches!(*label,"Move"|"Scale"|"Stretch"|"Rotate"|"Flip"|"Array");
let action_error=match *label {"Scale"=>scale_error,"Stretch"=>stretch_action_error,"Rotate"=>rotate_error,"Flip"=>flip_action_error,_=>move_error}; let action_error=match *label {"Scale"=>scale_error,"Stretch"=>stretch_action_error,"Rotate"=>rotate_error,"Flip"=>flip_action_error,"Array"=>array_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 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 { let base = if enabled {
icons::semantic(icon, 28.0) icons::semantic(icon, 28.0)
@@ -267,7 +268,7 @@ fn catalog(
} else if single { } 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() 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 { } else if geometry_action {
crate::t!(action_error.unwrap_or(if *label=="Stretch" {"Select a parameter endpoint, define the stretch frame, select its objects, then place the action marker."}else{"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() crate::t!(action_error.unwrap_or(if *label=="Stretch" {"Select a parameter endpoint, define the stretch frame, select its objects, then place the action marker."}else if *label=="Array" {"Select a parameter endpoint, select the objects to array, specify the column offset, then place the action marker."}else{"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 { } else if lookup_action {
crate::t!(lookup_error.unwrap_or("Create or edit property Lookup tables for this block.")).into_owned() crate::t!(lookup_error.unwrap_or("Create or edit property Lookup tables for this block.")).into_owned()
} else { } else {
@@ -285,7 +286,7 @@ fn catalog(
} else if single { } else if single {
container(entry).id(if *label=="Lookup" {"authoring-tool-lookup"}else{"authoring-tool-basepoint"}).into() container(entry).id(if *label=="Lookup" {"authoring-tool-lookup"}else{"authoring-tool-basepoint"}).into()
} else if geometry_action { } else if geometry_action {
container(entry).id(match *label {"Move"=>"authoring-tool-move-action","Scale"=>"authoring-tool-scale-action","Rotate"=>"authoring-tool-rotate-action","Stretch"=>"authoring-tool-stretch-action",_=>"authoring-tool-flip-action"}).into() container(entry).id(match *label {"Move"=>"authoring-tool-move-action","Scale"=>"authoring-tool-scale-action","Rotate"=>"authoring-tool-rotate-action","Stretch"=>"authoring-tool-stretch-action","Array"=>"authoring-tool-array-action",_=>"authoring-tool-flip-action"}).into()
} else if lookup_action { } else if lookup_action {
container(entry).id("authoring-tool-lookup-table").into() container(entry).id("authoring-tool-lookup-table").into()
} else { } else {
@@ -407,6 +408,7 @@ pub fn view(
rotate_error: Option<&'static str>, rotate_error: Option<&'static str>,
flip_action_error: Option<&'static str>, flip_action_error: Option<&'static str>,
stretch_action_error: Option<&'static str>, stretch_action_error: Option<&'static str>,
array_action_error: Option<&'static str>,
) -> Element<'static, Message> { ) -> Element<'static, Message> {
let tabs = column(Tab::ALL.into_iter().map(|category| { let tabs = column(Tab::ALL.into_iter().map(|category| {
let label = category.label(); let label = category.label();
@@ -466,7 +468,8 @@ pub fn view(
scale_error, scale_error,
rotate_error, rotate_error,
flip_action_error, flip_action_error,
stretch_action_error stretch_action_error,
array_action_error,
)) ))
.width(Fill) .width(Fill)
.height(Fill), .height(Fill),