feat(array): ARRAYRECT and ARRAYPOLAR create associative arrays

With the new Associative option on (the default), ARRAYRECT and
ARRAYPOLAR build the array the way the drawing format stores one: the
selected objects move into an anonymous source block, an anonymous
array block holds one ByBlock reference to it per item, an INSERT of
the array block takes their place, and an AcDbAssocAction with an
AcDbAssocArrayActionBody, its three dependencies, the BaseNormal /
Origin / basePoint parameters and the value parameters tie them
together in the model-space network. The array is then edited from
Properties like an array read from a file.

The option flips with AS and is remembered for the session; on a
rotated working plane the commands keep making plain copies. Saving
drops the action of an array whose reference was erased or exploded.

Checked: created arrays reopen as associative with their parameters
and items, and the ODA converter reads the saved DWG without errors.

Refs #1109

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-13 21:23:48 +03:00
1 parent 2bf77e4bd6
commit f0ac89629b
30 files changed
+705 -3

No files matched your search

+2
View File
@@ -17,6 +17,7 @@ thread_local! {
static ARRAY_P_COUNT: Cell<f64> = Cell::new(6.0);
static ARRAY_P_ANGLE: Cell<f64> = Cell::new(360.0); // degrees
static ARRAY_PATH_COUNT: Cell<f64> = Cell::new(6.0);
static ARRAY_ASSOC: Cell<f64> = Cell::new(1.0); // 1 = associative
static POLYGON_SIDES: Cell<f64> = Cell::new(6.0);
static RECT_ELEVATION: Cell<f64> = Cell::new(0.0);
static RECT_THICKNESS: Cell<f64> = Cell::new(0.0);
@@ -61,6 +62,7 @@ accessors!(get_fillet_radius, set_fillet_radius, FILLET_RADIUS);
accessors!(get_chamfer_dist1, set_chamfer_dist1, CHAMFER_DIST1);
accessors!(get_chamfer_dist2, set_chamfer_dist2, CHAMFER_DIST2);
accessors!(get_array_rows, set_array_rows, ARRAY_ROWS);
accessors!(get_array_assoc, set_array_assoc, ARRAY_ASSOC);
accessors!(get_array_cols, set_array_cols, ARRAY_COLS);
accessors!(get_array_row_sp, set_array_row_sp, ARRAY_ROW_SP);
accessors!(get_array_col_sp, set_array_col_sp, ARRAY_COL_SP);
+66
View File
@@ -19,6 +19,29 @@ use glam::DVec3;
use crate::command::{CadCommand, CmdResult, EntityTransform, WorkingPlane};
use crate::modules::draw::defaults;
/// The Associative option button, labelled with its current state.
fn associative_option(on: bool) -> crate::command::CmdOption {
crate::command::CmdOption {
label: format!(
"{}: {}",
crate::t!("Associative"),
crate::t!(if on { "Yes" } else { "No" })
),
keyword: "AS".to_string(),
}
}
/// Typed `AS` / `ASSOCIATIVE` flips the Associative option.
fn is_associative_keyword(text: &str) -> bool {
matches!(text.trim().to_ascii_uppercase().as_str(), "AS" | "ASSOCIATIVE" | "_AS")
}
/// Associative arrays are built in world axes; a rotated working plane
/// falls back to plain copies.
fn world_plane(plane: WorkingPlane) -> bool {
plane.x.abs_diff_eq(DVec3::X, 1e-12) && plane.z.abs_diff_eq(DVec3::Z, 1e-12)
}
use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel;
@@ -84,6 +107,7 @@ pub struct ArrayRectCommand {
default_cols: u32,
default_row_sp: f64,
default_col_sp: f64,
associative: bool,
plane: WorkingPlane,
}
@@ -97,6 +121,7 @@ impl ArrayRectCommand {
default_cols: (defaults::get_array_cols() as u32).clamp(1, MAX_ARRAY_ITEMS as u32),
default_row_sp: defaults::get_array_row_sp(),
default_col_sp: defaults::get_array_col_sp(),
associative: defaults::get_array_assoc() != 0.0,
plane: WorkingPlane::default(),
}
}
@@ -129,6 +154,10 @@ impl CadCommand for ArrayRectCommand {
"ARRAYRECT"
}
fn options(&self) -> Vec<crate::command::CmdOption> {
vec![associative_option(self.associative)]
}
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
@@ -165,6 +194,11 @@ impl CadCommand for ArrayRectCommand {
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if is_associative_keyword(text) {
self.associative = !self.associative;
defaults::set_array_assoc(f64::from(u8::from(self.associative)));
return None;
}
let t = text.trim().replace(',', ".");
let t = t.as_str();
match self.step {
@@ -221,6 +255,17 @@ impl CadCommand for ArrayRectCommand {
defaults::set_array_col_sp(v);
v
};
if self.associative && world_plane(self.plane) {
return Some(CmdResult::AssocArray {
handles: self.handles.clone(),
spec: crate::scene::assoc_array::AssocArraySpec::Rectangular {
columns: cols,
rows,
column_spacing: col_sp,
row_spacing: row_sp,
},
});
}
Some(CmdResult::BatchCopy(
self.handles.clone(),
Self::build_transforms(rows, cols, row_sp, col_sp, self.plane),
@@ -295,6 +340,7 @@ pub struct ArrayPolarCommand {
step: PolarStep,
default_count: u32,
default_angle: f64,
associative: bool,
plane: WorkingPlane,
}
@@ -306,6 +352,7 @@ impl ArrayPolarCommand {
step: PolarStep::Center,
default_count: (defaults::get_array_p_count() as u32).clamp(2, MAX_ARRAY_ITEMS as u32),
default_angle: defaults::get_array_p_angle(),
associative: defaults::get_array_assoc() != 0.0,
plane: WorkingPlane::default(),
}
}
@@ -320,6 +367,10 @@ impl CadCommand for ArrayPolarCommand {
"ARRAYPOLAR"
}
fn options(&self) -> Vec<crate::command::CmdOption> {
vec![associative_option(self.associative)]
}
fn prompt(&self) -> String {
match &self.step {
PolarStep::Center => crate::tf!(
@@ -351,6 +402,11 @@ impl CadCommand for ArrayPolarCommand {
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if is_associative_keyword(text) {
self.associative = !self.associative;
defaults::set_array_assoc(f64::from(u8::from(self.associative)));
return None;
}
let t = text.trim().replace(',', ".");
let t = t.as_str();
match &self.step {
@@ -383,6 +439,16 @@ impl CadCommand for ArrayPolarCommand {
defaults::set_array_p_angle(v);
v
};
if self.associative && world_plane(self.plane) {
return Some(CmdResult::AssocArray {
handles: self.handles.clone(),
spec: crate::scene::assoc_array::AssocArraySpec::Polar {
center,
items: count,
fill_angle: total_deg.to_radians(),
},
});
}
let step_rad = total_deg.to_radians() / count as f64;
let transforms = (1..count)
.map(|n| EntityTransform::Rotate {