feat(array): edit associative arrays through their standard records

An associative array is an AcDbAssocAction with an
AcDbAssocArrayActionBody, dependencies on the array INSERT, the block
holding the item references and the block holding the source objects,
and value parameters (Items, ItemSpacing, Rows, …). Selecting the array
INSERT now shows its counts and spacings in Properties; changing one
places the items again and rewrites the item references, the body's
item records and the parameter values together, in one undo step, so
the drawing stays associative when saved.

Rectangular arrays follow their RowProfile axis, AxesAngle and base
normal; polar arrays turn about the centre recovered from their first
item, Radius and StartAngle. Both match the stored items of the
associative arrays in the sample drawings (122 of 123 rectangular, all
polar items of the polar samples). Path arrays are shown read-only.

Refs #1109

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-13 20:32:51 +03:00
1 parent b371a0a59e
commit 2bf77e4bd6
27 files changed
+831

No files matched your search

+10
View File
@@ -295,6 +295,16 @@ command-copy =
.mode = COPY أدخل خيار وضع النسخ [Single/Multiple] <Multiple>:
common =
.assoc-array-array-path = مصفوفة (مسار)
.assoc-array-array-polar = مصفوفة (قطبية)
.assoc-array-array-rectangular = مصفوفة (مستطيلة)
.assoc-array-item-spacing = تباعد العناصر
.assoc-array-fill-angle = زاوية التعبئة
.assoc-array-items = العناصر
.assoc-array-level-spacing = تباعد المستويات
.assoc-array-levels = المستويات
.assoc-array-row-spacing = تباعد الصفوف
.assoc-array-column-spacing = تباعد الأعمدة
.fbxexport-exported-to = FBXEXPORT: تم التصدير إلى "__ocs_fmt_0__"
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: لا توجد بيانات شبكة ثلاثية الأبعاد في هذا الرسم.
.fbxexport-error = FBXEXPORT: ‏__ocs_fmt_0__
+10
View File
@@ -282,6 +282,16 @@ command-copy =
.displacement = COPY Задайте преместване:
.mode = COPY Въведете режим на копиране [Single/Multiple] <Multiple>:
common =
.assoc-array-array-path = Масив (по път)
.assoc-array-array-polar = Масив (полярен)
.assoc-array-array-rectangular = Масив (правоъгълен)
.assoc-array-item-spacing = Разстояние между елементите
.assoc-array-fill-angle = Ъгъл на запълване
.assoc-array-items = Елементи
.assoc-array-level-spacing = Разстояние между нивата
.assoc-array-levels = Нива
.assoc-array-row-spacing = Разстояние между редовете
.assoc-array-column-spacing = Разстояние между колоните
.fbxexport-exported-to = FBXEXPORT: експортирано в "__ocs_fmt_0__"
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: няма 3D данни за мрежа в този чертеж.
.fbxexport-error = FBXEXPORT: __ocs_fmt_0__
+10
View File
@@ -282,6 +282,16 @@ command-copy =
.mode = COPY Zadejte režim kopírování [Single/Multiple] <Multiple>:
common =
.assoc-array-array-path = Pole (podél cesty)
.assoc-array-array-polar = Pole (kruhové)
.assoc-array-array-rectangular = Pole (obdélníkové)
.assoc-array-item-spacing = Rozteč položek
.assoc-array-fill-angle = Úhel vyplnění
.assoc-array-items = Položky
.assoc-array-level-spacing = Rozteč úrovní
.assoc-array-levels = Úrovně
.assoc-array-row-spacing = Rozteč řádků
.assoc-array-column-spacing = Rozteč sloupců
.fbxexport-exported-to = FBXEXPORT: exportováno do "__ocs_fmt_0__"
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: v tomto výkresu nejsou žádná data 3D sítě.
.fbxexport-error = FBXEXPORT: __ocs_fmt_0__
+10
View File
@@ -283,6 +283,16 @@ command-copy =
.mode = COPY Kopiermodus eingeben [Single/Multiple] <Multiple>:
common =
.assoc-array-array-path = Reihe (Pfad)
.assoc-array-array-polar = Reihe (polar)
.assoc-array-array-rectangular = Reihe (rechteckig)
.assoc-array-item-spacing = Elementabstand
.assoc-array-fill-angle = Füllwinkel
.assoc-array-items = Elemente
.assoc-array-level-spacing = Ebenenabstand
.assoc-array-levels = Ebenen
.assoc-array-row-spacing = Zeilenabstand
.assoc-array-column-spacing = Spaltenabstand
.fbxexport-exported-to = FBXEXPORT: exportiert nach "__ocs_fmt_0__"
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: keine 3D-Mesh-Daten in dieser Zeichnung.
.fbxexport-error = FBXEXPORT: __ocs_fmt_0__
+10
View File
@@ -282,6 +282,16 @@ command-copy =
.mode = COPY Επιλέξτε λειτουργία αντιγραφής [Single/Multiple] <Multiple>:
common =
.assoc-array-array-path = Πίνακας (διαδρομή)
.assoc-array-array-polar = Πίνακας (πολικός)
.assoc-array-array-rectangular = Πίνακας (ορθογώνιος)
.assoc-array-item-spacing = Απόσταση στοιχείων
.assoc-array-fill-angle = Γωνία πλήρωσης
.assoc-array-items = Στοιχεία
.assoc-array-level-spacing = Απόσταση επιπέδων
.assoc-array-levels = Επίπεδα
.assoc-array-row-spacing = Απόσταση γραμμών
.assoc-array-column-spacing = Απόσταση στηλών
.fbxexport-exported-to = FBXEXPORT: εξήχθη στο "__ocs_fmt_0__"
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: δεν υπάρχουν δεδομένα πλέγματος 3D σε αυτό το σχέδιο.
.fbxexport-error = FBXEXPORT: __ocs_fmt_0__
+10
View File
@@ -282,6 +282,16 @@ command-copy =
.mode = COPY Enter a copy mode option [Single/Multiple] <Multiple>:
common =
.assoc-array-array-path = Array (Path)
.assoc-array-array-polar = Array (Polar)
.assoc-array-array-rectangular = Array (Rectangular)
.assoc-array-item-spacing = Item spacing
.assoc-array-fill-angle = Fill angle
.assoc-array-items = Items
.assoc-array-level-spacing = Level spacing
.assoc-array-levels = Levels
.assoc-array-row-spacing = Row spacing
.assoc-array-column-spacing = Column spacing
.fbxexport-exported-to = FBXEXPORT: exported to "__ocs_fmt_0__"
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: no 3D mesh data in this drawing.
.fbxexport-error = FBXEXPORT: __ocs_fmt_0__
+10
View File
@@ -283,6 +283,16 @@ command-copy =
.mode = COPY Indique modo de copia [Single/Multiple] <Multiple>:
common =
.assoc-array-array-path = Matriz (trayectoria)
.assoc-array-array-polar = Matriz (polar)
.assoc-array-array-rectangular = Matriz (rectangular)
.assoc-array-item-spacing = Espaciado de elementos
.assoc-array-fill-angle = Ángulo de relleno
.assoc-array-items = Elementos
.assoc-array-level-spacing = Espaciado de niveles
.assoc-array-levels = Niveles
.assoc-array-row-spacing = Espaciado de filas
.assoc-array-column-spacing = Espaciado de columnas
.fbxexport-exported-to = FBXEXPORT: exportado a «__ocs_fmt_0__»
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: no hay datos de malla 3D en este dibujo.
.fbxexport-error = FBXEXPORT: __ocs_fmt_0__
+10
View File
@@ -282,6 +282,16 @@ command-copy =
.mode = COPY Valitse kopiointitila [Single/Multiple] <Multiple>:
common =
.assoc-array-array-path = Taulukko (polku)
.assoc-array-array-polar = Taulukko (polaarinen)
.assoc-array-array-rectangular = Taulukko (suorakulmainen)
.assoc-array-item-spacing = Kohdeväli
.assoc-array-fill-angle = Täyttökulma
.assoc-array-items = Kohteet
.assoc-array-level-spacing = Tasoväli
.assoc-array-levels = Tasot
.assoc-array-row-spacing = Riviväli
.assoc-array-column-spacing = Sarakeväli
.fbxexport-exported-to = FBXEXPORT: viety tiedostoon "__ocs_fmt_0__"
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: tässä piirustuksessa ei ole 3D-verkkotietoja.
.fbxexport-error = FBXEXPORT: __ocs_fmt_0__
+10
View File
@@ -283,6 +283,16 @@ command-copy =
.mode = COPY Entrez le mode de copie [Single/Multiple] <Multiple> :
common =
.assoc-array-array-path = Réseau (trajectoire)
.assoc-array-array-polar = Réseau (polaire)
.assoc-array-array-rectangular = Réseau (rectangulaire)
.assoc-array-item-spacing = Espacement des éléments
.assoc-array-fill-angle = Angle de remplissage
.assoc-array-items = Éléments
.assoc-array-level-spacing = Espacement des niveaux
.assoc-array-levels = Niveaux
.assoc-array-row-spacing = Espacement des rangées
.assoc-array-column-spacing = Espacement des colonnes
.fbxexport-exported-to = FBXEXPORT: exporté vers "__ocs_fmt_0__"
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: aucune donnée de maillage 3D dans ce dessin.
.fbxexport-error = FBXEXPORT: __ocs_fmt_0__
+10
View File
@@ -271,6 +271,16 @@ command-copy =
.mode = COPY कॉपी मोड विकल्प दर्ज करें [Single/Multiple] <Multiple>:
common =
.assoc-array-array-path = ऐरे (पथ)
.assoc-array-array-polar = ऐरे (ध्रुवीय)
.assoc-array-array-rectangular = ऐरे (आयताकार)
.assoc-array-item-spacing = आइटम अंतराल
.assoc-array-fill-angle = भरण कोण
.assoc-array-items = आइटम
.assoc-array-level-spacing = स्तर अंतराल
.assoc-array-levels = स्तर
.assoc-array-row-spacing = पंक्ति अंतराल
.assoc-array-column-spacing = कॉलम अंतराल
.fbxexport-exported-to = FBXEXPORT: "__ocs_fmt_0__" में निर्यात किया गया
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: इस ड्रॉइंग में कोई 3D मेश डेटा नहीं है।
.fbxexport-error = FBXEXPORT: __ocs_fmt_0__
+10
View File
@@ -281,6 +281,16 @@ command-copy =
.mode = COPY Adja meg a másolási módot [Single/Multiple] <Multiple>:
common =
.assoc-array-array-path = Tömb (útvonal)
.assoc-array-array-polar = Tömb (poláris)
.assoc-array-array-rectangular = Tömb (téglalap)
.assoc-array-item-spacing = Elemtávolság
.assoc-array-fill-angle = Kitöltési szög
.assoc-array-items = Elemek
.assoc-array-level-spacing = Szinttávolság
.assoc-array-levels = Szintek
.assoc-array-row-spacing = Sortávolság
.assoc-array-column-spacing = Oszloptávolság
.fbxexport-exported-to = FBXEXPORT: exportálva a következőbe: "__ocs_fmt_0__"
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: a rajz nem tartalmaz 3D hálóadatot.
.fbxexport-error = FBXEXPORT: __ocs_fmt_0__
+10
View File
@@ -282,6 +282,16 @@ command-copy =
.mode = COPY Specificare modalità di copia [Single/Multiple] <Multiple>:
common =
.assoc-array-array-path = Serie (percorso)
.assoc-array-array-polar = Serie (polare)
.assoc-array-array-rectangular = Serie (rettangolare)
.assoc-array-item-spacing = Spaziatura elementi
.assoc-array-fill-angle = Angolo di riempimento
.assoc-array-items = Elementi
.assoc-array-level-spacing = Spaziatura livelli
.assoc-array-levels = Livelli
.assoc-array-row-spacing = Spaziatura righe
.assoc-array-column-spacing = Spaziatura colonne
.fbxexport-exported-to = FBXEXPORT: esportato in "__ocs_fmt_0__"
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: nessun dato mesh 3D in questo disegno.
.fbxexport-error = FBXEXPORT: __ocs_fmt_0__
+10
View File
@@ -273,6 +273,16 @@ command-copy =
.mode = COPY コピーモードを入力 [Single/Multiple] <Multiple>:
common =
.assoc-array-array-path = 配列複写 (パス)
.assoc-array-array-polar = 配列複写 (円形状)
.assoc-array-array-rectangular = 配列複写 (矩形状)
.assoc-array-item-spacing = 項目の間隔
.assoc-array-fill-angle = 充填角度
.assoc-array-items = 項目
.assoc-array-level-spacing = レベルの間隔
.assoc-array-levels = レベル
.assoc-array-row-spacing = 行の間隔
.assoc-array-column-spacing = 列の間隔
.fbxexport-exported-to = FBXEXPORT: "__ocs_fmt_0__" に書き出しました
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: この図面に 3D メッシュデータがありません。
.fbxexport-error = FBXEXPORT: __ocs_fmt_0__
+10
View File
@@ -282,6 +282,16 @@ command-copy =
.mode = COPY 복사 모드 옵션 입력 [Single/Multiple] <Multiple>:
common =
.assoc-array-array-path = 배열 (경로)
.assoc-array-array-polar = 배열 (원형)
.assoc-array-array-rectangular = 배열 (직사각형)
.assoc-array-item-spacing = 항목 간격
.assoc-array-fill-angle = 채우기 각도
.assoc-array-items = 항목
.assoc-array-level-spacing = 레벨 간격
.assoc-array-levels = 레벨
.assoc-array-row-spacing = 행 간격
.assoc-array-column-spacing = 열 간격
.fbxexport-exported-to = FBXEXPORT: "__ocs_fmt_0__"로 내보냈습니다.
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: 이 도면에는 3D 메쉬 데이터가 없습니다.
.fbxexport-error = FBXEXPORT: __ocs_fmt_0__
+10
View File
@@ -282,6 +282,16 @@ command-copy =
.mode = COPY ປ້ອນຕົວເລືອກໂໝດການຄັດລອກ [Single/Multiple] <Multiple>:
common =
.assoc-array-array-path = ອາເຣ (ເສັ້ນທາງ)
.assoc-array-array-polar = ອາເຣ (ວົງມົນ)
.assoc-array-array-rectangular = ອາເຣ (ສີ່ຫຼ່ຽມ)
.assoc-array-item-spacing = ໄລຍະຫ່າງລາຍການ
.assoc-array-fill-angle = ມຸມເຕີມ
.assoc-array-items = ລາຍການ
.assoc-array-level-spacing = ໄລຍະຫ່າງລະດັບ
.assoc-array-levels = ລະດັບ
.assoc-array-row-spacing = ໄລຍະຫ່າງແຖວ
.assoc-array-column-spacing = ໄລຍະຫ່າງຖັນ
.fbxexport-exported-to = FBXEXPORT: ສົ່ງອອກໄປທີ່ "__ocs_fmt_0__" ແລ້ວ
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: ບໍ່ມີຂໍ້ມູນເມຊ 3D ໃນຮູບແຕ້ມນີ້.
.fbxexport-error = FBXEXPORT: __ocs_fmt_0__
+10
View File
@@ -283,6 +283,16 @@ command-copy =
.mode = COPY Geef kopieermodus op [Single/Multiple] <Multiple>:
common =
.assoc-array-array-path = Reeks (pad)
.assoc-array-array-polar = Reeks (polair)
.assoc-array-array-rectangular = Reeks (rechthoekig)
.assoc-array-item-spacing = Itemafstand
.assoc-array-fill-angle = Vulhoek
.assoc-array-items = Items
.assoc-array-level-spacing = Niveau-afstand
.assoc-array-levels = Niveaus
.assoc-array-row-spacing = Rijafstand
.assoc-array-column-spacing = Kolomafstand
.fbxexport-exported-to = FBXEXPORT: geëxporteerd naar "__ocs_fmt_0__"
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: geen 3D-maasgegevens in deze tekening.
.fbxexport-error = FBXEXPORT: __ocs_fmt_0__
+10
View File
@@ -282,6 +282,16 @@ command-copy =
.mode = COPY Podaj tryb kopiowania [Single/Multiple] <Multiple>:
common =
.assoc-array-array-path = Szyk (ścieżka)
.assoc-array-array-polar = Szyk (biegunowy)
.assoc-array-array-rectangular = Szyk (prostokątny)
.assoc-array-item-spacing = Odstęp elementów
.assoc-array-fill-angle = Kąt wypełnienia
.assoc-array-items = Elementy
.assoc-array-level-spacing = Odstęp poziomów
.assoc-array-levels = Poziomy
.assoc-array-row-spacing = Odstęp wierszy
.assoc-array-column-spacing = Odstęp kolumn
.fbxexport-exported-to = FBXEXPORT: wyeksportowano do „__ocs_fmt_0__”
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: brak danych siatki 3D na tym rysunku.
.fbxexport-error = FBXEXPORT: __ocs_fmt_0__
+10
View File
@@ -283,6 +283,16 @@ command-copy =
.mode = COPY Informe o modo de cópia [Single/Multiple] <Multiple>:
common =
.assoc-array-array-path = Matriz (caminho)
.assoc-array-array-polar = Matriz (polar)
.assoc-array-array-rectangular = Matriz (retangular)
.assoc-array-item-spacing = Espaçamento dos itens
.assoc-array-fill-angle = Ângulo de preenchimento
.assoc-array-items = Itens
.assoc-array-level-spacing = Espaçamento dos níveis
.assoc-array-levels = Níveis
.assoc-array-row-spacing = Espaçamento das linhas
.assoc-array-column-spacing = Espaçamento das colunas
.fbxexport-exported-to = FBXEXPORT: exportado para “__ocs_fmt_0__”
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: não há dados de malha 3D neste desenho.
.fbxexport-error = FBXEXPORT: __ocs_fmt_0__
+10
View File
@@ -286,6 +286,16 @@ command-copy =
.mode = COPY Задайте режим копирования [Single/Multiple] <Multiple>:
common =
.assoc-array-array-path = Массив (по траектории)
.assoc-array-array-polar = Массив (круговой)
.assoc-array-array-rectangular = Массив (прямоугольный)
.assoc-array-item-spacing = Расстояние между элементами
.assoc-array-fill-angle = Угол заполнения
.assoc-array-items = Элементы
.assoc-array-level-spacing = Расстояние между уровнями
.assoc-array-levels = Уровни
.assoc-array-row-spacing = Расстояние между рядами
.assoc-array-column-spacing = Расстояние между столбцами
.fbxexport-exported-to = FBXEXPORT: экспортируется в "__ocs_fmt_0__"
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: в этом чертеже нет данных 3D-сети.
.fbxexport-error = FBXEXPORT: __ocs_fmt_0__
+10
View File
@@ -268,6 +268,16 @@ command-copy =
.mode = COPY Kopyalama modu seçin [Single/Multiple] <Multiple>:
common =
.assoc-array-array-path = Dizi (Yol)
.assoc-array-array-polar = Dizi (Kutupsal)
.assoc-array-array-rectangular = Dizi (Dikdörtgen)
.assoc-array-item-spacing = Öğe aralığı
.assoc-array-fill-angle = Doldurma açısı
.assoc-array-items = Öğeler
.assoc-array-level-spacing = Seviye aralığı
.assoc-array-levels = Seviyeler
.assoc-array-row-spacing = Satır aralığı
.assoc-array-column-spacing = Sütun aralığı
.fbxexport-exported-to = FBXEXPORT: "__ocs_fmt_0__" konumuna dışa aktarıldı
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT: Bu çizimde 3B örgü verisi yok.
.fbxexport-error = FBXEXPORT: __ocs_fmt_0__
+10
View File
@@ -270,6 +270,16 @@ command-copy =
.mode = COPY 输入复制模式选项 [Single/Multiple] <Multiple>:
common =
.assoc-array-array-path = 阵列 (路径)
.assoc-array-array-polar = 阵列 (环形)
.assoc-array-array-rectangular = 阵列 (矩形)
.assoc-array-item-spacing = 项目间距
.assoc-array-fill-angle = 填充角度
.assoc-array-items = 项目
.assoc-array-level-spacing = 层间距
.assoc-array-levels = 层数
.assoc-array-row-spacing = 行间距
.assoc-array-column-spacing = 列间距
.fbxexport-exported-to = FBXEXPORT:导出为"__ocs_fmt_0__"
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT:此绘图中没有3D网格数据。
.fbxexport-error = FBXEXPORT:__ocs_fmt_0__ 时间轴:
+10
View File
@@ -282,6 +282,16 @@ command-copy =
.mode = COPY 輸入複製模式選項 [Single/Multiple] <Multiple>:
common =
.assoc-array-array-path = 陣列 (路徑)
.assoc-array-array-polar = 陣列 (環形)
.assoc-array-array-rectangular = 陣列 (矩形)
.assoc-array-item-spacing = 項目間距
.assoc-array-fill-angle = 填滿角度
.assoc-array-items = 項目
.assoc-array-level-spacing = 層間距
.assoc-array-levels = 層數
.assoc-array-row-spacing = 列間距
.assoc-array-column-spacing = 欄間距
.fbxexport-exported-to = FBXEXPORT:匯出為"__ocs_fmt_0__"
.fbxexport-no-3d-mesh-data-in-this-drawing = FBXEXPORT:此繪圖中沒有3D網格資料。
.fbxexport-error = FBXEXPORT:__ocs_fmt_0__ 時間軸:
+4
View File
@@ -924,6 +924,10 @@ impl OpenCADStudio {
crate::io::xref::layer_overrides(doc, &br.name, &br.xref_path, &base_dir);
xref_rows(&mut sections, ins, &br.xref_path, factor, overrides);
}
// An associative array's counts and spacings.
if let Some(array) = crate::scene::assoc_array::array_of_insert(doc, handle) {
sections.push(crate::scene::assoc_array::property_section(doc, &array));
}
}
// Underlay: name, page, path and size from the definition.
codec::EntityType::Underlay(ul) => {
+38
View File
@@ -3748,6 +3748,40 @@ impl OpenCADStudio {
Task::none()
}
/// An associative array row: the typed value changes the array's
/// parameter and its items are placed again, in one undo step.
fn on_assoc_array_field_commit(&mut self, i: usize, field: &'static str, handles: &[Handle]) {
let Some(text) = self.tabs[i]
.properties
.edit_buf
.remove(&crate::ui::properties::FieldKey::Geom(field))
else {
return;
};
let arrays: Vec<_> = handles
.iter()
.filter_map(|handle| {
crate::scene::assoc_array::array_of_insert(&self.tabs[i].scene.document, *handle)
})
.collect();
if arrays.is_empty() {
return;
}
self.push_undo_snapshot(i, "ARRAYEDIT");
for array in &arrays {
if let Err(error) = crate::scene::assoc_array::apply_field(
&mut self.tabs[i].scene,
array,
field,
&crate::app::expr_eval::eval_to_string(&text),
) {
self.command_line.push_error(&format!("ARRAYEDIT: {error}"));
}
}
self.tabs[i].dirty = true;
self.refresh_properties();
}
pub(super) fn on_prop_geom_commit(&mut self, field: &'static str) -> Task<Message> {
use crate::ui::window::named_parameters::ParamField;
// A dynamic dimension's Name/Expression rows edit its parameter.
@@ -3765,6 +3799,10 @@ impl OpenCADStudio {
let i = self.active_tab;
self.tabs[i].properties.active_field = None;
let handles = self.property_target_handles(i);
if field.starts_with("assoc_array_") {
self.on_assoc_array_field_commit(i, field, &handles);
return Task::none();
}
if !handles.is_empty() {
let driven_reference = match field {
"start_x" | "start_y" | "start_z" => Some(Some(0)),
+10
View File
@@ -3702,6 +3702,16 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"Width factor" => Some(("common", "width-factor-sentence-case")),
"Width: %{value}" => Some(("ui", "width-value")),
"Window" => Some(("common", "window")),
"Column spacing" => Some(("common", "assoc-array-column-spacing")),
"Row spacing" => Some(("common", "assoc-array-row-spacing")),
"Levels" => Some(("common", "assoc-array-levels")),
"Level spacing" => Some(("common", "assoc-array-level-spacing")),
"Items" => Some(("common", "assoc-array-items")),
"Fill angle" => Some(("common", "assoc-array-fill-angle")),
"Item spacing" => Some(("common", "assoc-array-item-spacing")),
"Array (Rectangular)" => Some(("common", "assoc-array-array-rectangular")),
"Array (Polar)" => Some(("common", "assoc-array-array-polar")),
"Array (Path)" => Some(("common", "assoc-array-array-path")),
"Window Elements" => Some(("common", "options-window-elements")),
"Show the menu bar (MENUBAR)" => Some(("common", "options-show-menu-bar")),
"Show the ribbon (RIBBON / RIBBONCLOSE)" => Some(("common", "options-show-ribbon")),
+558
View File
@@ -0,0 +1,558 @@
//! Associative arrays as the drawing stores them: an AcDbAssocAction whose
//! body is an AcDbAssocArrayActionBody, with dependencies on the array INSERT
//! (read + write), the anonymous block holding the item INSERTs (write) and the
//! anonymous block holding the source objects (read). The action's value
//! parameters (Items, ItemSpacing, Rows, …) define where the items go.
use codec::objects::{
AssocAction, AssocArrayActionBody, AssocDependency, AssocEvalValue, AssocValueParam,
AssociativeData, ObjectType,
};
use codec::{CadDocument, Handle};
use glam::{DMat4, DVec3};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ArrayKind {
Rectangular,
Polar,
Path,
}
/// One associative array, resolved from its array INSERT.
#[derive(Clone, Debug)]
pub struct AssocArray {
pub insert: Handle,
pub action: Handle,
pub body: Handle,
/// Block record holding the item INSERTs.
pub array_block: Handle,
/// Block record holding the source objects.
pub source_block: Handle,
pub kind: ArrayKind,
}
fn dependency(document: &CadDocument, handle: Handle) -> Option<&AssocDependency> {
match document.objects.get(&handle)? {
ObjectType::Associative(object) => match &object.data {
AssociativeData::Dependency(dependency) => Some(dependency),
AssociativeData::GeomDependency(geom) => Some(&geom.dependency),
AssociativeData::ValueDependency(value) => Some(&value.dependency),
_ => None,
},
_ => None,
}
}
pub fn action(document: &CadDocument, handle: Handle) -> Option<&AssocAction> {
match document.objects.get(&handle)? {
ObjectType::Associative(object) => match &object.data {
AssociativeData::Action(action) => Some(action),
_ => None,
},
_ => None,
}
}
pub fn body(document: &CadDocument, handle: Handle) -> Option<&AssocArrayActionBody> {
match document.objects.get(&handle)? {
ObjectType::Associative(object) => match &object.data {
AssociativeData::ArrayActionBody(body) => Some(body),
AssociativeData::ArrayModifyActionBody(modify) => Some(&modify.body),
_ => None,
},
_ => None,
}
}
/// The associative array whose array INSERT is `insert`, if any.
pub fn array_of_insert(document: &CadDocument, insert: Handle) -> Option<AssocArray> {
document.objects.iter().find_map(|(&handle, object)| {
let ObjectType::Associative(object) = object else {
return None;
};
let AssociativeData::Action(action) = &object.data else {
return None;
};
let array_body = body(document, action.action_body)?;
let (mut array_block, mut source_block, mut owns_insert) =
(Handle::NULL, Handle::NULL, false);
for link in &action.dependencies {
let Some(dependency) = dependency(document, link.dependency) else {
continue;
};
match (dependency.is_read_dependency, dependency.is_write_dependency) {
(true, true) if dependency.dependent_on == insert => owns_insert = true,
(false, true) => array_block = dependency.dependent_on,
(true, false) if source_block.is_null() => {
source_block = dependency.dependent_on
}
_ => {}
}
}
let kind = match array_body.parameter_block.as_str() {
"AcDbAssocArrayRectangularParameters" => ArrayKind::Rectangular,
"AcDbAssocArrayPolarParameters" => ArrayKind::Polar,
"AcDbAssocArrayPathParameters" => ArrayKind::Path,
_ => return None,
};
(owns_insert && !array_block.is_null() && !source_block.is_null()).then_some(
AssocArray {
insert,
action: handle,
body: action.action_body,
array_block,
source_block,
kind,
},
)
})
}
/// 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] {
match action(document, array.action) {
Some(action) if !action.values.is_empty() => &action.values,
_ => body(document, array.body).map_or(&[], |body| &body.parameter_body.values),
}
}
/// The number a value parameter holds, whatever its stored type.
pub fn param_value(params: &[AssocValueParam], name: &str) -> Option<f64> {
let param = params.iter().find(|param| param.name == name)?;
match &param.variables.first()?.value.value {
AssocEvalValue::Real(value) => Some(*value),
AssocEvalValue::Long(value) => Some(f64::from(*value)),
AssocEvalValue::Short(value) => Some(f64::from(*value)),
AssocEvalValue::Byte(value) => Some(f64::from(*value)),
_ => None,
}
}
/// Store `value` in a value parameter, keeping its stored type.
pub fn set_param_value(params: &mut [AssocValueParam], name: &str, value: f64) -> bool {
let Some(variable) = params
.iter_mut()
.find(|param| param.name == name)
.and_then(|param| param.variables.first_mut())
else {
return false;
};
variable.value.value = match variable.value.value {
AssocEvalValue::Long(_) => AssocEvalValue::Long(value.round() as i32),
AssocEvalValue::Short(_) => AssocEvalValue::Short(value.round() as i16),
AssocEvalValue::Byte(_) => AssocEvalValue::Byte(value.round() as u8),
_ => AssocEvalValue::Real(value),
};
true
}
/// The direction columns step along: the line of the action's "RowProfile"
/// parameter, X when the array has none.
pub fn column_axis(document: &CadDocument, array: &AssocArray) -> DVec3 {
let Some(action) = action(document, array.action) else {
return DVec3::X;
};
action
.owned_parameters
.iter()
.find_map(|handle| {
let ObjectType::Associative(object) = document.objects.get(handle)? else {
return None;
};
let AssociativeData::EdgeActionParam(edge) = &object.data else {
return None;
};
if edge.single_dependency.action_param.name != "RowProfile" || edge.action_type != 19
{
return None;
}
match edge.curve.get(1)? {
codec::objects::AssocCurveValue::Point(direction) => {
DVec3::new(direction.x, direction.y, direction.z).try_normalize()
}
_ => None,
}
})
.unwrap_or(DVec3::X)
}
/// The plane normal of an array: its "BaseNormal" vertex parameter, Z when
/// the array has none.
pub fn base_normal(document: &CadDocument, array: &AssocArray) -> DVec3 {
let Some(action) = action(document, array.action) else {
return DVec3::Z;
};
action
.owned_parameters
.iter()
.find_map(|handle| {
let ObjectType::Associative(object) = document.objects.get(handle)? else {
return None;
};
let AssociativeData::VertexActionParam(vertex) = &object.data else {
return None;
};
if vertex.single_dependency.action_param.name != "BaseNormal" {
return None;
}
DVec3::new(vertex.point.x, vertex.point.y, vertex.point.z).try_normalize()
})
.unwrap_or(DVec3::Z)
}
/// Item placements of a rectangular array in array-local coordinates:
/// (column, row, level) and the offset of the item's base point. Columns
/// step along `axis`; rows run at AxesAngle from it about `normal`, and
/// levels along `normal`.
pub fn rectangular_items(
params: &[AssocValueParam],
axis: DVec3,
normal: DVec3,
) -> Vec<([i32; 3], DVec3)> {
let get = |name: &str, default: f64| param_value(params, name).unwrap_or(default);
let (columns, rows, levels) = (
get("Items", 1.0).max(1.0) as i32,
get("Rows", 1.0).max(1.0) as i32,
get("Levels", 1.0).max(1.0) as i32,
);
let column_step = axis * get("ItemSpacing", 0.0);
let axes = get("AxesAngle", std::f64::consts::FRAC_PI_2);
let row_axis = glam::DQuat::from_axis_angle(normal, axes) * axis;
let row_step = row_axis * get("RowSpacing", 0.0) + normal * get("RowElevation", 0.0);
let level_step = normal * get("LevelSpacing", 0.0);
let mut items = Vec::with_capacity((columns * rows * levels).max(0) as usize);
for level in 0..levels {
for row in 0..rows {
for column in 0..columns {
let offset = column_step * f64::from(column)
+ row_step * f64::from(row)
+ level_step * f64::from(level);
items.push(([column, row, level], offset));
}
}
}
items
}
/// An array item's placement in array-local coordinates, as stored.
pub fn stored_item_transform(item: &codec::objects::AssocArrayItem) -> DMat4 {
if item.uses_default_transform {
let offset = item.x_direction;
DMat4::from_translation(DVec3::new(offset.x, offset.y, offset.z))
} else {
DMat4::from_cols_array(&item.transform).transpose()
}
}
/// The in-plane X axis angles are measured from: world X for a plan array,
/// the arbitrary-axis direction otherwise.
fn plane_x(normal: DVec3) -> DVec3 {
if normal.x.abs() < 1.0 / 64.0 && normal.y.abs() < 1.0 / 64.0 {
DVec3::Y.cross(normal).normalize()
} else {
DVec3::Z.cross(normal).normalize()
}
}
/// The centre of a polar array, recovered from its first item and its
/// Radius and StartAngle.
pub fn polar_center(document: &CadDocument, array: &AssocArray) -> Option<DVec3> {
let params = value_params(document, array);
let first = body(document, array.body)?
.items
.iter()
.find(|item| item.location == [0, 0, 0])?;
let normal = base_normal(document, array);
let x = plane_x(normal);
let y = normal.cross(x);
let start = param_value(params, "StartAngle").unwrap_or(0.0);
let radius = param_value(params, "Radius")?;
Some(
stored_item_transform(first).w_axis.truncate()
- (x * start.cos() + y * start.sin()) * radius,
)
}
/// Item placements of a polar array about `center`: each item turns about
/// the centre by its multiple of ItemSpacing (rotated with it unless
/// AlignItems is 0); rows step the radius out, levels step along the normal.
pub fn polar_items(
params: &[AssocValueParam],
center: DVec3,
normal: DVec3,
) -> Vec<([i32; 3], DMat4)> {
let get = |name: &str, default: f64| param_value(params, name).unwrap_or(default);
let (count, rows, levels) = (
get("Items", 1.0).max(1.0) as i32,
get("Rows", 1.0).max(1.0) as i32,
get("Levels", 1.0).max(1.0) as i32,
);
// ponytail: Direction 0 read as clockwise; no sample with 0 to confirm.
let sign = if get("Direction", 1.0) == 0.0 { -1.0 } else { 1.0 };
let align = get("AlignItems", 1.0) != 0.0;
let (spacing, start, radius) =
(get("ItemSpacing", 0.0), get("StartAngle", 0.0), get("Radius", 0.0));
let x = plane_x(normal);
let y = normal.cross(x);
let start_dir = x * start.cos() + y * start.sin();
let mut items = Vec::with_capacity((count * rows * levels).max(0) as usize);
for level in 0..levels {
for row in 0..rows {
let ring = radius + f64::from(row) * get("RowSpacing", 0.0);
let lift = normal
* (f64::from(row) * get("RowElevation", 0.0)
+ f64::from(level) * get("LevelSpacing", 0.0));
for item in 0..count {
let turn = glam::DQuat::from_axis_angle(normal, sign * f64::from(item) * spacing);
let position = center + turn * (start_dir * ring) + lift;
let placement = if align {
DMat4::from_rotation_translation(turn, position)
} else {
DMat4::from_translation(position)
};
items.push(([item, row, level], placement));
}
}
}
items
}
/// The rows the Properties palette shows for an array: (field, parameter,
/// label, kind) where kind 0 is a count, 1 a length and 2 an angle.
fn property_rows(kind: ArrayKind) -> &'static [(&'static str, &'static str, &'static str, u8)] {
match kind {
ArrayKind::Rectangular => &[
("assoc_array_columns", "Items", "Columns", 0),
("assoc_array_column_spacing", "ItemSpacing", "Column spacing", 1),
("assoc_array_rows", "Rows", "Rows", 0),
("assoc_array_row_spacing", "RowSpacing", "Row spacing", 1),
("assoc_array_levels", "Levels", "Levels", 0),
("assoc_array_level_spacing", "LevelSpacing", "Level spacing", 1),
],
ArrayKind::Polar => &[
("assoc_array_items", "Items", "Items", 0),
("assoc_array_fill_angle", "FillAngle", "Fill angle", 2),
("assoc_array_radius", "Radius", "Radius", 1),
("assoc_array_rows", "Rows", "Rows", 0),
("assoc_array_row_spacing", "RowSpacing", "Row spacing", 1),
("assoc_array_levels", "Levels", "Levels", 0),
("assoc_array_level_spacing", "LevelSpacing", "Level spacing", 1),
],
ArrayKind::Path => &[
("assoc_array_items", "Items", "Items", 0),
("assoc_array_item_spacing", "ItemSpacing", "Item spacing", 1),
("assoc_array_rows", "Rows", "Rows", 0),
("assoc_array_row_spacing", "RowSpacing", "Row spacing", 1),
("assoc_array_levels", "Levels", "Levels", 0),
("assoc_array_level_spacing", "LevelSpacing", "Level spacing", 1),
],
}
}
/// The Properties section of an associative array. Path arrays are shown
/// read-only: their items follow a curve this does not re-place yet.
pub fn property_section(
document: &CadDocument,
array: &AssocArray,
) -> crate::scene::model::object::PropSection {
use crate::entities::common::{format_angle, format_length};
use crate::scene::model::object::{PropSection, PropValue, Property};
let params = value_params(document, array);
let props = property_rows(array.kind)
.iter()
.filter_map(|&(field, name, label, kind)| {
let value = param_value(params, name)?;
let text = match kind {
0 => format!("{}", value.round() as i64),
1 => format_length(value),
_ => format_angle(value),
};
Some(Property {
label: crate::t!(label).into_owned(),
field,
value: if array.kind == ArrayKind::Path {
PropValue::ReadOnly(text)
} else {
PropValue::EditText(text)
},
})
})
.collect();
let title = match array.kind {
ArrayKind::Rectangular => "Array (Rectangular)",
ArrayKind::Polar => "Array (Polar)",
ArrayKind::Path => "Array (Path)",
};
PropSection {
title: crate::t!(title).into_owned(),
props,
}
}
/// Write a Properties edit of an array row: the parameter changes and the
/// items are placed again. `text` is the typed value (counts, lengths in
/// drawing units, angles as typed angles).
pub fn apply_field(
scene: &mut crate::scene::Scene,
array: &AssocArray,
field: &str,
text: &str,
) -> Result<(), String> {
let Some(&(_, name, _, kind)) =
property_rows(array.kind).iter().find(|row| row.0 == field)
else {
return Err("unknown array field".into());
};
let value = match kind {
0 => text
.trim()
.parse::<f64>()
.ok()
.filter(|v| *v >= 1.0 && *v <= 100_000.0)
.map(f64::round),
1 => crate::entities::common::parse_typed_length(text),
_ => crate::entities::common::parse_typed_angle(text),
}
.filter(|v| v.is_finite())
.ok_or("invalid value")?;
update(scene, array, &[(name, value)])
}
/// Change an array's parameters and place its items again: the item
/// INSERTs in the array block, the body's item records and the parameter
/// values move together, so the array stays associative on save.
pub fn update(
scene: &mut crate::scene::Scene,
array: &AssocArray,
changes: &[(&str, f64)],
) -> Result<(), String> {
use codec::objects::AssocArrayItem;
use codec::EntityType;
if array.kind == ArrayKind::Path {
return Err("path arrays cannot be edited yet".into());
}
let document = &scene.document;
let mut params = value_params(document, array).to_vec();
let center = match array.kind {
ArrayKind::Polar => polar_center(document, array).ok_or("array has no first item")?,
_ => DVec3::ZERO,
};
for (name, value) in changes {
set_param_value(&mut params, name, *value);
}
if array.kind == ArrayKind::Polar
&& changes.iter().any(|(name, _)| matches!(*name, "Items" | "FillAngle"))
{
let fill = param_value(&params, "FillAngle").unwrap_or(std::f64::consts::TAU);
let count = param_value(&params, "Items").unwrap_or(1.0).max(1.0);
let spacing = if (fill.abs() - std::f64::consts::TAU).abs() < 1e-9 {
fill / count
} else if count > 1.0 {
fill / (count - 1.0)
} else {
0.0
};
set_param_value(&mut params, "ItemSpacing", spacing);
}
let normal = base_normal(document, array);
let placements: Vec<([i32; 3], DMat4)> = match array.kind {
ArrayKind::Rectangular => {
rectangular_items(&params, column_axis(document, array), normal)
.into_iter()
.map(|(location, offset)| (location, DMat4::from_translation(offset)))
.collect()
}
_ => polar_items(&params, center, normal),
};
let current = body(document, array.body).ok_or("array body missing")?;
let template = current
.items
.iter()
.filter_map(|item| item.first_handle)
.find_map(|handle| match document.get_entity(handle) {
Some(EntityType::Insert(insert)) => Some(insert.clone()),
_ => None,
})
.ok_or("array has no item reference")?;
let old_items: Vec<Handle> = document
.block_records
.iter()
.find(|record| record.handle == array.array_block)
.map(|record| record.entity_handles.clone())
.unwrap_or_default()
.into_iter()
.filter(|handle| matches!(document.get_entity(*handle), Some(EntityType::Insert(_))))
.collect();
let document = &mut scene.document;
for handle in &old_items {
document.remove_entity(*handle);
}
let mut items = Vec::with_capacity(placements.len());
let mut new_handles = Vec::with_capacity(placements.len());
for (location, placement) in placements {
let mut insert = template.clone();
insert.common.handle = Handle::NULL;
insert.common.owner_handle = array.array_block;
insert.common.reactors.clear();
insert.common.xdictionary_handle = None;
let position = placement.w_axis.truncate();
insert.insert_point = codec::types::Vector3::new(position.x, position.y, position.z);
insert.rotation = placement.x_axis.y.atan2(placement.x_axis.x);
let handle = document
.add_entity(EntityType::Insert(insert))
.map_err(|error| error.to_string())?;
let turned = (placement.x_axis.truncate() - DVec3::X).length() > 1e-12
|| (placement.y_axis.truncate() - DVec3::Y).length() > 1e-12;
items.push(AssocArrayItem {
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()
});
new_handles.push(handle);
}
if let Some(record) = document
.block_records
.iter_mut()
.find(|record| record.handle == array.source_block)
{
record.insert_handles.retain(|handle| !old_items.contains(handle));
record.insert_handles.extend(new_handles);
}
let values_in_action = action(document, array.action).is_some_and(|a| !a.values.is_empty());
if let Some(ObjectType::Associative(object)) = document.objects.get_mut(&array.body) {
let array_body = match &mut object.data {
AssociativeData::ArrayActionBody(body) => Some(body),
AssociativeData::ArrayModifyActionBody(modify) => Some(&mut modify.body),
_ => None,
};
if let Some(array_body) = array_body {
array_body.items = items;
if !values_in_action {
array_body.parameter_body.values = params.clone();
}
}
}
if values_in_action {
if let Some(ObjectType::Associative(object)) = document.objects.get_mut(&array.action) {
if let AssociativeData::Action(action) = &mut object.data {
action.values = params;
}
}
}
scene.bump_geometry();
Ok(())
}
+1
View File
@@ -6,6 +6,7 @@
// view — camera, transforms, viewport, render pipeline driver
// cache — block-definition and property caches
pub mod annotative;
pub mod assoc_array;
pub mod block_service;
pub mod cache;
pub mod convert;