forked from 1b-agent/opencadstudio-upstream
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:
1 parent
2bf77e4bd6
commit
f0ac89629b
30 files changed
+705
-3
No files matched your search
@@ -295,6 +295,7 @@ command-copy =
|
||||
.mode = COPY أدخل خيار وضع النسخ [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.label-associative-array-created = __ocs_fmt_0__: تم إنشاء مصفوفة ترابطية.
|
||||
.assoc-array-array-path = مصفوفة (مسار)
|
||||
.assoc-array-array-polar = مصفوفة (قطبية)
|
||||
.assoc-array-array-rectangular = مصفوفة (مستطيلة)
|
||||
|
||||
@@ -282,6 +282,7 @@ command-copy =
|
||||
.displacement = COPY Задайте преместване:
|
||||
.mode = COPY Въведете режим на копиране [Single/Multiple] <Multiple>:
|
||||
common =
|
||||
.label-associative-array-created = __ocs_fmt_0__: създаден е асоциативен масив.
|
||||
.assoc-array-array-path = Масив (по път)
|
||||
.assoc-array-array-polar = Масив (полярен)
|
||||
.assoc-array-array-rectangular = Масив (правоъгълен)
|
||||
|
||||
@@ -282,6 +282,7 @@ command-copy =
|
||||
.mode = COPY Zadejte režim kopírování [Single/Multiple] <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é)
|
||||
|
||||
@@ -283,6 +283,7 @@ command-copy =
|
||||
.mode = COPY Kopiermodus eingeben [Single/Multiple] <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)
|
||||
|
||||
@@ -282,6 +282,7 @@ command-copy =
|
||||
.mode = COPY Επιλέξτε λειτουργία αντιγραφής [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.label-associative-array-created = __ocs_fmt_0__: δημιουργήθηκε συσχετιστικός πίνακας.
|
||||
.assoc-array-array-path = Πίνακας (διαδρομή)
|
||||
.assoc-array-array-polar = Πίνακας (πολικός)
|
||||
.assoc-array-array-rectangular = Πίνακας (ορθογώνιος)
|
||||
|
||||
@@ -282,6 +282,7 @@ command-copy =
|
||||
.mode = COPY Enter a copy mode option [Single/Multiple] <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)
|
||||
|
||||
@@ -283,6 +283,7 @@ command-copy =
|
||||
.mode = COPY Indique modo de copia [Single/Multiple] <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)
|
||||
|
||||
@@ -282,6 +282,7 @@ command-copy =
|
||||
.mode = COPY Valitse kopiointitila [Single/Multiple] <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)
|
||||
|
||||
@@ -283,6 +283,7 @@ command-copy =
|
||||
.mode = COPY Entrez le mode de copie [Single/Multiple] <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)
|
||||
|
||||
@@ -271,6 +271,7 @@ command-copy =
|
||||
.mode = COPY कॉपी मोड विकल्प दर्ज करें [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.label-associative-array-created = __ocs_fmt_0__: सहयोगी ऐरे बनाया गया।
|
||||
.assoc-array-array-path = ऐरे (पथ)
|
||||
.assoc-array-array-polar = ऐरे (ध्रुवीय)
|
||||
.assoc-array-array-rectangular = ऐरे (आयताकार)
|
||||
|
||||
@@ -281,6 +281,7 @@ command-copy =
|
||||
.mode = COPY Adja meg a másolási módot [Single/Multiple] <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)
|
||||
|
||||
@@ -282,6 +282,7 @@ command-copy =
|
||||
.mode = COPY Specificare modalità di copia [Single/Multiple] <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)
|
||||
|
||||
@@ -273,6 +273,7 @@ command-copy =
|
||||
.mode = COPY コピーモードを入力 [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.label-associative-array-created = __ocs_fmt_0__: 自動調整配列複写を作成しました。
|
||||
.assoc-array-array-path = 配列複写 (パス)
|
||||
.assoc-array-array-polar = 配列複写 (円形状)
|
||||
.assoc-array-array-rectangular = 配列複写 (矩形状)
|
||||
|
||||
@@ -282,6 +282,7 @@ command-copy =
|
||||
.mode = COPY 복사 모드 옵션 입력 [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.label-associative-array-created = __ocs_fmt_0__: 연관 배열을 만들었습니다.
|
||||
.assoc-array-array-path = 배열 (경로)
|
||||
.assoc-array-array-polar = 배열 (원형)
|
||||
.assoc-array-array-rectangular = 배열 (직사각형)
|
||||
|
||||
@@ -282,6 +282,7 @@ command-copy =
|
||||
.mode = COPY ປ້ອນຕົວເລືອກໂໝດການຄັດລອກ [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.label-associative-array-created = __ocs_fmt_0__: ສ້າງອາເຣແບບເຊື່ອມໂຍງແລ້ວ.
|
||||
.assoc-array-array-path = ອາເຣ (ເສັ້ນທາງ)
|
||||
.assoc-array-array-polar = ອາເຣ (ວົງມົນ)
|
||||
.assoc-array-array-rectangular = ອາເຣ (ສີ່ຫຼ່ຽມ)
|
||||
|
||||
@@ -283,6 +283,7 @@ command-copy =
|
||||
.mode = COPY Geef kopieermodus op [Single/Multiple] <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)
|
||||
|
||||
@@ -282,6 +282,7 @@ command-copy =
|
||||
.mode = COPY Podaj tryb kopiowania [Single/Multiple] <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)
|
||||
|
||||
@@ -283,6 +283,7 @@ command-copy =
|
||||
.mode = COPY Informe o modo de cópia [Single/Multiple] <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)
|
||||
|
||||
@@ -286,6 +286,7 @@ command-copy =
|
||||
.mode = COPY Задайте режим копирования [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.label-associative-array-created = __ocs_fmt_0__: создан ассоциативный массив.
|
||||
.assoc-array-array-path = Массив (по траектории)
|
||||
.assoc-array-array-polar = Массив (круговой)
|
||||
.assoc-array-array-rectangular = Массив (прямоугольный)
|
||||
|
||||
@@ -268,6 +268,7 @@ command-copy =
|
||||
.mode = COPY Kopyalama modu seçin [Single/Multiple] <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)
|
||||
|
||||
@@ -270,6 +270,7 @@ command-copy =
|
||||
.mode = COPY 输入复制模式选项 [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.label-associative-array-created = __ocs_fmt_0__: 已创建关联阵列。
|
||||
.assoc-array-array-path = 阵列 (路径)
|
||||
.assoc-array-array-polar = 阵列 (环形)
|
||||
.assoc-array-array-rectangular = 阵列 (矩形)
|
||||
|
||||
@@ -282,6 +282,7 @@ command-copy =
|
||||
.mode = COPY 輸入複製模式選項 [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.label-associative-array-created = __ocs_fmt_0__: 已建立關聯陣列。
|
||||
.assoc-array-array-path = 陣列 (路徑)
|
||||
.assoc-array-array-polar = 陣列 (環形)
|
||||
.assoc-array-array-rectangular = 陣列 (矩形)
|
||||
|
||||
@@ -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<Handle>,
|
||||
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<codec::EntityType>)>,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1621,6 +1621,11 @@ pub enum CmdResult {
|
||||
},
|
||||
/// Copy selected entities with multiple transforms (e.g. rectangular array); end command.
|
||||
BatchCopy(Vec<Handle>, Vec<EntityTransform>),
|
||||
/// Turn the objects into an associative array; end command.
|
||||
AssocArray {
|
||||
handles: Vec<Handle>,
|
||||
spec: crate::scene::assoc_array::AssocArraySpec,
|
||||
},
|
||||
/// Erase `handle` and replace with new entities; command stays active.
|
||||
ReplaceEntity(Handle, Vec<EntityType>),
|
||||
/// XCLIP on block references; ends the command.
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -108,6 +108,24 @@ pub fn array_of_insert(document: &CadDocument, insert: Handle) -> Option<AssocAr
|
||||
})
|
||||
}
|
||||
|
||||
/// An array action whose array INSERT is gone (erased or exploded): it has
|
||||
/// nothing left to drive and is dropped when the networks are written.
|
||||
pub fn orphaned_array_action(document: &CadDocument, handle: Handle) -> 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::<u64>().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<Handle> {
|
||||
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<AssocValueParam> {
|
||||
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<AssocValueParam> {
|
||||
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<AssocValueParam>,
|
||||
) -> Result<Handle, String> {
|
||||
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<EntityType> = 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<Handle, String> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user