diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 54ddae65e..d7f98e422 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -295,6 +295,7 @@ command-copy = .mode = COPY أدخل خيار وضع النسخ [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__: تم إنشاء مصفوفة ترابطية. .assoc-array-array-path = مصفوفة (مسار) .assoc-array-array-polar = مصفوفة (قطبية) .assoc-array-array-rectangular = مصفوفة (مستطيلة) diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index caac7af4f..cb7085f4d 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -282,6 +282,7 @@ command-copy = .displacement = COPY Задайте преместване: .mode = COPY Въведете режим на копиране [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__: създаден е асоциативен масив. .assoc-array-array-path = Масив (по път) .assoc-array-array-polar = Масив (полярен) .assoc-array-array-rectangular = Масив (правоъгълен) diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 4d14ecb17..71bf17c6f 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -282,6 +282,7 @@ command-copy = .mode = COPY Zadejte režim kopírování [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__: vytvořeno asociativní pole. .assoc-array-array-path = Pole (podél cesty) .assoc-array-array-polar = Pole (kruhové) .assoc-array-array-rectangular = Pole (obdélníkové) diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index f7f626bcb..6ef673520 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -283,6 +283,7 @@ command-copy = .mode = COPY Kopiermodus eingeben [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__: assoziative Reihe erstellt. .assoc-array-array-path = Reihe (Pfad) .assoc-array-array-polar = Reihe (polar) .assoc-array-array-rectangular = Reihe (rechteckig) diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 665c5f931..48f3a6273 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -282,6 +282,7 @@ command-copy = .mode = COPY Επιλέξτε λειτουργία αντιγραφής [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__: δημιουργήθηκε συσχετιστικός πίνακας. .assoc-array-array-path = Πίνακας (διαδρομή) .assoc-array-array-polar = Πίνακας (πολικός) .assoc-array-array-rectangular = Πίνακας (ορθογώνιος) diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 1a7e856c3..d1781a219 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -282,6 +282,7 @@ command-copy = .mode = COPY Enter a copy mode option [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__: associative array created. .assoc-array-array-path = Array (Path) .assoc-array-array-polar = Array (Polar) .assoc-array-array-rectangular = Array (Rectangular) diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index ef384b506..96d19d420 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -283,6 +283,7 @@ command-copy = .mode = COPY Indique modo de copia [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__: se ha creado una matriz asociativa. .assoc-array-array-path = Matriz (trayectoria) .assoc-array-array-polar = Matriz (polar) .assoc-array-array-rectangular = Matriz (rectangular) diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index a2780d625..672a1be0e 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -282,6 +282,7 @@ command-copy = .mode = COPY Valitse kopiointitila [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__: assosiatiivinen taulukko luotu. .assoc-array-array-path = Taulukko (polku) .assoc-array-array-polar = Taulukko (polaarinen) .assoc-array-array-rectangular = Taulukko (suorakulmainen) diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index f0a89f5fc..eb9f59748 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -283,6 +283,7 @@ command-copy = .mode = COPY Entrez le mode de copie [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__ : réseau associatif créé. .assoc-array-array-path = Réseau (trajectoire) .assoc-array-array-polar = Réseau (polaire) .assoc-array-array-rectangular = Réseau (rectangulaire) diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 6f8c8cff4..6c58c7b57 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -271,6 +271,7 @@ command-copy = .mode = COPY कॉपी मोड विकल्प दर्ज करें [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__: सहयोगी ऐरे बनाया गया। .assoc-array-array-path = ऐरे (पथ) .assoc-array-array-polar = ऐरे (ध्रुवीय) .assoc-array-array-rectangular = ऐरे (आयताकार) diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index d9e0e6191..75d445a98 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -281,6 +281,7 @@ command-copy = .mode = COPY Adja meg a másolási módot [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__: asszociatív tömb létrehozva. .assoc-array-array-path = Tömb (útvonal) .assoc-array-array-polar = Tömb (poláris) .assoc-array-array-rectangular = Tömb (téglalap) diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 084c7be63..fc91c30db 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -282,6 +282,7 @@ command-copy = .mode = COPY Specificare modalità di copia [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__: serie associativa creata. .assoc-array-array-path = Serie (percorso) .assoc-array-array-polar = Serie (polare) .assoc-array-array-rectangular = Serie (rettangolare) diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index c8e6e5786..5fbb8a6e6 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -273,6 +273,7 @@ command-copy = .mode = COPY コピーモードを入力 [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__: 自動調整配列複写を作成しました。 .assoc-array-array-path = 配列複写 (パス) .assoc-array-array-polar = 配列複写 (円形状) .assoc-array-array-rectangular = 配列複写 (矩形状) diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index bc8c8e056..8d25f85c4 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -282,6 +282,7 @@ command-copy = .mode = COPY 복사 모드 옵션 입력 [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__: 연관 배열을 만들었습니다. .assoc-array-array-path = 배열 (경로) .assoc-array-array-polar = 배열 (원형) .assoc-array-array-rectangular = 배열 (직사각형) diff --git a/locales/lo-LA/opencadstudio.ftl b/locales/lo-LA/opencadstudio.ftl index dc08d761c..c188faa06 100644 --- a/locales/lo-LA/opencadstudio.ftl +++ b/locales/lo-LA/opencadstudio.ftl @@ -282,6 +282,7 @@ command-copy = .mode = COPY ປ້ອນຕົວເລືອກໂໝດການຄັດລອກ [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__: ສ້າງອາເຣແບບເຊື່ອມໂຍງແລ້ວ. .assoc-array-array-path = ອາເຣ (ເສັ້ນທາງ) .assoc-array-array-polar = ອາເຣ (ວົງມົນ) .assoc-array-array-rectangular = ອາເຣ (ສີ່ຫຼ່ຽມ) diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 93ef29985..f621a55f9 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -283,6 +283,7 @@ command-copy = .mode = COPY Geef kopieermodus op [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__: associatieve reeks gemaakt. .assoc-array-array-path = Reeks (pad) .assoc-array-array-polar = Reeks (polair) .assoc-array-array-rectangular = Reeks (rechthoekig) diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 37ceb77c7..648827533 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -282,6 +282,7 @@ command-copy = .mode = COPY Podaj tryb kopiowania [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__: utworzono szyk skojarzony. .assoc-array-array-path = Szyk (ścieżka) .assoc-array-array-polar = Szyk (biegunowy) .assoc-array-array-rectangular = Szyk (prostokątny) diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 8ff75756a..eacb449da 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -283,6 +283,7 @@ command-copy = .mode = COPY Informe o modo de cópia [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__: matriz associativa criada. .assoc-array-array-path = Matriz (caminho) .assoc-array-array-polar = Matriz (polar) .assoc-array-array-rectangular = Matriz (retangular) diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 2deb1f83f..eac8d2f50 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -286,6 +286,7 @@ command-copy = .mode = COPY Задайте режим копирования [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__: создан ассоциативный массив. .assoc-array-array-path = Массив (по траектории) .assoc-array-array-polar = Массив (круговой) .assoc-array-array-rectangular = Массив (прямоугольный) diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 103792d3a..de6964037 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -268,6 +268,7 @@ command-copy = .mode = COPY Kopyalama modu seçin [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__: ilişkisel dizi oluşturuldu. .assoc-array-array-path = Dizi (Yol) .assoc-array-array-polar = Dizi (Kutupsal) .assoc-array-array-rectangular = Dizi (Dikdörtgen) diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 14d64345a..c1c21d60e 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -270,6 +270,7 @@ command-copy = .mode = COPY 输入复制模式选项 [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__: 已创建关联阵列。 .assoc-array-array-path = 阵列 (路径) .assoc-array-array-polar = 阵列 (环形) .assoc-array-array-rectangular = 阵列 (矩形) diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index be38f6cbc..11dd5b6ee 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -282,6 +282,7 @@ command-copy = .mode = COPY 輸入複製模式選項 [Single/Multiple] : common = + .label-associative-array-created = __ocs_fmt_0__: 已建立關聯陣列。 .assoc-array-array-path = 陣列 (路徑) .assoc-array-array-polar = 陣列 (環形) .assoc-array-array-rectangular = 陣列 (矩形) diff --git a/src/app/command_driver/clipboard.rs b/src/app/command_driver/clipboard.rs index 9c1ffc150..1bfcfce95 100644 --- a/src/app/command_driver/clipboard.rs +++ b/src/app/command_driver/clipboard.rs @@ -66,6 +66,46 @@ impl OpenCADStudio { None } + /// ARRAYRECT / ARRAYPOLAR with Associative on: the objects become the + /// source of an associative array, in one undo step. + pub(super) fn handle_assoc_array( + &mut self, + mut handles: Vec, + spec: crate::scene::assoc_array::AssocArraySpec, + ) { + let i = self.active_tab; + handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle)); + self.tabs[i].scene.clear_preview_wire(); + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.restore_pre_cmd_tangent(); + if handles.is_empty() { + return; + } + let label = self.history_label_from_active_cmd(i, "ARRAY"); + let source_point = crate::app::helpers::entities_lower_left_by_bbox( + &self.tabs[i].scene.document, + &handles, + ); + self.push_undo_snapshot(i, label.clone()); + match crate::scene::assoc_array::create_from_spec( + &mut self.tabs[i].scene, + &handles, + source_point, + &spec, + ) { + Ok(array) => { + self.tabs[i].scene.deselect_all(); + self.tabs[i].scene.select_entity(array, false); + self.command_line + .push_output(crate::tf!("{label}: associative array created.").as_ref()); + } + Err(error) => self.command_line.push_error(&format!("{label}: {error}")), + } + self.tabs[i].dirty = true; + self.refresh_properties(); + } + pub(super) fn handle_replace_many( &mut self, replacements: Vec<(Handle, Vec)>, diff --git a/src/app/command_driver/mod.rs b/src/app/command_driver/mod.rs index 5a7580e02..7c4a40807 100644 --- a/src/app/command_driver/mod.rs +++ b/src/app/command_driver/mod.rs @@ -606,6 +606,9 @@ impl OpenCADStudio { return task; } } + CmdResult::AssocArray { handles, spec } => { + self.handle_assoc_array(handles, spec); + } CmdResult::ReplaceMany(replacements, additions) => { if let Some(task) = self.handle_replace_many(replacements, additions) { return task; diff --git a/src/command.rs b/src/command.rs index a7a24d303..b3b3a9b1c 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1621,6 +1621,11 @@ pub enum CmdResult { }, /// Copy selected entities with multiple transforms (e.g. rectangular array); end command. BatchCopy(Vec, Vec), + /// Turn the objects into an associative array; end command. + AssocArray { + handles: Vec, + spec: crate::scene::assoc_array::AssocArraySpec, + }, /// Erase `handle` and replace with new entities; command stays active. ReplaceEntity(Handle, Vec), /// XCLIP on block references; ends the command. diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index c73cbb067..9bb77b77b 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -3960,6 +3960,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "{exploded} object(s) exploded." => Some(("annotate", "exploded-object-s-exploded")), "{label}:" => Some(("command", "label")), "{label}: {count} {noun} created." => Some(("common", "label-count-noun-created")), + "{label}: associative array created." => Some(("common", "label-associative-array-created")), "{n} object(s) erased." => Some(("draw", "n-object-s-erased")), "{prefix}: '{style_name}' font set to '{font}'." => Some(("layers", "prefix-style-name-font-set-to-font")), "{prefix}: '{style_name}' oblique angle set to {deg:.1}°." => Some(("layers", "prefix-style-name-oblique-angle-set-to-deg-1")), diff --git a/src/modules/draw/defaults.rs b/src/modules/draw/defaults.rs index ac0ce079e..0da88ee54 100644 --- a/src/modules/draw/defaults.rs +++ b/src/modules/draw/defaults.rs @@ -17,6 +17,7 @@ thread_local! { static ARRAY_P_COUNT: Cell = Cell::new(6.0); static ARRAY_P_ANGLE: Cell = Cell::new(360.0); // degrees static ARRAY_PATH_COUNT: Cell = Cell::new(6.0); + static ARRAY_ASSOC: Cell = Cell::new(1.0); // 1 = associative static POLYGON_SIDES: Cell = Cell::new(6.0); static RECT_ELEVATION: Cell = Cell::new(0.0); static RECT_THICKNESS: Cell = 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); diff --git a/src/modules/draw/modify/array.rs b/src/modules/draw/modify/array.rs index fdf7ed41b..ff9dbb9a7 100644 --- a/src/modules/draw/modify/array.rs +++ b/src/modules/draw/modify/array.rs @@ -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 { + 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 { + 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 { + 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 { + 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 { diff --git a/src/scene/assoc_array.rs b/src/scene/assoc_array.rs index 133b5b51d..7402a7f26 100644 --- a/src/scene/assoc_array.rs +++ b/src/scene/assoc_array.rs @@ -108,6 +108,24 @@ pub fn array_of_insert(document: &CadDocument, insert: Handle) -> Option bool { + let Some(action) = action(document, handle) else { + return false; + }; + if body(document, action.action_body).is_none() { + return false; + } + !action.dependencies.iter().any(|link| { + dependency(document, link.dependency).is_some_and(|dependency| { + dependency.is_read_dependency + && dependency.is_write_dependency + && document.get_entity(dependency.dependent_on).is_some() + }) + }) +} + /// The value parameters of an array action: inline in the action (R2013+) /// or in the body's parameter section (R2007). pub fn value_params<'a>(document: &'a CadDocument, array: &AssocArray) -> &'a [AssocValueParam] { @@ -556,3 +574,542 @@ pub fn update( scene.bump_geometry(); Ok(()) } + +/// Class records the array objects need, with the values the reference +/// product writes: (DXF name, class name, DWG version, maintenance version). +const ARRAY_CLASSES: &[(&str, &str, i32, i32)] = &[ + ("ACDBASSOCNETWORK", "AcDbAssocNetwork", 27, 45), + ("ACDBASSOCACTION", "AcDbAssocAction", 27, 45), + ("ACDBASSOCDEPENDENCY", "AcDbAssocDependency", 27, 50), + ("ACDBASSOCVERTEXACTIONPARAM", "AcDbAssocVertexActionParam", 27, 50), + ("ACDBASSOCEDGEACTIONPARAM", "AcDbAssocEdgeActionParam", 27, 50), + ("ACDBASSOCARRAYACTIONBODY", "AcDbAssocArrayActionBody", 29, 55), + ("ACDBASSOCARRAYRECTANGULARPARAMETERS", "AcDbAssocArrayRectangularParameters", 29, 55), + ("ACDBASSOCARRAYPOLARPARAMETERS", "AcDbAssocArrayPolarParameters", 29, 55), +]; + +fn register_array_classes(document: &mut CadDocument) { + for &(dxf_name, cpp_class_name, dwg_version, maintenance_version) in ARRAY_CLASSES { + if document.classes.get_by_name(dxf_name).is_some() { + continue; + } + document.classes.add_or_update(codec::classes::DxfClass { + dxf_name: dxf_name.to_string(), + cpp_class_name: cpp_class_name.to_string(), + application_name: "ObjectDBX Classes".to_string(), + proxy_flags: codec::classes::ProxyFlags(1025), + instance_count: 0, + was_zombie: false, + is_an_entity: false, + class_number: 0, + item_class_id: 0x1F3, + dwg_version, + maintenance_version, + unknown1: 0, + unknown2: 0, + }); + } +} + +fn next_anonymous_name(document: &CadDocument) -> String { + let next = document + .block_records + .iter() + .filter_map(|record| record.name.strip_prefix("*U")?.parse::().ok()) + .max() + .map_or(1, |n| n + 1); + format!("*U{next}") +} + +fn associative( + handle: Handle, + owner: Handle, + dxf_name: &str, + cpp_class_name: &str, + data: AssociativeData, +) -> ObjectType { + let mut object = codec::objects::AssociativeObject::new(dxf_name, cpp_class_name); + object.handle = handle; + object.owner = owner; + object.data = data; + ObjectType::Associative(object) +} + +fn dictionary_entry(document: &CadDocument, dictionary: Handle, key: &str) -> Option { + match document.objects.get(&dictionary)? { + ObjectType::Dictionary(dictionary) => dictionary.get(key), + _ => None, + } +} + +/// A dictionary stored under `key` in `parent`, created when absent. +fn child_dictionary(document: &mut CadDocument, parent: Handle, key: &str) -> Handle { + if let Some(handle) = dictionary_entry(document, parent, key) + .filter(|handle| matches!(document.objects.get(handle), Some(ObjectType::Dictionary(_)))) + { + return handle; + } + let handle = document.allocate_handle(); + let mut dictionary = codec::objects::Dictionary::new(); + dictionary.handle = handle; + dictionary.owner = parent; + dictionary.reactors.push(parent); + document.objects.insert(handle, ObjectType::Dictionary(dictionary)); + if let Some(ObjectType::Dictionary(parent)) = document.objects.get_mut(&parent) { + parent.add_entry(key, handle); + } + handle +} + +/// The network holding the actions of model space, created with the root +/// network above it when the drawing has none. +fn model_space_network(document: &mut CadDocument) -> Handle { + const KEY: &str = "ACAD_ASSOCNETWORK"; + let model = document.header.model_space_block_handle; + let scope = document.ensure_extension_dictionary(model); + let holder = child_dictionary(document, scope, KEY); + if let Some(network) = dictionary_entry(document, holder, KEY).filter(|handle| { + matches!( + document.objects.get(handle), + Some(ObjectType::Associative(object)) + if matches!(object.data, AssociativeData::Network(_)) + ) + }) { + return network; + } + let root_holder = child_dictionary(document, document.header.named_objects_dict_handle, KEY); + let root = match dictionary_entry(document, root_holder, KEY) { + Some(root) if matches!(document.objects.get(&root), Some(ObjectType::Associative(_))) => { + root + } + _ => { + let root = document.allocate_handle(); + let network = codec::objects::AssocNetwork { + action: AssocAction { + class_version: 2, + ..Default::default() + }, + ..Default::default() + }; + let mut object = associative( + root, + root_holder, + "ACDBASSOCNETWORK", + "AcDbAssocNetwork", + AssociativeData::Network(network), + ); + if let ObjectType::Associative(object) = &mut object { + object.reactors.push(root_holder); + } + document.objects.insert(root, object); + if let Some(ObjectType::Dictionary(holder)) = document.objects.get_mut(&root_holder) { + holder.add_entry(KEY, root); + } + root + } + }; + let network = document.allocate_handle(); + let mut object = associative( + network, + holder, + "ACDBASSOCNETWORK", + "AcDbAssocNetwork", + AssociativeData::Network(codec::objects::AssocNetwork { + action: AssocAction { + class_version: 2, + owning_network: root, + ..Default::default() + }, + ..Default::default() + }), + ); + if let ObjectType::Associative(object) = &mut object { + object.reactors.push(holder); + } + document.objects.insert(network, object); + if let Some(ObjectType::Dictionary(dictionary)) = document.objects.get_mut(&holder) { + dictionary.add_entry(KEY, network); + } + if let Some(ObjectType::Associative(object)) = document.objects.get_mut(&root) { + if let AssociativeData::Network(root) = &mut object.data { + root.network_action_index += 1; + root.actions.push(codec::objects::AssocActionDependency { + is_owned: false, + dependency: network, + }); + } + } + network +} + +fn value_param(name: &str, unit_type: i32, value: f64, whole: bool) -> AssocValueParam { + let variant = if whole { + codec::objects::AssocEvalVariant { + code: 90, + value: AssocEvalValue::Long(value.round() as i32), + } + } else { + codec::objects::AssocEvalVariant { + code: 40, + value: AssocEvalValue::Real(value), + } + }; + AssocValueParam { + name: name.to_string(), + unit_type, + variables: vec![codec::objects::AssocValueParamVariable { + value: variant, + handle: Handle::NULL, + }], + ..Default::default() + } +} + +/// The value parameters of a new rectangular array, in the order the +/// reference product writes them. +pub fn rectangular_params( + columns: u32, + rows: u32, + column_spacing: f64, + row_spacing: f64, +) -> Vec { + vec![ + value_param("AlignItems", 0, 0.0, true), + value_param("AxesAngle", 2, std::f64::consts::FRAC_PI_2, false), + value_param("ItemSpacing", 1, column_spacing, false), + value_param("Items", 0, f64::from(columns), true), + value_param("LevelSpacing", 1, 1.0, false), + value_param("Levels", 0, 1.0, true), + value_param("RowElevation", 1, 0.0, false), + value_param("RowSpacing", 1, row_spacing, false), + value_param("Rows", 0, f64::from(rows), true), + ] +} + +/// The value parameters of a new polar array. +pub fn polar_params( + items: u32, + fill_angle: f64, + radius: f64, + start_angle: f64, +) -> Vec { + let count = f64::from(items.max(1)); + let full = (fill_angle.abs() - std::f64::consts::TAU).abs() < 1e-9; + let spacing = if full { + fill_angle / count + } else if items > 1 { + fill_angle / (count - 1.0) + } else { + 0.0 + }; + vec![ + value_param("FillAngle", 2, fill_angle, false), + value_param("ItemSpacing", 2, spacing, false), + value_param("Items", 0, count, true), + value_param("LevelSpacing", 1, 1.0, false), + value_param("Levels", 0, 1.0, true), + value_param("RowElevation", 1, 0.0, false), + value_param("RowSpacing", 1, radius.abs().max(1.0), false), + value_param("Rows", 0, 1.0, true), + value_param("Radius", 1, radius, false), + value_param("StartAngle", 2, start_angle, false), + ] +} + +/// What ARRAYRECT / ARRAYPOLAR ask for an associative array. +#[derive(Clone, Debug, PartialEq)] +pub enum AssocArraySpec { + Rectangular { + columns: u32, + rows: u32, + column_spacing: f64, + row_spacing: f64, + }, + Polar { + center: DVec3, + items: u32, + fill_angle: f64, + }, +} + +/// Turn `sources` into an associative array: they move into an anonymous +/// source block whose origin is `source_point`, an anonymous array block +/// holds one reference to it per item, and an INSERT of the array block at +/// `base` takes their place, tied together by an array action in the +/// model-space network. A polar array turns about `base`. Returns the +/// array INSERT. +pub fn create( + scene: &mut crate::scene::Scene, + sources: &[Handle], + source_point: DVec3, + base: DVec3, + kind: ArrayKind, + params: Vec, +) -> Result { + use codec::objects::{ + AssocActionBody, AssocActionDependency, AssocActionParam, AssocArrayItem, + AssocSingleDependencyActionParam, AssocVertexActionParam, + }; + use codec::EntityType; + let normal = DVec3::Z; + let placements: Vec<([i32; 3], DMat4)> = match kind { + ArrayKind::Rectangular => rectangular_items(¶ms, DVec3::X, normal) + .into_iter() + .map(|(location, offset)| (location, DMat4::from_translation(offset))) + .collect(), + ArrayKind::Polar => polar_items(¶ms, DVec3::ZERO, normal), + ArrayKind::Path => return Err("path arrays cannot be created yet".into()), + }; + let mut entities: Vec = sources + .iter() + .filter_map(|handle| scene.document.get_entity(*handle).cloned()) + .collect(); + if entities.is_empty() { + return Err("nothing to array".into()); + } + let layer = scene.document.header.current_layer_name.clone(); + for entity in &mut entities { + crate::scene::view::dispatch::apply_transform( + entity, + &crate::command::EntityTransform::Translate(-source_point), + ); + } + register_array_classes(&mut scene.document); + + let source_name = next_anonymous_name(&scene.document); + scene.define_block_raw(&source_name, codec::types::Vector3::ZERO, entities); + let item = |offset: DMat4| { + let position = offset.w_axis.truncate(); + let mut insert = codec::entities::Insert::new( + source_name.clone(), + codec::types::Vector3::new(position.x, position.y, position.z), + ); + insert.rotation = offset.x_axis.y.atan2(offset.x_axis.x); + insert.common.layer = "0".to_string(); + insert.common.color = codec::types::Color::ByBlock; + insert.common.linetype = "ByBlock".to_string(); + insert.common.line_weight = codec::types::LineWeight::ByBlock; + insert.common.transparency = codec::types::Transparency::ByBlock; + EntityType::Insert(insert) + }; + let array_name = next_anonymous_name(&scene.document); + scene.define_block_raw( + &array_name, + codec::types::Vector3::ZERO, + placements.iter().map(|(_, placement)| item(*placement)).collect(), + ); + let record = |document: &CadDocument, name: &str| { + document + .block_records + .get(name) + .map(|record| (record.handle, record.entity_handles.clone())) + }; + let (source_block, _) = record(&scene.document, &source_name).ok_or("source block")?; + let (array_block, item_handles) = record(&scene.document, &array_name).ok_or("array block")?; + for name in [&source_name, &array_name] { + if let Some(record) = scene.document.block_records.get_mut(name) { + record.flags.anonymous = true; + record.explodable = true; + record.scale_uniformly = *name == array_name; + } + } + if let Some(record) = scene.document.block_records.get_mut(&source_name) { + record.insert_handles = item_handles.clone(); + } + + let mut array_insert = codec::entities::Insert::new( + array_name.clone(), + codec::types::Vector3::new(base.x, base.y, base.z), + ); + array_insert.common.layer = layer; + let insert = scene.add_entity(EntityType::Insert(array_insert)); + scene.erase_entities(sources); + + let document = &mut scene.document; + let network = model_space_network(document); + let [action, body, write_dep, read_dep, both_dep] = + std::array::from_fn(|_| document.allocate_handle()); + let mut owned_parameters = Vec::new(); + let vertex_params = + [("BaseNormal", normal), ("Origin", DVec3::ZERO), ("basePoint", DVec3::ZERO)]; + for (name, point) in vertex_params { + let handle = document.allocate_handle(); + document.objects.insert( + handle, + associative( + handle, + action, + "ACDBASSOCVERTEXACTIONPARAM", + "AcDbAssocVertexActionParam", + AssociativeData::VertexActionParam(AssocVertexActionParam { + single_dependency: AssocSingleDependencyActionParam { + action_param: AssocActionParam { + is_r2013: 1, + version: 0, + name: name.to_string(), + }, + ..Default::default() + }, + point: codec::types::Vector3::new(point.x, point.y, point.z), + }), + ), + ); + owned_parameters.push(handle); + } + for (handle, read, write, order, target, id) in [ + (write_dep, false, true, 0, array_block, 1), + (read_dep, true, false, 0, source_block, 2), + (both_dep, true, true, i32::MIN, insert, 3), + ] { + document.objects.insert( + handle, + associative( + handle, + action, + "ACDBASSOCDEPENDENCY", + "AcDbAssocDependency", + AssociativeData::Dependency(AssocDependency { + class_version: 2, + is_read_dependency: read, + is_write_dependency: write, + is_attached_to_object: true, + is_delegating_to_owning_action: true, + order, + dependent_on: target, + dependency_body_id: id, + ..Default::default() + }), + ), + ); + } + if let Some(EntityType::Insert(array)) = document.get_entity_mut(insert) { + array.common.reactors.push(both_dep); + } + let items = placements + .iter() + .zip(&item_handles) + .map(|((location, placement), handle)| { + let position = placement.w_axis.truncate(); + let turned = (placement.x_axis.truncate() - DVec3::X).length() > 1e-12; + AssocArrayItem { + location: *location, + flags: if turned { 12 } else { 8 }, + uses_default_transform: !turned, + x_direction: if turned { + codec::types::Vector3::ZERO + } else { + codec::types::Vector3::new(position.x, position.y, position.z) + }, + transform: if turned { + placement.transpose().to_cols_array() + } else { + [0.0; 16] + }, + first_handle: Some(*handle), + ..Default::default() + } + }) + .collect(); + let parameter_block = match kind { + ArrayKind::Polar => "AcDbAssocArrayPolarParameters", + _ => "AcDbAssocArrayRectangularParameters", + }; + document.objects.insert( + body, + associative( + body, + action, + "ACDBASSOCARRAYACTIONBODY", + "AcDbAssocArrayActionBody", + AssociativeData::ArrayActionBody(AssocArrayActionBody { + action_body: AssocActionBody { version: 2 }, + version: 1, + parameter_block: parameter_block.to_string(), + item_class: "AcDbAssocArrayItem".to_string(), + items, + transform: DMat4::from_translation(base).transpose().to_cols_array(), + ..Default::default() + }), + ), + ); + let action_index = match document.objects.get_mut(&network) { + Some(ObjectType::Associative(object)) => match &mut object.data { + AssociativeData::Network(network) => { + network.network_action_index += 1; + network.actions.push(AssocActionDependency { + is_owned: true, + dependency: action, + }); + network.network_action_index + } + _ => 1, + }, + _ => 1, + }; + document.objects.insert( + action, + associative( + action, + network, + "ACDBASSOCACTION", + "AcDbAssocAction", + AssociativeData::Action(AssocAction { + class_version: 2, + owning_network: network, + action_body: body, + action_index, + max_dependency_index: 3, + dependencies: [write_dep, read_dep, both_dep] + .map(|dependency| AssocActionDependency { + is_owned: true, + dependency, + }) + .to_vec(), + owned_parameters, + values: params, + ..Default::default() + }), + ), + ); + scene.bump_geometry(); + Ok(insert) +} + +/// Build an associative array from `sources` per `spec`, its source block +/// based at their lower-left corner `source_point`. +pub fn create_from_spec( + scene: &mut crate::scene::Scene, + sources: &[Handle], + source_point: DVec3, + spec: &AssocArraySpec, +) -> Result { + match *spec { + AssocArraySpec::Rectangular { + columns, + rows, + column_spacing, + row_spacing, + } => create( + scene, + sources, + source_point, + source_point, + ArrayKind::Rectangular, + rectangular_params(columns, rows, column_spacing, row_spacing), + ), + AssocArraySpec::Polar { + center, + items, + fill_angle, + } => { + let offset = source_point - center; + let params = polar_params( + items, + fill_angle, + offset.truncate().length(), + offset.y.atan2(offset.x), + ); + create(scene, sources, source_point, center, ArrayKind::Polar, params) + } + } +} diff --git a/src/scene/dwg_native_constraints.rs b/src/scene/dwg_native_constraints.rs index 22efdc24f..80ffa9a55 100644 --- a/src/scene/dwg_native_constraints.rs +++ b/src/scene/dwg_native_constraints.rs @@ -2489,9 +2489,15 @@ fn detach_preserved_scope( Some(ObjectType::Associative(object)) => match &object.data { AssociativeData::ConstraintGroup(_) | AssociativeData::Network(_) => false, AssociativeData::Variable(variable) => variable.name.trim().is_empty(), - // A surface action whose surface was erased goes with it. - AssociativeData::Action(_) => action_surface(document, *handle) - .is_none_or(|surface| document.get_entity(surface).is_some()), + // A surface action whose surface was erased goes with it, + // and an array action whose array reference was. + AssociativeData::Action(_) => { + action_surface(document, *handle) + .is_none_or(|surface| document.get_entity(surface).is_some()) + && !crate::scene::assoc_array::orphaned_array_action( + document, *handle, + ) + } _ => true, }, _ => false,