diff --git a/assets/icons/attributes/lineweight.svg b/assets/icons/attributes/lineweight.svg
new file mode 100644
index 000000000..1cd7f45e2
--- /dev/null
+++ b/assets/icons/attributes/lineweight.svg
@@ -0,0 +1,57 @@
+
+
diff --git a/assets/icons/attributes/transparency.svg b/assets/icons/attributes/transparency.svg
new file mode 100644
index 000000000..451288a65
--- /dev/null
+++ b/assets/icons/attributes/transparency.svg
@@ -0,0 +1,50 @@
+
+
diff --git a/assets/icons/gradient/gradient_curved.svg b/assets/icons/gradient/gradient_curved.svg
new file mode 100644
index 000000000..62e2dcada
--- /dev/null
+++ b/assets/icons/gradient/gradient_curved.svg
@@ -0,0 +1,135 @@
+
+
diff --git a/assets/icons/gradient/gradient_curved_inverted.svg b/assets/icons/gradient/gradient_curved_inverted.svg
new file mode 100644
index 000000000..75177eb43
--- /dev/null
+++ b/assets/icons/gradient/gradient_curved_inverted.svg
@@ -0,0 +1,155 @@
+
+
diff --git a/assets/icons/gradient/gradient_cylindrical.svg b/assets/icons/gradient/gradient_cylindrical.svg
new file mode 100644
index 000000000..5ae41e792
--- /dev/null
+++ b/assets/icons/gradient/gradient_cylindrical.svg
@@ -0,0 +1,97 @@
+
+
diff --git a/assets/icons/gradient/gradient_cylindrical_inverted.svg b/assets/icons/gradient/gradient_cylindrical_inverted.svg
new file mode 100644
index 000000000..015caca73
--- /dev/null
+++ b/assets/icons/gradient/gradient_cylindrical_inverted.svg
@@ -0,0 +1,97 @@
+
+
diff --git a/assets/icons/gradient/gradient_hemispherical.svg b/assets/icons/gradient/gradient_hemispherical.svg
new file mode 100644
index 000000000..675bc94a8
--- /dev/null
+++ b/assets/icons/gradient/gradient_hemispherical.svg
@@ -0,0 +1,90 @@
+
+
diff --git a/assets/icons/gradient/gradient_hemispherical_inverted.svg b/assets/icons/gradient/gradient_hemispherical_inverted.svg
new file mode 100644
index 000000000..e2a254153
--- /dev/null
+++ b/assets/icons/gradient/gradient_hemispherical_inverted.svg
@@ -0,0 +1,100 @@
+
+
diff --git a/assets/icons/gradient/gradient_linear.svg b/assets/icons/gradient/gradient_linear.svg
new file mode 100644
index 000000000..f1f3c2736
--- /dev/null
+++ b/assets/icons/gradient/gradient_linear.svg
@@ -0,0 +1,89 @@
+
+
diff --git a/assets/icons/gradient/gradient_spherical.svg b/assets/icons/gradient/gradient_spherical.svg
new file mode 100644
index 000000000..b95f3b4f7
--- /dev/null
+++ b/assets/icons/gradient/gradient_spherical.svg
@@ -0,0 +1,90 @@
+
+
diff --git a/assets/icons/gradient/gradient_spherical_inverted.svg b/assets/icons/gradient/gradient_spherical_inverted.svg
new file mode 100644
index 000000000..2e6816449
--- /dev/null
+++ b/assets/icons/gradient/gradient_spherical_inverted.svg
@@ -0,0 +1,90 @@
+
+
diff --git a/assets/icons/graphic_attributes.svg b/assets/icons/graphic_attributes.svg
new file mode 100644
index 000000000..aed1edd32
--- /dev/null
+++ b/assets/icons/graphic_attributes.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/hatch/hatch_gradient.svg b/assets/icons/hatch/hatch_gradient.svg
index 199d8a941..7faf6ee0a 100644
--- a/assets/icons/hatch/hatch_gradient.svg
+++ b/assets/icons/hatch/hatch_gradient.svg
@@ -1,10 +1,87 @@
-
diff --git a/assets/icons/hatch/hatch_none.svg b/assets/icons/hatch/hatch_none.svg
new file mode 100644
index 000000000..fe8936ab9
--- /dev/null
+++ b/assets/icons/hatch/hatch_none.svg
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
diff --git a/assets/icons/hatch/hatch_solid.svg b/assets/icons/hatch/hatch_solid.svg
index 5cccf8100..846d024a7 100644
--- a/assets/icons/hatch/hatch_solid.svg
+++ b/assets/icons/hatch/hatch_solid.svg
@@ -1,5 +1,45 @@
-
+
+
+
+
-
-
+
diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl
index d2055bb35..a13a13223 100644
--- a/locales/ar-SA/opencadstudio.ftl
+++ b/locales/ar-SA/opencadstudio.ftl
@@ -6185,3 +6185,38 @@ dock =
.new-group = مجموعة جديدة
.browser = المستعرض
.node-graph = مخطط العقد
+
+graphic-attributes =
+ .title = سمات الرسم
+ .set-all-bylayer = تعيين كل السمات إلى ByLayer
+ .set-all-byblock = تعيين كل السمات إلى ByBlock
+ .remove-bylayer-byblock = إزالة ByLayer / ByBlock
+ .create-layer-active-settings = إنشاء طبقة بالإعدادات النشطة
+ .fills-on-current-layer = إنشاء التعبئات على الطبقة الحالية
+ .edit-hatch = تحرير التهشير
+ .edit-gradient = تحرير التدرج
+ .close-open-object = الكائن المحدد مفتوح. هل تريد إغلاقه قبل إنشاء التعبئة؟ [Yes / No]
+ .close-open-objects = __ocs_fmt_0__ من الكائنات المحددة مفتوحة. هل تريد إغلاقها قبل إنشاء التعبئة؟ [Yes / No]
+ .enter-new-layer-name = أدخل اسمًا للطبقة الجديدة:
+ .invalid-layer-name = سمات الرسم: أدخل اسم طبقة صالحًا بدون <>/\":;?*|,=`.
+ .layer-exists = سمات الرسم: الطبقة "__ocs_fmt_0__" موجودة بالفعل.
+ .layer-not-created = سمات الرسم: تعذر إنشاء الطبقة.
+ .layer-created = تم إنشاء الطبقة "__ocs_fmt_0__" بإعدادات سمات الرسم النشطة.
+ .gradient-fill-locked = تعبئة متدرجة: التعبئة على طبقة مقفلة.
+ .select-closed-objects = حدد كائنًا مغلقًا واحدًا أو أكثر أولاً.
+ .locked-objects-skipped = سمات الرسم: لم يتم تغيير __ocs_fmt_0__ من الكائنات على طبقة مقفلة.
+ .no-hatch-pattern = سمات الرسم: لا يتوفر نمط تهشير.
+ .associated-fill-locked = سمات الرسم: توجد تعبئة مرتبطة على طبقة مقفلة.
+ .one-unsupported = سمات الرسم: لم يتم تغيير كائن واحد غير مدعوم.
+ .unsupported-skipped = سمات الرسم: لم يتم تغيير __ocs_fmt_0__ من الكائنات غير المدعومة.
+ .fill-transparency = شفافية التعبئة
+ .fill-change = تغيير التعبئة
+ .line-transparency = شفافية الخطوط
+ .cylindrical-short = أسطواني
+ .spherical-short = كروي
+ .hemispherical-short = نصف كروي
+ .invert-gradient = عكس التدرج
+ .tone = الدرجة
+ .reset-angle = إعادة تعيين الزاوية
+ .one-color = لون واحد
+ .two-colors = لونان
diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl
index c843de1e1..d6d751979 100644
--- a/locales/bg-BG/opencadstudio.ftl
+++ b/locales/bg-BG/opencadstudio.ftl
@@ -6173,3 +6173,38 @@ dock =
.new-group = Нова група
.browser = Браузър
.node-graph = Граф на възлите
+
+graphic-attributes =
+ .title = Графични атрибути
+ .set-all-bylayer = Задаване на всички атрибути на ByLayer
+ .set-all-byblock = Задаване на всички атрибути на ByBlock
+ .remove-bylayer-byblock = Премахване на ByLayer / ByBlock
+ .create-layer-active-settings = Създаване на слой с активните настройки
+ .fills-on-current-layer = Създаване на запълвания в текущия слой
+ .edit-hatch = Редактиране на щриховката
+ .edit-gradient = Редактиране на преливането
+ .close-open-object = Избраният обект е отворен. Да се затвори ли преди създаване на запълването? [Yes / No]
+ .close-open-objects = __ocs_fmt_0__ от избраните обекти са отворени. Да се затворят ли преди създаване на запълването? [Yes / No]
+ .enter-new-layer-name = Въведете име за новия слой:
+ .invalid-layer-name = Графични атрибути: въведете валидно име на слой без <>/\":;?*|,=`.
+ .layer-exists = Графични атрибути: слоят "__ocs_fmt_0__" вече съществува.
+ .layer-not-created = Графични атрибути: слоят не можа да бъде създаден.
+ .layer-created = Създаден е слой "__ocs_fmt_0__" с активните настройки на графичните атрибути.
+ .gradient-fill-locked = Преливащо запълване: запълването е в заключен слой.
+ .select-closed-objects = Първо изберете един или повече затворени обекта.
+ .locked-objects-skipped = Графични атрибути: __ocs_fmt_0__ обекта в заключен слой не бяха променени.
+ .no-hatch-pattern = Графични атрибути: няма наличен шаблон за щриховка.
+ .associated-fill-locked = Графични атрибути: свързано запълване е в заключен слой.
+ .one-unsupported = Графични атрибути: 1 неподдържан обект не беше променен.
+ .unsupported-skipped = Графични атрибути: __ocs_fmt_0__ неподдържани обекта не бяха променени.
+ .fill-transparency = Прозрачност на запълването
+ .fill-change = Промяна на запълването
+ .line-transparency = Прозрачност на линиите
+ .cylindrical-short = Цил.
+ .spherical-short = Сфер.
+ .hemispherical-short = Полусф.
+ .invert-gradient = Обръщане на преливането
+ .tone = Тон
+ .reset-angle = Нулиране на ъгъла
+ .one-color = Един цвят
+ .two-colors = Два цвята
diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl
index 1ff56bf14..779f63fb7 100644
--- a/locales/cs-CZ/opencadstudio.ftl
+++ b/locales/cs-CZ/opencadstudio.ftl
@@ -6170,3 +6170,38 @@ dock =
.new-group = Nová skupina
.browser = Prohlížeč
.node-graph = Graf uzlů
+
+graphic-attributes =
+ .title = Grafické atributy
+ .set-all-bylayer = Nastavit všechny atributy na ByLayer
+ .set-all-byblock = Nastavit všechny atributy na ByBlock
+ .remove-bylayer-byblock = Odebrat ByLayer / ByBlock
+ .create-layer-active-settings = Vytvořit hladinu s aktivním nastavením
+ .fills-on-current-layer = Vytvářet výplně v aktuální hladině
+ .edit-hatch = Upravit šrafu
+ .edit-gradient = Upravit přechod
+ .close-open-object = Vybraný objekt je otevřený. Uzavřít jej před vytvořením výplně? [Yes / No]
+ .close-open-objects = Vybrané objekty jsou otevřené (__ocs_fmt_0__). Uzavřít je před vytvořením výplně? [Yes / No]
+ .enter-new-layer-name = Zadejte název nové hladiny:
+ .invalid-layer-name = Grafické atributy: zadejte platný název hladiny bez <>/\":;?*|,=`.
+ .layer-exists = Grafické atributy: hladina "__ocs_fmt_0__" již existuje.
+ .layer-not-created = Grafické atributy: hladinu nelze vytvořit.
+ .layer-created = Hladina "__ocs_fmt_0__" byla vytvořena s aktivním nastavením grafických atributů.
+ .gradient-fill-locked = Přechodová výplň: výplň je v zamčené hladině.
+ .select-closed-objects = Nejprve vyberte jeden nebo více uzavřených objektů.
+ .locked-objects-skipped = Grafické atributy: objekty v zamčené hladině (__ocs_fmt_0__) nebyly změněny.
+ .no-hatch-pattern = Grafické atributy: není k dispozici žádný vzor šraf.
+ .associated-fill-locked = Grafické atributy: přidružená výplň je v zamčené hladině.
+ .one-unsupported = Grafické atributy: 1 nepodporovaný objekt nebyl změněn.
+ .unsupported-skipped = Grafické atributy: nepodporované objekty (__ocs_fmt_0__) nebyly změněny.
+ .fill-transparency = Průhlednost výplně
+ .fill-change = Změna výplně
+ .line-transparency = Průhlednost čar
+ .cylindrical-short = Válc.
+ .spherical-short = Kul.
+ .hemispherical-short = Polokul.
+ .invert-gradient = Obrátit přechod
+ .tone = Tón
+ .reset-angle = Obnovit úhel
+ .one-color = Jedna barva
+ .two-colors = Dvě barvy
diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl
index b7ee02cae..f0e33e76b 100644
--- a/locales/de-DE/opencadstudio.ftl
+++ b/locales/de-DE/opencadstudio.ftl
@@ -6171,3 +6171,38 @@ dock =
.new-group = Neue Gruppe
.browser = Browser
.node-graph = Knotengraph
+
+graphic-attributes =
+ .title = Grafikattribute
+ .set-all-bylayer = Alle Attribute auf ByLayer setzen
+ .set-all-byblock = Alle Attribute auf ByBlock setzen
+ .remove-bylayer-byblock = ByLayer / ByBlock entfernen
+ .create-layer-active-settings = Layer mit aktiven Einstellungen erstellen
+ .fills-on-current-layer = Füllungen auf dem aktuellen Layer erstellen
+ .edit-hatch = Schraffur bearbeiten
+ .edit-gradient = Verlauf bearbeiten
+ .close-open-object = Das ausgewählte Objekt ist offen. Vor dem Erstellen der Füllung schließen? [Yes / No]
+ .close-open-objects = __ocs_fmt_0__ ausgewählte Objekte sind offen. Vor dem Erstellen der Füllung schließen? [Yes / No]
+ .enter-new-layer-name = Namen für den neuen Layer eingeben:
+ .invalid-layer-name = Grafikattribute: gültigen Layernamen ohne <>/\":;?*|,=` eingeben.
+ .layer-exists = Grafikattribute: Layer "__ocs_fmt_0__" ist bereits vorhanden.
+ .layer-not-created = Grafikattribute: Der Layer konnte nicht erstellt werden.
+ .layer-created = Layer "__ocs_fmt_0__" mit den aktiven Grafikattributen erstellt.
+ .gradient-fill-locked = Verlaufsfüllung: Die Füllung liegt auf einem gesperrten Layer.
+ .select-closed-objects = Zuerst ein oder mehrere geschlossene Objekte auswählen.
+ .locked-objects-skipped = Grafikattribute: __ocs_fmt_0__ Objekt(e) auf einem gesperrten Layer wurden nicht geändert.
+ .no-hatch-pattern = Grafikattribute: Kein Schraffurmuster verfügbar.
+ .associated-fill-locked = Grafikattribute: Eine zugehörige Füllung liegt auf einem gesperrten Layer.
+ .one-unsupported = Grafikattribute: 1 nicht unterstütztes Objekt wurde nicht geändert.
+ .unsupported-skipped = Grafikattribute: __ocs_fmt_0__ nicht unterstützte Objekte wurden nicht geändert.
+ .fill-transparency = Füllungstransparenz
+ .fill-change = Füllung ändern
+ .line-transparency = Linientransparenz
+ .cylindrical-short = Zyl.
+ .spherical-short = Kugel
+ .hemispherical-short = Halbk.
+ .invert-gradient = Verlauf umkehren
+ .tone = Ton
+ .reset-angle = Winkel zurücksetzen
+ .one-color = Einfarbig
+ .two-colors = Zweifarbig
diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl
index 9d7e93337..d3906e20d 100644
--- a/locales/el-GR/opencadstudio.ftl
+++ b/locales/el-GR/opencadstudio.ftl
@@ -6173,3 +6173,38 @@ dock =
.new-group = Νέα ομάδα
.browser = Περιηγητής
.node-graph = Γράφος κόμβων
+
+graphic-attributes =
+ .title = Γραφικά χαρακτηριστικά
+ .set-all-bylayer = Ορισμός όλων των χαρακτηριστικών σε ByLayer
+ .set-all-byblock = Ορισμός όλων των χαρακτηριστικών σε ByBlock
+ .remove-bylayer-byblock = Αφαίρεση ByLayer / ByBlock
+ .create-layer-active-settings = Δημιουργία επιπέδου με τις ενεργές ρυθμίσεις
+ .fills-on-current-layer = Δημιουργία γεμισμάτων στο τρέχον επίπεδο
+ .edit-hatch = Επεξεργασία διαγράμμισης
+ .edit-gradient = Επεξεργασία διαβάθμισης
+ .close-open-object = Το επιλεγμένο αντικείμενο είναι ανοιχτό. Να κλείσει πριν από τη δημιουργία του γεμίσματος; [Yes / No]
+ .close-open-objects = __ocs_fmt_0__ επιλεγμένα αντικείμενα είναι ανοιχτά. Να κλείσουν πριν από τη δημιουργία του γεμίσματος; [Yes / No]
+ .enter-new-layer-name = Εισαγάγετε όνομα για το νέο επίπεδο:
+ .invalid-layer-name = Γραφικά χαρακτηριστικά: εισαγάγετε έγκυρο όνομα επιπέδου χωρίς <>/\":;?*|,=`.
+ .layer-exists = Γραφικά χαρακτηριστικά: το επίπεδο "__ocs_fmt_0__" υπάρχει ήδη.
+ .layer-not-created = Γραφικά χαρακτηριστικά: δεν ήταν δυνατή η δημιουργία του επιπέδου.
+ .layer-created = Δημιουργήθηκε το επίπεδο "__ocs_fmt_0__" με τις ενεργές ρυθμίσεις γραφικών χαρακτηριστικών.
+ .gradient-fill-locked = Γέμισμα διαβάθμισης: το γέμισμα βρίσκεται σε κλειδωμένο επίπεδο.
+ .select-closed-objects = Επιλέξτε πρώτα ένα ή περισσότερα κλειστά αντικείμενα.
+ .locked-objects-skipped = Γραφικά χαρακτηριστικά: __ocs_fmt_0__ αντικείμενα σε κλειδωμένο επίπεδο δεν άλλαξαν.
+ .no-hatch-pattern = Γραφικά χαρακτηριστικά: δεν υπάρχει διαθέσιμο μοτίβο διαγράμμισης.
+ .associated-fill-locked = Γραφικά χαρακτηριστικά: ένα συσχετισμένο γέμισμα βρίσκεται σε κλειδωμένο επίπεδο.
+ .one-unsupported = Γραφικά χαρακτηριστικά: 1 μη υποστηριζόμενο αντικείμενο δεν άλλαξε.
+ .unsupported-skipped = Γραφικά χαρακτηριστικά: __ocs_fmt_0__ μη υποστηριζόμενα αντικείμενα δεν άλλαξαν.
+ .fill-transparency = Διαφάνεια γεμίσματος
+ .fill-change = Αλλαγή γεμίσματος
+ .line-transparency = Διαφάνεια γραμμών
+ .cylindrical-short = Κυλ.
+ .spherical-short = Σφαιρ.
+ .hemispherical-short = Ημισφ.
+ .invert-gradient = Αντιστροφή διαβάθμισης
+ .tone = Τόνος
+ .reset-angle = Επαναφορά γωνίας
+ .one-color = Ένα χρώμα
+ .two-colors = Δύο χρώματα
diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl
index 744f93a65..8ae79dd11 100644
--- a/locales/en-US/opencadstudio.ftl
+++ b/locales/en-US/opencadstudio.ftl
@@ -6174,3 +6174,38 @@ dock =
.new-group = New group
.browser = Browser
.node-graph = Node Graph
+
+graphic-attributes =
+ .title = Graphic Attributes
+ .set-all-bylayer = Set all attributes ByLayer
+ .set-all-byblock = Set all attributes ByBlock
+ .remove-bylayer-byblock = Remove ByLayer / ByBlock
+ .create-layer-active-settings = Create Layer with active settings
+ .fills-on-current-layer = Create fills on the current layer
+ .edit-hatch = Edit hatch
+ .edit-gradient = Edit gradient
+ .close-open-object = The selected object is open. Close it before creating the fill? [Yes / No]
+ .close-open-objects = __ocs_fmt_0__ selected objects are open. Close them before creating the fill? [Yes / No]
+ .enter-new-layer-name = Enter a name for the new layer:
+ .invalid-layer-name = Graphic Attributes: enter a valid layer name without <>/\":;?*|,=`.
+ .layer-exists = Graphic Attributes: layer "__ocs_fmt_0__" already exists.
+ .layer-not-created = Graphic Attributes: the layer could not be created.
+ .layer-created = Created layer "__ocs_fmt_0__" with the active Graphic Attributes settings.
+ .gradient-fill-locked = Gradient Fill: the fill is on a locked layer.
+ .select-closed-objects = Select one or more closed objects first.
+ .locked-objects-skipped = Graphic Attributes: __ocs_fmt_0__ object(s) on a locked layer were not changed.
+ .no-hatch-pattern = Graphic Attributes: no hatch pattern is available.
+ .associated-fill-locked = Graphic Attributes: an associated fill is on a locked layer.
+ .one-unsupported = Graphic Attributes: 1 unsupported object was not changed.
+ .unsupported-skipped = Graphic Attributes: __ocs_fmt_0__ unsupported objects were not changed.
+ .fill-transparency = Fill transparency
+ .fill-change = Fill change
+ .line-transparency = Line transparency
+ .cylindrical-short = Cyl.
+ .spherical-short = Spher.
+ .hemispherical-short = Hemi.
+ .invert-gradient = Invert gradient
+ .tone = Tone
+ .reset-angle = Reset angle
+ .one-color = One Color
+ .two-colors = Two Colors
diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl
index 11687f899..282915ac7 100644
--- a/locales/es-ES/opencadstudio.ftl
+++ b/locales/es-ES/opencadstudio.ftl
@@ -6173,3 +6173,38 @@ dock =
.new-group = Nuevo grupo
.browser = Explorador
.node-graph = Grafo de nodos
+
+graphic-attributes =
+ .title = Atributos gráficos
+ .set-all-bylayer = Establecer todos los atributos en ByLayer
+ .set-all-byblock = Establecer todos los atributos en ByBlock
+ .remove-bylayer-byblock = Quitar ByLayer / ByBlock
+ .create-layer-active-settings = Crear capa con la configuración activa
+ .fills-on-current-layer = Crear rellenos en la capa actual
+ .edit-hatch = Editar sombreado
+ .edit-gradient = Editar degradado
+ .close-open-object = El objeto seleccionado está abierto. ¿Cerrarlo antes de crear el relleno? [Yes / No]
+ .close-open-objects = __ocs_fmt_0__ objetos seleccionados están abiertos. ¿Cerrarlos antes de crear el relleno? [Yes / No]
+ .enter-new-layer-name = Introduzca un nombre para la capa nueva:
+ .invalid-layer-name = Atributos gráficos: introduzca un nombre de capa válido sin <>/\":;?*|,=`.
+ .layer-exists = Atributos gráficos: la capa "__ocs_fmt_0__" ya existe.
+ .layer-not-created = Atributos gráficos: no se pudo crear la capa.
+ .layer-created = Se creó la capa "__ocs_fmt_0__" con la configuración activa de atributos gráficos.
+ .gradient-fill-locked = Relleno degradado: el relleno está en una capa bloqueada.
+ .select-closed-objects = Seleccione primero uno o varios objetos cerrados.
+ .locked-objects-skipped = Atributos gráficos: no se modificaron __ocs_fmt_0__ objeto(s) en una capa bloqueada.
+ .no-hatch-pattern = Atributos gráficos: no hay ningún patrón de sombreado disponible.
+ .associated-fill-locked = Atributos gráficos: un relleno asociado está en una capa bloqueada.
+ .one-unsupported = Atributos gráficos: no se modificó 1 objeto no admitido.
+ .unsupported-skipped = Atributos gráficos: no se modificaron __ocs_fmt_0__ objetos no admitidos.
+ .fill-transparency = Transparencia del relleno
+ .fill-change = Cambio de relleno
+ .line-transparency = Transparencia de línea
+ .cylindrical-short = Cil.
+ .spherical-short = Esfér.
+ .hemispherical-short = Semiesf.
+ .invert-gradient = Invertir degradado
+ .tone = Tono
+ .reset-angle = Restablecer ángulo
+ .one-color = Un color
+ .two-colors = Dos colores
diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl
index bc4d6dbb2..71e626c9e 100644
--- a/locales/fi-FI/opencadstudio.ftl
+++ b/locales/fi-FI/opencadstudio.ftl
@@ -6170,3 +6170,38 @@ dock =
.new-group = Uusi ryhmä
.browser = Selain
.node-graph = Solmukaavio
+
+graphic-attributes =
+ .title = Grafiikkamääritteet
+ .set-all-bylayer = Aseta kaikki määritteet arvoon ByLayer
+ .set-all-byblock = Aseta kaikki määritteet arvoon ByBlock
+ .remove-bylayer-byblock = Poista ByLayer / ByBlock
+ .create-layer-active-settings = Luo taso aktiivisilla asetuksilla
+ .fills-on-current-layer = Luo täytöt nykyiselle tasolle
+ .edit-hatch = Muokkaa viivoitusta
+ .edit-gradient = Muokkaa liukuväriä
+ .close-open-object = Valittu objekti on avoin. Suljetaanko se ennen täytön luomista? [Yes / No]
+ .close-open-objects = __ocs_fmt_0__ valittua objektia on avoimia. Suljetaanko ne ennen täytön luomista? [Yes / No]
+ .enter-new-layer-name = Anna uuden tason nimi:
+ .invalid-layer-name = Grafiikkamääritteet: anna kelvollinen tason nimi ilman merkkejä <>/\":;?*|,=`.
+ .layer-exists = Grafiikkamääritteet: taso "__ocs_fmt_0__" on jo olemassa.
+ .layer-not-created = Grafiikkamääritteet: tasoa ei voitu luoda.
+ .layer-created = Taso "__ocs_fmt_0__" luotiin aktiivisilla grafiikkamääritteillä.
+ .gradient-fill-locked = Liukuväritäyttö: täyttö on lukitulla tasolla.
+ .select-closed-objects = Valitse ensin yksi tai useampi suljettu objekti.
+ .locked-objects-skipped = Grafiikkamääritteet: __ocs_fmt_0__ lukitulla tasolla olevaa objektia jätettiin muuttamatta.
+ .no-hatch-pattern = Grafiikkamääritteet: viivoituskuviota ei ole saatavilla.
+ .associated-fill-locked = Grafiikkamääritteet: liitetty täyttö on lukitulla tasolla.
+ .one-unsupported = Grafiikkamääritteet: 1 tukematonta objektia ei muutettu.
+ .unsupported-skipped = Grafiikkamääritteet: __ocs_fmt_0__ tukematonta objektia jätettiin muuttamatta.
+ .fill-transparency = Täytön läpinäkyvyys
+ .fill-change = Täytön muutos
+ .line-transparency = Viivojen läpinäkyvyys
+ .cylindrical-short = Lieriö
+ .spherical-short = Pallo
+ .hemispherical-short = Puolip.
+ .invert-gradient = Käännä liukuväri
+ .tone = Sävy
+ .reset-angle = Palauta kulma
+ .one-color = Yksi väri
+ .two-colors = Kaksi väriä
diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl
index e27a389f4..47ab852c5 100644
--- a/locales/fr-FR/opencadstudio.ftl
+++ b/locales/fr-FR/opencadstudio.ftl
@@ -6172,3 +6172,38 @@ dock =
.new-group = Nouveau groupe
.browser = Navigateur
.node-graph = Graphe de nœuds
+
+graphic-attributes =
+ .title = Attributs graphiques
+ .set-all-bylayer = Définir tous les attributs sur ByLayer
+ .set-all-byblock = Définir tous les attributs sur ByBlock
+ .remove-bylayer-byblock = Supprimer ByLayer / ByBlock
+ .create-layer-active-settings = Créer un calque avec les paramètres actifs
+ .fills-on-current-layer = Créer les remplissages sur le calque courant
+ .edit-hatch = Modifier la hachure
+ .edit-gradient = Modifier le dégradé
+ .close-open-object = L'objet sélectionné est ouvert. Le fermer avant de créer le remplissage ? [Yes / No]
+ .close-open-objects = __ocs_fmt_0__ objets sélectionnés sont ouverts. Les fermer avant de créer le remplissage ? [Yes / No]
+ .enter-new-layer-name = Entrez un nom pour le nouveau calque :
+ .invalid-layer-name = Attributs graphiques : entrez un nom de calque valide sans <>/\":;?*|,=`.
+ .layer-exists = Attributs graphiques : le calque "__ocs_fmt_0__" existe déjà.
+ .layer-not-created = Attributs graphiques : impossible de créer le calque.
+ .layer-created = Calque "__ocs_fmt_0__" créé avec les attributs graphiques actifs.
+ .gradient-fill-locked = Remplissage dégradé : le remplissage est sur un calque verrouillé.
+ .select-closed-objects = Sélectionnez d'abord un ou plusieurs objets fermés.
+ .locked-objects-skipped = Attributs graphiques : __ocs_fmt_0__ objet(s) sur un calque verrouillé n'ont pas été modifiés.
+ .no-hatch-pattern = Attributs graphiques : aucun motif de hachure n'est disponible.
+ .associated-fill-locked = Attributs graphiques : un remplissage associé est sur un calque verrouillé.
+ .one-unsupported = Attributs graphiques : 1 objet non pris en charge n'a pas été modifié.
+ .unsupported-skipped = Attributs graphiques : __ocs_fmt_0__ objets non pris en charge n'ont pas été modifiés.
+ .fill-transparency = Transparence du remplissage
+ .fill-change = Modification du remplissage
+ .line-transparency = Transparence des lignes
+ .cylindrical-short = Cyl.
+ .spherical-short = Sphér.
+ .hemispherical-short = Hémi.
+ .invert-gradient = Inverser le dégradé
+ .tone = Ton
+ .reset-angle = Réinitialiser l'angle
+ .one-color = Une couleur
+ .two-colors = Deux couleurs
diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl
index d73c8d401..b6e4ed947 100644
--- a/locales/hi-IN/opencadstudio.ftl
+++ b/locales/hi-IN/opencadstudio.ftl
@@ -6161,3 +6161,38 @@ dock =
.new-group = नया समूह
.browser = ब्राउज़र
.node-graph = नोड ग्राफ़
+
+graphic-attributes =
+ .title = ग्राफ़िक गुण
+ .set-all-bylayer = सभी गुण ByLayer पर सेट करें
+ .set-all-byblock = सभी गुण ByBlock पर सेट करें
+ .remove-bylayer-byblock = ByLayer / ByBlock हटाएँ
+ .create-layer-active-settings = सक्रिय सेटिंग्स के साथ लेयर बनाएँ
+ .fills-on-current-layer = भराव वर्तमान लेयर पर बनाएँ
+ .edit-hatch = हैच संपादित करें
+ .edit-gradient = ग्रेडिएंट संपादित करें
+ .close-open-object = चयनित ऑब्जेक्ट खुला है। भराव बनाने से पहले इसे बंद करें? [Yes / No]
+ .close-open-objects = __ocs_fmt_0__ चयनित ऑब्जेक्ट खुले हैं। भराव बनाने से पहले इन्हें बंद करें? [Yes / No]
+ .enter-new-layer-name = नई लेयर का नाम दर्ज करें:
+ .invalid-layer-name = ग्राफ़िक गुण: <>/\":;?*|,=` के बिना मान्य लेयर नाम दर्ज करें।
+ .layer-exists = ग्राफ़िक गुण: लेयर "__ocs_fmt_0__" पहले से मौजूद है।
+ .layer-not-created = ग्राफ़िक गुण: लेयर नहीं बनाई जा सकी।
+ .layer-created = सक्रिय ग्राफ़िक गुण सेटिंग्स के साथ लेयर "__ocs_fmt_0__" बनाई गई।
+ .gradient-fill-locked = ग्रेडिएंट भराव: भराव लॉक की गई लेयर पर है।
+ .select-closed-objects = पहले एक या अधिक बंद ऑब्जेक्ट चुनें।
+ .locked-objects-skipped = ग्राफ़िक गुण: लॉक की गई लेयर पर __ocs_fmt_0__ ऑब्जेक्ट नहीं बदले गए।
+ .no-hatch-pattern = ग्राफ़िक गुण: कोई हैच पैटर्न उपलब्ध नहीं है।
+ .associated-fill-locked = ग्राफ़िक गुण: एक संबद्ध भराव लॉक की गई लेयर पर है।
+ .one-unsupported = ग्राफ़िक गुण: 1 असमर्थित ऑब्जेक्ट नहीं बदला गया।
+ .unsupported-skipped = ग्राफ़िक गुण: __ocs_fmt_0__ असमर्थित ऑब्जेक्ट नहीं बदले गए।
+ .fill-transparency = भराव पारदर्शिता
+ .fill-change = भराव परिवर्तन
+ .line-transparency = रेखा पारदर्शिता
+ .cylindrical-short = बेलन.
+ .spherical-short = गोल.
+ .hemispherical-short = अर्धगोल.
+ .invert-gradient = ग्रेडिएंट उलटें
+ .tone = टोन
+ .reset-angle = कोण रीसेट करें
+ .one-color = एक रंग
+ .two-colors = दो रंग
diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl
index a18d25a9b..00674b1f2 100644
--- a/locales/hu-HU/opencadstudio.ftl
+++ b/locales/hu-HU/opencadstudio.ftl
@@ -6159,3 +6159,38 @@ dock =
.new-group = Új csoport
.browser = Böngésző
.node-graph = Csomópontgráf
+
+graphic-attributes =
+ .title = Grafikus jellemzők
+ .set-all-bylayer = Minden jellemző ByLayer értékre
+ .set-all-byblock = Minden jellemző ByBlock értékre
+ .remove-bylayer-byblock = ByLayer / ByBlock eltávolítása
+ .create-layer-active-settings = Fólia létrehozása az aktív beállításokkal
+ .fills-on-current-layer = Kitöltések létrehozása az aktuális fólián
+ .edit-hatch = Sraffozás szerkesztése
+ .edit-gradient = Színátmenet szerkesztése
+ .close-open-object = A kijelölt objektum nyitott. Bezárja a kitöltés létrehozása előtt? [Yes / No]
+ .close-open-objects = __ocs_fmt_0__ kijelölt objektum nyitott. Bezárja őket a kitöltés létrehozása előtt? [Yes / No]
+ .enter-new-layer-name = Adja meg az új fólia nevét:
+ .invalid-layer-name = Grafikus jellemzők: adjon meg érvényes fólianevet <>/\":;?*|,=` nélkül.
+ .layer-exists = Grafikus jellemzők: a(z) "__ocs_fmt_0__" fólia már létezik.
+ .layer-not-created = Grafikus jellemzők: a fóliát nem sikerült létrehozni.
+ .layer-created = A(z) "__ocs_fmt_0__" fólia létrejött az aktív grafikus jellemzőkkel.
+ .gradient-fill-locked = Színátmenetes kitöltés: a kitöltés zárolt fólián van.
+ .select-closed-objects = Először jelöljön ki egy vagy több zárt objektumot.
+ .locked-objects-skipped = Grafikus jellemzők: __ocs_fmt_0__ zárolt fólián lévő objektum nem változott.
+ .no-hatch-pattern = Grafikus jellemzők: nincs elérhető sraffozási minta.
+ .associated-fill-locked = Grafikus jellemzők: egy társított kitöltés zárolt fólián van.
+ .one-unsupported = Grafikus jellemzők: 1 nem támogatott objektum nem változott.
+ .unsupported-skipped = Grafikus jellemzők: __ocs_fmt_0__ nem támogatott objektum nem változott.
+ .fill-transparency = Kitöltés átlátszósága
+ .fill-change = Kitöltés módosítása
+ .line-transparency = Vonalak átlátszósága
+ .cylindrical-short = Henger
+ .spherical-short = Gömb
+ .hemispherical-short = Félgömb
+ .invert-gradient = Színátmenet megfordítása
+ .tone = Tónus
+ .reset-angle = Szög visszaállítása
+ .one-color = Egy szín
+ .two-colors = Két szín
diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl
index 9ae5c0d42..d84e500ad 100644
--- a/locales/it-IT/opencadstudio.ftl
+++ b/locales/it-IT/opencadstudio.ftl
@@ -6172,3 +6172,38 @@ dock =
.new-group = Nuovo gruppo
.browser = Browser
.node-graph = Grafo dei nodi
+
+graphic-attributes =
+ .title = Attributi grafici
+ .set-all-bylayer = Imposta tutti gli attributi su ByLayer
+ .set-all-byblock = Imposta tutti gli attributi su ByBlock
+ .remove-bylayer-byblock = Rimuovi ByLayer / ByBlock
+ .create-layer-active-settings = Crea layer con le impostazioni attive
+ .fills-on-current-layer = Crea i riempimenti sul layer corrente
+ .edit-hatch = Modifica tratteggio
+ .edit-gradient = Modifica sfumatura
+ .close-open-object = L'oggetto selezionato è aperto. Chiuderlo prima di creare il riempimento? [Yes / No]
+ .close-open-objects = __ocs_fmt_0__ oggetti selezionati sono aperti. Chiuderli prima di creare il riempimento? [Yes / No]
+ .enter-new-layer-name = Immettere un nome per il nuovo layer:
+ .invalid-layer-name = Attributi grafici: immettere un nome di layer valido senza <>/\":;?*|,=`.
+ .layer-exists = Attributi grafici: il layer "__ocs_fmt_0__" esiste già.
+ .layer-not-created = Attributi grafici: impossibile creare il layer.
+ .layer-created = Layer "__ocs_fmt_0__" creato con le impostazioni attive degli attributi grafici.
+ .gradient-fill-locked = Riempimento sfumato: il riempimento è su un layer bloccato.
+ .select-closed-objects = Selezionare prima uno o più oggetti chiusi.
+ .locked-objects-skipped = Attributi grafici: __ocs_fmt_0__ oggetto/i su un layer bloccato non sono stati modificati.
+ .no-hatch-pattern = Attributi grafici: nessun modello di tratteggio disponibile.
+ .associated-fill-locked = Attributi grafici: un riempimento associato è su un layer bloccato.
+ .one-unsupported = Attributi grafici: 1 oggetto non supportato non è stato modificato.
+ .unsupported-skipped = Attributi grafici: __ocs_fmt_0__ oggetti non supportati non sono stati modificati.
+ .fill-transparency = Trasparenza riempimento
+ .fill-change = Modifica riempimento
+ .line-transparency = Trasparenza linee
+ .cylindrical-short = Cil.
+ .spherical-short = Sfer.
+ .hemispherical-short = Emisf.
+ .invert-gradient = Inverti sfumatura
+ .tone = Tono
+ .reset-angle = Reimposta angolo
+ .one-color = Un colore
+ .two-colors = Due colori
diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl
index e09121acc..ed37d6a97 100644
--- a/locales/ja-JP/opencadstudio.ftl
+++ b/locales/ja-JP/opencadstudio.ftl
@@ -6161,3 +6161,38 @@ dock =
.new-group = 新しいグループ
.browser = ブラウザー
.node-graph = ノードグラフ
+
+graphic-attributes =
+ .title = グラフィック属性
+ .set-all-bylayer = すべての属性を ByLayer に設定
+ .set-all-byblock = すべての属性を ByBlock に設定
+ .remove-bylayer-byblock = ByLayer / ByBlock を解除
+ .create-layer-active-settings = 現在の設定で画層を作成
+ .fills-on-current-layer = 塗り潰しを現在の画層に作成
+ .edit-hatch = ハッチングを編集
+ .edit-gradient = グラデーションを編集
+ .close-open-object = 選択したオブジェクトは開いています。塗り潰しを作成する前に閉じますか? [Yes / No]
+ .close-open-objects = 選択した __ocs_fmt_0__ 個のオブジェクトは開いています。塗り潰しを作成する前に閉じますか? [Yes / No]
+ .enter-new-layer-name = 新しい画層の名前を入力:
+ .invalid-layer-name = グラフィック属性: <>/\":;?*|,=` を含まない有効な画層名を入力してください。
+ .layer-exists = グラフィック属性: 画層 "__ocs_fmt_0__" は既に存在します。
+ .layer-not-created = グラフィック属性: 画層を作成できませんでした。
+ .layer-created = 現在のグラフィック属性の設定で画層 "__ocs_fmt_0__" を作成しました。
+ .gradient-fill-locked = グラデーション塗り潰し: 塗り潰しはロックされた画層にあります。
+ .select-closed-objects = 先に 1 つ以上の閉じたオブジェクトを選択してください。
+ .locked-objects-skipped = グラフィック属性: ロックされた画層上の __ocs_fmt_0__ 個のオブジェクトは変更されませんでした。
+ .no-hatch-pattern = グラフィック属性: 使用できるハッチング パターンがありません。
+ .associated-fill-locked = グラフィック属性: 関連付けられた塗り潰しがロックされた画層にあります。
+ .one-unsupported = グラフィック属性: サポートされていないオブジェクト 1 個は変更されませんでした。
+ .unsupported-skipped = グラフィック属性: サポートされていないオブジェクト __ocs_fmt_0__ 個は変更されませんでした。
+ .fill-transparency = 塗り潰しの透過性
+ .fill-change = 塗り潰しの変更
+ .line-transparency = 線の透過性
+ .cylindrical-short = 円柱
+ .spherical-short = 球
+ .hemispherical-short = 半球
+ .invert-gradient = グラデーションを反転
+ .tone = 濃淡
+ .reset-angle = 角度をリセット
+ .one-color = 1 色
+ .two-colors = 2 色
diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl
index fc7b52230..a5ed580ee 100644
--- a/locales/ko-KR/opencadstudio.ftl
+++ b/locales/ko-KR/opencadstudio.ftl
@@ -6172,3 +6172,38 @@ dock =
.new-group = 새 그룹
.browser = 브라우저
.node-graph = 노드 그래프
+
+graphic-attributes =
+ .title = 그래픽 속성
+ .set-all-bylayer = 모든 속성을 ByLayer로 설정
+ .set-all-byblock = 모든 속성을 ByBlock으로 설정
+ .remove-bylayer-byblock = ByLayer / ByBlock 제거
+ .create-layer-active-settings = 현재 설정으로 도면층 작성
+ .fills-on-current-layer = 현재 도면층에 채우기 작성
+ .edit-hatch = 해치 편집
+ .edit-gradient = 그라데이션 편집
+ .close-open-object = 선택한 객체가 열려 있습니다. 채우기를 작성하기 전에 닫겠습니까? [Yes / No]
+ .close-open-objects = 선택한 객체 __ocs_fmt_0__개가 열려 있습니다. 채우기를 작성하기 전에 닫겠습니까? [Yes / No]
+ .enter-new-layer-name = 새 도면층 이름 입력:
+ .invalid-layer-name = 그래픽 속성: <>/\":;?*|,=`이(가) 없는 올바른 도면층 이름을 입력하십시오.
+ .layer-exists = 그래픽 속성: 도면층 "__ocs_fmt_0__"이(가) 이미 있습니다.
+ .layer-not-created = 그래픽 속성: 도면층을 작성할 수 없습니다.
+ .layer-created = 현재 그래픽 속성 설정으로 도면층 "__ocs_fmt_0__"을(를) 작성했습니다.
+ .gradient-fill-locked = 그라데이션 채우기: 채우기가 잠긴 도면층에 있습니다.
+ .select-closed-objects = 먼저 닫힌 객체를 하나 이상 선택하십시오.
+ .locked-objects-skipped = 그래픽 속성: 잠긴 도면층의 객체 __ocs_fmt_0__개가 변경되지 않았습니다.
+ .no-hatch-pattern = 그래픽 속성: 사용 가능한 해치 패턴이 없습니다.
+ .associated-fill-locked = 그래픽 속성: 연관된 채우기가 잠긴 도면층에 있습니다.
+ .one-unsupported = 그래픽 속성: 지원되지 않는 객체 1개가 변경되지 않았습니다.
+ .unsupported-skipped = 그래픽 속성: 지원되지 않는 객체 __ocs_fmt_0__개가 변경되지 않았습니다.
+ .fill-transparency = 채우기 투명도
+ .fill-change = 채우기 변경
+ .line-transparency = 선 투명도
+ .cylindrical-short = 원통
+ .spherical-short = 구
+ .hemispherical-short = 반구
+ .invert-gradient = 그라데이션 반전
+ .tone = 색조
+ .reset-angle = 각도 재설정
+ .one-color = 단색
+ .two-colors = 두 색
diff --git a/locales/lo-LA/opencadstudio.ftl b/locales/lo-LA/opencadstudio.ftl
index 71e4dcad5..7304c8344 100644
--- a/locales/lo-LA/opencadstudio.ftl
+++ b/locales/lo-LA/opencadstudio.ftl
@@ -6174,3 +6174,38 @@ dock =
.new-group = ກຸ່ມໃໝ່
.browser = ບຣາວເຊີ
.node-graph = ກຣາຟໂນດ
+
+graphic-attributes =
+ .title = ຄຸນລັກສະນະກຣາຟິກ
+ .set-all-bylayer = ຕັ້ງຄຸນລັກສະນະທັງໝົດເປັນ ByLayer
+ .set-all-byblock = ຕັ້ງຄຸນລັກສະນະທັງໝົດເປັນ ByBlock
+ .remove-bylayer-byblock = ລຶບ ByLayer / ByBlock
+ .create-layer-active-settings = ສ້າງເລເຢີດ້ວຍການຕັ້ງຄ່າປັດຈຸບັນ
+ .fills-on-current-layer = ສ້າງການຕື່ມໃສ່ເລເຢີປັດຈຸບັນ
+ .edit-hatch = ແກ້ໄຂແຮັດ
+ .edit-gradient = ແກ້ໄຂການໄລ່ສີ
+ .close-open-object = ວັດຖຸທີ່ເລືອກເປັນແບບເປີດ. ປິດມັນກ່ອນສ້າງການຕື່ມບໍ່? [Yes / No]
+ .close-open-objects = ວັດຖຸທີ່ເລືອກ __ocs_fmt_0__ ອັນເປັນແບບເປີດ. ປິດພວກມັນກ່ອນສ້າງການຕື່ມບໍ່? [Yes / No]
+ .enter-new-layer-name = ປ້ອນຊື່ສຳລັບເລເຢີໃໝ່:
+ .invalid-layer-name = ຄຸນລັກສະນະກຣາຟິກ: ປ້ອນຊື່ເລເຢີທີ່ຖືກຕ້ອງ ໂດຍບໍ່ມີ <>/\":;?*|,=`.
+ .layer-exists = ຄຸນລັກສະນະກຣາຟິກ: ເລເຢີ "__ocs_fmt_0__" ມີຢູ່ແລ້ວ.
+ .layer-not-created = ຄຸນລັກສະນະກຣາຟິກ: ບໍ່ສາມາດສ້າງເລເຢີໄດ້.
+ .layer-created = ສ້າງເລເຢີ "__ocs_fmt_0__" ດ້ວຍການຕັ້ງຄ່າຄຸນລັກສະນະກຣາຟິກປັດຈຸບັນແລ້ວ.
+ .gradient-fill-locked = ການຕື່ມໄລ່ສີ: ການຕື່ມຢູ່ໃນເລເຢີທີ່ຖືກລັອກ.
+ .select-closed-objects = ເລືອກວັດຖຸປິດໜຶ່ງອັນ ຫຼື ຫຼາຍກວ່ານັ້ນກ່ອນ.
+ .locked-objects-skipped = ຄຸນລັກສະນະກຣາຟິກ: ວັດຖຸ __ocs_fmt_0__ ອັນໃນເລເຢີທີ່ຖືກລັອກບໍ່ໄດ້ຖືກປ່ຽນ.
+ .no-hatch-pattern = ຄຸນລັກສະນະກຣາຟິກ: ບໍ່ມີຮູບແບບແຮັດ.
+ .associated-fill-locked = ຄຸນລັກສະນະກຣາຟິກ: ການຕື່ມທີ່ກ່ຽວຂ້ອງຢູ່ໃນເລເຢີທີ່ຖືກລັອກ.
+ .one-unsupported = ຄຸນລັກສະນະກຣາຟິກ: ວັດຖຸທີ່ບໍ່ຮອງຮັບ 1 ອັນບໍ່ໄດ້ຖືກປ່ຽນ.
+ .unsupported-skipped = ຄຸນລັກສະນະກຣາຟິກ: ວັດຖຸທີ່ບໍ່ຮອງຮັບ __ocs_fmt_0__ ອັນບໍ່ໄດ້ຖືກປ່ຽນ.
+ .fill-transparency = ຄວາມໂປ່ງໃສຂອງການຕື່ມ
+ .fill-change = ປ່ຽນການຕື່ມ
+ .line-transparency = ຄວາມໂປ່ງໃສຂອງເສັ້ນ
+ .cylindrical-short = ທໍ່.
+ .spherical-short = ກົມ.
+ .hemispherical-short = ເຄິ່ງກົມ.
+ .invert-gradient = ກັບການໄລ່ສີ
+ .tone = ໂທນ
+ .reset-angle = ຣີເຊັດມຸມ
+ .one-color = ໜຶ່ງສີ
+ .two-colors = ສອງສີ
diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl
index 80ef8724d..4ad871607 100644
--- a/locales/nl-NL/opencadstudio.ftl
+++ b/locales/nl-NL/opencadstudio.ftl
@@ -6171,3 +6171,38 @@ dock =
.new-group = Nieuwe groep
.browser = Browser
.node-graph = Knooppuntgrafiek
+
+graphic-attributes =
+ .title = Grafische attributen
+ .set-all-bylayer = Alle attributen op ByLayer zetten
+ .set-all-byblock = Alle attributen op ByBlock zetten
+ .remove-bylayer-byblock = ByLayer / ByBlock verwijderen
+ .create-layer-active-settings = Laag maken met actieve instellingen
+ .fills-on-current-layer = Vullingen op de huidige laag maken
+ .edit-hatch = Arcering bewerken
+ .edit-gradient = Kleurverloop bewerken
+ .close-open-object = Het geselecteerde object is open. Sluiten voordat de vulling wordt gemaakt? [Yes / No]
+ .close-open-objects = __ocs_fmt_0__ geselecteerde objecten zijn open. Sluiten voordat de vulling wordt gemaakt? [Yes / No]
+ .enter-new-layer-name = Voer een naam in voor de nieuwe laag:
+ .invalid-layer-name = Grafische attributen: voer een geldige laagnaam in zonder <>/\":;?*|,=`.
+ .layer-exists = Grafische attributen: laag "__ocs_fmt_0__" bestaat al.
+ .layer-not-created = Grafische attributen: de laag kon niet worden gemaakt.
+ .layer-created = Laag "__ocs_fmt_0__" gemaakt met de actieve instellingen van Grafische attributen.
+ .gradient-fill-locked = Verloopvulling: de vulling staat op een vergrendelde laag.
+ .select-closed-objects = Selecteer eerst een of meer gesloten objecten.
+ .locked-objects-skipped = Grafische attributen: __ocs_fmt_0__ object(en) op een vergrendelde laag zijn niet gewijzigd.
+ .no-hatch-pattern = Grafische attributen: er is geen arceerpatroon beschikbaar.
+ .associated-fill-locked = Grafische attributen: een gekoppelde vulling staat op een vergrendelde laag.
+ .one-unsupported = Grafische attributen: 1 niet-ondersteund object is niet gewijzigd.
+ .unsupported-skipped = Grafische attributen: __ocs_fmt_0__ niet-ondersteunde objecten zijn niet gewijzigd.
+ .fill-transparency = Transparantie vulling
+ .fill-change = Vulling wijzigen
+ .line-transparency = Transparantie lijnen
+ .cylindrical-short = Cil.
+ .spherical-short = Bol
+ .hemispherical-short = Halfbol
+ .invert-gradient = Kleurverloop omkeren
+ .tone = Toon
+ .reset-angle = Hoek herstellen
+ .one-color = Eén kleur
+ .two-colors = Twee kleuren
diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl
index b2c44d546..7ba65e368 100644
--- a/locales/pl-PL/opencadstudio.ftl
+++ b/locales/pl-PL/opencadstudio.ftl
@@ -6172,3 +6172,38 @@ dock =
.new-group = Nowa grupa
.browser = Przeglądarka
.node-graph = Graf węzłów
+
+graphic-attributes =
+ .title = Atrybuty graficzne
+ .set-all-bylayer = Ustaw wszystkie atrybuty na ByLayer
+ .set-all-byblock = Ustaw wszystkie atrybuty na ByBlock
+ .remove-bylayer-byblock = Usuń ByLayer / ByBlock
+ .create-layer-active-settings = Utwórz warstwę z aktywnymi ustawieniami
+ .fills-on-current-layer = Twórz wypełnienia na bieżącej warstwie
+ .edit-hatch = Edytuj kreskowanie
+ .edit-gradient = Edytuj gradient
+ .close-open-object = Wybrany obiekt jest otwarty. Zamknąć go przed utworzeniem wypełnienia? [Yes / No]
+ .close-open-objects = Wybrane obiekty są otwarte (__ocs_fmt_0__). Zamknąć je przed utworzeniem wypełnienia? [Yes / No]
+ .enter-new-layer-name = Podaj nazwę nowej warstwy:
+ .invalid-layer-name = Atrybuty graficzne: podaj prawidłową nazwę warstwy bez <>/\":;?*|,=`.
+ .layer-exists = Atrybuty graficzne: warstwa "__ocs_fmt_0__" już istnieje.
+ .layer-not-created = Atrybuty graficzne: nie można utworzyć warstwy.
+ .layer-created = Utworzono warstwę "__ocs_fmt_0__" z aktywnymi atrybutami graficznymi.
+ .gradient-fill-locked = Wypełnienie gradientowe: wypełnienie jest na zablokowanej warstwie.
+ .select-closed-objects = Najpierw wybierz jeden lub więcej zamkniętych obiektów.
+ .locked-objects-skipped = Atrybuty graficzne: obiekty na zablokowanej warstwie (__ocs_fmt_0__) nie zostały zmienione.
+ .no-hatch-pattern = Atrybuty graficzne: brak dostępnego wzoru kreskowania.
+ .associated-fill-locked = Atrybuty graficzne: skojarzone wypełnienie jest na zablokowanej warstwie.
+ .one-unsupported = Atrybuty graficzne: 1 nieobsługiwany obiekt nie został zmieniony.
+ .unsupported-skipped = Atrybuty graficzne: nieobsługiwane obiekty (__ocs_fmt_0__) nie zostały zmienione.
+ .fill-transparency = Przezroczystość wypełnienia
+ .fill-change = Zmiana wypełnienia
+ .line-transparency = Przezroczystość linii
+ .cylindrical-short = Walc.
+ .spherical-short = Sfer.
+ .hemispherical-short = Półsf.
+ .invert-gradient = Odwróć gradient
+ .tone = Ton
+ .reset-angle = Resetuj kąt
+ .one-color = Jeden kolor
+ .two-colors = Dwa kolory
diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl
index 92a940a8d..2e21eeeef 100644
--- a/locales/pt-BR/opencadstudio.ftl
+++ b/locales/pt-BR/opencadstudio.ftl
@@ -6173,3 +6173,38 @@ dock =
.new-group = Novo grupo
.browser = Navegador
.node-graph = Grafo de nós
+
+graphic-attributes =
+ .title = Atributos gráficos
+ .set-all-bylayer = Definir todos os atributos como ByLayer
+ .set-all-byblock = Definir todos os atributos como ByBlock
+ .remove-bylayer-byblock = Remover ByLayer / ByBlock
+ .create-layer-active-settings = Criar camada com as configurações ativas
+ .fills-on-current-layer = Criar preenchimentos na camada atual
+ .edit-hatch = Editar hachura
+ .edit-gradient = Editar gradiente
+ .close-open-object = O objeto selecionado está aberto. Fechá-lo antes de criar o preenchimento? [Yes / No]
+ .close-open-objects = __ocs_fmt_0__ objetos selecionados estão abertos. Fechá-los antes de criar o preenchimento? [Yes / No]
+ .enter-new-layer-name = Digite um nome para a nova camada:
+ .invalid-layer-name = Atributos gráficos: digite um nome de camada válido sem <>/\":;?*|,=`.
+ .layer-exists = Atributos gráficos: a camada "__ocs_fmt_0__" já existe.
+ .layer-not-created = Atributos gráficos: não foi possível criar a camada.
+ .layer-created = Camada "__ocs_fmt_0__" criada com as configurações ativas de atributos gráficos.
+ .gradient-fill-locked = Preenchimento gradiente: o preenchimento está em uma camada bloqueada.
+ .select-closed-objects = Selecione primeiro um ou mais objetos fechados.
+ .locked-objects-skipped = Atributos gráficos: __ocs_fmt_0__ objeto(s) em uma camada bloqueada não foram alterados.
+ .no-hatch-pattern = Atributos gráficos: nenhum padrão de hachura disponível.
+ .associated-fill-locked = Atributos gráficos: um preenchimento associado está em uma camada bloqueada.
+ .one-unsupported = Atributos gráficos: 1 objeto não suportado não foi alterado.
+ .unsupported-skipped = Atributos gráficos: __ocs_fmt_0__ objetos não suportados não foram alterados.
+ .fill-transparency = Transparência do preenchimento
+ .fill-change = Alteração de preenchimento
+ .line-transparency = Transparência da linha
+ .cylindrical-short = Cil.
+ .spherical-short = Esfér.
+ .hemispherical-short = Hemisf.
+ .invert-gradient = Inverter gradiente
+ .tone = Tom
+ .reset-angle = Redefinir ângulo
+ .one-color = Uma cor
+ .two-colors = Duas cores
diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl
index 5bf008a66..9672928b6 100644
--- a/locales/ru-RU/opencadstudio.ftl
+++ b/locales/ru-RU/opencadstudio.ftl
@@ -6174,3 +6174,38 @@ dock =
.new-group = Новая группа
.browser = Обозреватель
.node-graph = Граф узлов
+
+graphic-attributes =
+ .title = Графические атрибуты
+ .set-all-bylayer = Задать всем атрибутам ByLayer
+ .set-all-byblock = Задать всем атрибутам ByBlock
+ .remove-bylayer-byblock = Убрать ByLayer / ByBlock
+ .create-layer-active-settings = Создать слой с текущими настройками
+ .fills-on-current-layer = Создавать заливки на текущем слое
+ .edit-hatch = Редактировать штриховку
+ .edit-gradient = Редактировать градиент
+ .close-open-object = Выбранный объект разомкнут. Замкнуть его перед созданием заливки? [Yes / No]
+ .close-open-objects = Выбранные объекты разомкнуты (__ocs_fmt_0__). Замкнуть их перед созданием заливки? [Yes / No]
+ .enter-new-layer-name = Введите имя нового слоя:
+ .invalid-layer-name = Графические атрибуты: введите допустимое имя слоя без <>/\":;?*|,=`.
+ .layer-exists = Графические атрибуты: слой "__ocs_fmt_0__" уже существует.
+ .layer-not-created = Графические атрибуты: не удалось создать слой.
+ .layer-created = Создан слой "__ocs_fmt_0__" с текущими графическими атрибутами.
+ .gradient-fill-locked = Градиентная заливка: заливка находится на заблокированном слое.
+ .select-closed-objects = Сначала выберите один или несколько замкнутых объектов.
+ .locked-objects-skipped = Графические атрибуты: объекты на заблокированном слое (__ocs_fmt_0__) не изменены.
+ .no-hatch-pattern = Графические атрибуты: нет доступного образца штриховки.
+ .associated-fill-locked = Графические атрибуты: связанная заливка находится на заблокированном слое.
+ .one-unsupported = Графические атрибуты: 1 неподдерживаемый объект не изменён.
+ .unsupported-skipped = Графические атрибуты: неподдерживаемые объекты (__ocs_fmt_0__) не изменены.
+ .fill-transparency = Прозрачность заливки
+ .fill-change = Изменение заливки
+ .line-transparency = Прозрачность линий
+ .cylindrical-short = Цил.
+ .spherical-short = Сфер.
+ .hemispherical-short = Полусф.
+ .invert-gradient = Инвертировать градиент
+ .tone = Тон
+ .reset-angle = Сбросить угол
+ .one-color = Один цвет
+ .two-colors = Два цвета
diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl
index 07531cada..31ed14f3f 100644
--- a/locales/tr-TR/opencadstudio.ftl
+++ b/locales/tr-TR/opencadstudio.ftl
@@ -6096,3 +6096,38 @@ dock =
.new-group = Yeni grup
.browser = Tarayıcı
.node-graph = Düğüm Grafiği
+
+graphic-attributes =
+ .title = Grafik öznitelikleri
+ .set-all-bylayer = Tüm öznitelikleri ByLayer yap
+ .set-all-byblock = Tüm öznitelikleri ByBlock yap
+ .remove-bylayer-byblock = ByLayer / ByBlock kaldır
+ .create-layer-active-settings = Etkin ayarlarla katman oluştur
+ .fills-on-current-layer = Dolguları geçerli katmanda oluştur
+ .edit-hatch = Taramayı düzenle
+ .edit-gradient = Gradyanı düzenle
+ .close-open-object = Seçili nesne açık. Dolgu oluşturmadan önce kapatılsın mı? [Yes / No]
+ .close-open-objects = __ocs_fmt_0__ seçili nesne açık. Dolgu oluşturmadan önce kapatılsın mı? [Yes / No]
+ .enter-new-layer-name = Yeni katman için bir ad girin:
+ .invalid-layer-name = Grafik öznitelikleri: <>/\":;?*|,=` içermeyen geçerli bir katman adı girin.
+ .layer-exists = Grafik öznitelikleri: "__ocs_fmt_0__" katmanı zaten var.
+ .layer-not-created = Grafik öznitelikleri: katman oluşturulamadı.
+ .layer-created = "__ocs_fmt_0__" katmanı etkin grafik öznitelikleriyle oluşturuldu.
+ .gradient-fill-locked = Gradyan dolgu: dolgu kilitli bir katmanda.
+ .select-closed-objects = Önce bir veya daha fazla kapalı nesne seçin.
+ .locked-objects-skipped = Grafik öznitelikleri: kilitli katmandaki __ocs_fmt_0__ nesne değiştirilmedi.
+ .no-hatch-pattern = Grafik öznitelikleri: kullanılabilir tarama deseni yok.
+ .associated-fill-locked = Grafik öznitelikleri: ilişkili bir dolgu kilitli katmanda.
+ .one-unsupported = Grafik öznitelikleri: desteklenmeyen 1 nesne değiştirilmedi.
+ .unsupported-skipped = Grafik öznitelikleri: desteklenmeyen __ocs_fmt_0__ nesne değiştirilmedi.
+ .fill-transparency = Dolgu saydamlığı
+ .fill-change = Dolgu değişikliği
+ .line-transparency = Çizgi saydamlığı
+ .cylindrical-short = Sil.
+ .spherical-short = Küre
+ .hemispherical-short = Yarıküre
+ .invert-gradient = Gradyanı ters çevir
+ .tone = Ton
+ .reset-angle = Açıyı sıfırla
+ .one-color = Tek renk
+ .two-colors = İki renk
diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl
index 9a70bd46a..521e92320 100644
--- a/locales/zh-CN/opencadstudio.ftl
+++ b/locales/zh-CN/opencadstudio.ftl
@@ -6156,3 +6156,38 @@ dock =
.new-group = 新建组
.browser = 浏览器
.node-graph = 节点图
+
+graphic-attributes =
+ .title = 图形特性
+ .set-all-bylayer = 将所有特性设为 ByLayer
+ .set-all-byblock = 将所有特性设为 ByBlock
+ .remove-bylayer-byblock = 移除 ByLayer / ByBlock
+ .create-layer-active-settings = 用当前设置创建图层
+ .fills-on-current-layer = 在当前图层上创建填充
+ .edit-hatch = 编辑填充图案
+ .edit-gradient = 编辑渐变色
+ .close-open-object = 选定对象未闭合。创建填充前是否将其闭合? [Yes / No]
+ .close-open-objects = __ocs_fmt_0__ 个选定对象未闭合。创建填充前是否将其闭合? [Yes / No]
+ .enter-new-layer-name = 输入新图层的名称:
+ .invalid-layer-name = 图形特性: 请输入不含 <>/\":;?*|,=` 的有效图层名称。
+ .layer-exists = 图形特性: 图层 "__ocs_fmt_0__" 已存在。
+ .layer-not-created = 图形特性: 无法创建图层。
+ .layer-created = 已用当前图形特性设置创建图层 "__ocs_fmt_0__"。
+ .gradient-fill-locked = 渐变填充: 填充位于锁定的图层上。
+ .select-closed-objects = 请先选择一个或多个闭合对象。
+ .locked-objects-skipped = 图形特性: 锁定图层上的 __ocs_fmt_0__ 个对象未更改。
+ .no-hatch-pattern = 图形特性: 没有可用的填充图案。
+ .associated-fill-locked = 图形特性: 关联的填充位于锁定的图层上。
+ .one-unsupported = 图形特性: 1 个不支持的对象未更改。
+ .unsupported-skipped = 图形特性: __ocs_fmt_0__ 个不支持的对象未更改。
+ .fill-transparency = 填充透明度
+ .fill-change = 更改填充
+ .line-transparency = 线的透明度
+ .cylindrical-short = 圆柱
+ .spherical-short = 球形
+ .hemispherical-short = 半球
+ .invert-gradient = 反转渐变
+ .tone = 明暗
+ .reset-angle = 重置角度
+ .one-color = 单色
+ .two-colors = 双色
diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl
index afd769830..3346a8f09 100644
--- a/locales/zh-TW/opencadstudio.ftl
+++ b/locales/zh-TW/opencadstudio.ftl
@@ -6168,3 +6168,38 @@ dock =
.new-group = 新增群組
.browser = 瀏覽器
.node-graph = 節點圖
+
+graphic-attributes =
+ .title = 圖形特性
+ .set-all-bylayer = 將所有特性設為 ByLayer
+ .set-all-byblock = 將所有特性設為 ByBlock
+ .remove-bylayer-byblock = 移除 ByLayer / ByBlock
+ .create-layer-active-settings = 以目前設定建立圖層
+ .fills-on-current-layer = 在目前圖層上建立填充
+ .edit-hatch = 編輯填充線
+ .edit-gradient = 編輯漸層
+ .close-open-object = 選取的物件未封閉。建立填充前是否將其封閉? [Yes / No]
+ .close-open-objects = __ocs_fmt_0__ 個選取的物件未封閉。建立填充前是否將其封閉? [Yes / No]
+ .enter-new-layer-name = 輸入新圖層的名稱:
+ .invalid-layer-name = 圖形特性: 請輸入不含 <>/\":;?*|,=` 的有效圖層名稱。
+ .layer-exists = 圖形特性: 圖層 "__ocs_fmt_0__" 已存在。
+ .layer-not-created = 圖形特性: 無法建立圖層。
+ .layer-created = 已以目前圖形特性設定建立圖層 "__ocs_fmt_0__"。
+ .gradient-fill-locked = 漸層填充: 填充位於鎖定的圖層上。
+ .select-closed-objects = 請先選取一個或多個封閉物件。
+ .locked-objects-skipped = 圖形特性: 鎖定圖層上的 __ocs_fmt_0__ 個物件未變更。
+ .no-hatch-pattern = 圖形特性: 沒有可用的填充線樣式。
+ .associated-fill-locked = 圖形特性: 關聯的填充位於鎖定的圖層上。
+ .one-unsupported = 圖形特性: 1 個不支援的物件未變更。
+ .unsupported-skipped = 圖形特性: __ocs_fmt_0__ 個不支援的物件未變更。
+ .fill-transparency = 填充透明度
+ .fill-change = 變更填充
+ .line-transparency = 線的透明度
+ .cylindrical-short = 圓柱
+ .spherical-short = 球形
+ .hemispherical-short = 半球
+ .invert-gradient = 反轉漸層
+ .tone = 明暗
+ .reset-angle = 重設角度
+ .one-color = 單色
+ .two-colors = 雙色
diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs
index 4e06139a9..3820f560e 100644
--- a/src/app/commands/display.rs
+++ b/src/app/commands/display.rs
@@ -500,6 +500,11 @@ impl OpenCADStudio {
return Some(Task::done(Message::ToggleProperties));
}
+ // ── GRAPHICATTRIBUTES — toggle Graphic Attributes palette ───────────
+ "GRAPHICATTRIBUTES" => {
+ return Some(Task::done(Message::ToggleGraphicAttributes));
+ }
+
// ── FILETAB — toggle file/document tabs ──────────────────────────────
"FILETAB" => {
return Some(Task::done(Message::ToggleFileTabs));
diff --git a/src/app/commands/layers.rs b/src/app/commands/layers.rs
index 725b15704..7560af332 100644
--- a/src/app/commands/layers.rs
+++ b/src/app/commands/layers.rs
@@ -3,6 +3,10 @@ use super::*;
impl OpenCADStudio {
pub(super) fn dispatch_layers(&mut self, cmd: &str, i: usize) -> Option> {
match cmd {
+ cmd if cmd.starts_with("GRAPHICATTRIBUTELAYER ") => {
+ let name = cmd.trim_start_matches("GRAPHICATTRIBUTELAYER").trim();
+ return Some(self.create_graphic_attributes_layer(name));
+ }
// ── Layer object commands ──────────────────────────────────────
"LAYOFF" => {
let handles: Vec<_> = self.tabs[i]
diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs
index be8cdbfa7..367b7262a 100644
--- a/src/app/commands/mod.rs
+++ b/src/app/commands/mod.rs
@@ -468,6 +468,7 @@ inventory::submit!(crate::command::CommandRegistration {
"DSETTINGS",
"PARAMETERS",
"GRID",
+ "GRAPHICATTRIBUTES",
"ISODRAFT",
"ISOPLANE",
"OSNAP",
diff --git a/src/app/config.rs b/src/app/config.rs
index 0b2224c79..958111e8e 100644
--- a/src/app/config.rs
+++ b/src/app/config.rs
@@ -30,6 +30,11 @@ pub struct AppConfig {
/// General edge-stack dock layout (which panels are docked, side, order,
/// width and auto-collapse) for the Properties panel and block palette.
pub dock: crate::ui::dock::DockState,
+ /// Whether the optional Graphic Attributes palette is visible.
+ pub show_graphic_attributes: bool,
+ /// Graphic Attributes creates fills on the current layer instead of the
+ /// layer of the filled object.
+ pub graphic_fills_on_current_layer: bool,
/// Add a newly selected annotation scale to existing annotative objects.
pub annotation_auto_scale: i8,
/// Ribbon collapse density.
@@ -52,6 +57,8 @@ impl Default for AppConfig {
start: StartConfig::default(),
statusbar: StatusBarConfig::default(),
dock: crate::ui::dock::DockState::default(),
+ show_graphic_attributes: false,
+ graphic_fills_on_current_layer: false,
annotation_auto_scale: -4,
ribbon: RibbonConfig::default(),
plot: PlotDialogState::default(),
@@ -612,6 +619,18 @@ mod tests {
assert_eq!(deserialized.model_space.selection_window_color, 5);
}
+ #[test]
+ fn graphic_attributes_visibility_defaults_off_and_round_trips() {
+ let missing: AppConfig = serde_json::from_str("{}").expect("deserialize old config");
+ assert!(!missing.show_graphic_attributes);
+
+ let mut original = AppConfig::default();
+ original.show_graphic_attributes = true;
+ let serialized = serde_json::to_string(&original).expect("serialize config");
+ let restored: AppConfig = serde_json::from_str(&serialized).expect("deserialize config");
+ assert!(restored.show_graphic_attributes);
+ }
+
#[test]
fn test_parse_theme_name() {
assert_eq!(parse_theme_name("light"), Some(iced::Theme::Light));
diff --git a/src/app/dock/update.rs b/src/app/dock/update.rs
index e526c7d11..4ca867c98 100644
--- a/src/app/dock/update.rs
+++ b/src/app/dock/update.rs
@@ -442,6 +442,11 @@ impl OpenCADStudio {
self.show_properties = true;
self.ribbon.set_properties(true);
}
+ PanelId::GraphicAttributes => {
+ self.show_graphic_attributes = true;
+ self.ribbon.set_graphic_attributes(true);
+ self.save_config();
+ }
PanelId::BlockPalette => self.open_blocks_palette(None),
PanelId::ExternalReferences => {
self.show_external_references = true;
@@ -488,6 +493,12 @@ impl OpenCADStudio {
self.show_layers = false;
self.ribbon.deactivate_tool_if("LAYERS");
}
+ PanelId::GraphicAttributes => {
+ self.show_graphic_attributes = false;
+ self.ribbon.set_graphic_attributes(false);
+ self.graphic_attributes.close_popups();
+ self.save_config();
+ }
}
self.dock_unpeek(id);
// A closed pallet cannot stay held by the pointer.
@@ -555,6 +566,7 @@ impl OpenCADStudio {
PanelId::Count => self.count_palette.show,
PanelId::SheetSetManager => self.sheet_set.show,
PanelId::Layers => self.show_layers,
+ PanelId::GraphicAttributes => self.show_graphic_attributes,
}
}
diff --git a/src/app/dock/view.rs b/src/app/dock/view.rs
index f1d4d3ff6..7fb744d6a 100644
--- a/src/app/dock/view.rs
+++ b/src/app/dock/view.rs
@@ -381,6 +381,20 @@ impl OpenCADStudio {
};
match id {
crate::ui::dock::PanelId::Properties => tab.properties.view(width, chrome),
+ crate::ui::dock::PanelId::GraphicAttributes => crate::ui::window::graphic_attributes::view(
+ &self.graphic_attributes,
+ &tab.scene,
+ &tab.properties,
+ width,
+ chrome,
+ self.dock
+ .location(id)
+ .map_or(crate::app::config::DockSide::Right, |(side, _)| side),
+ matches!(
+ self.color_pick_target,
+ Some((crate::app::ColorPickTarget::Gradient(_), _))
+ ),
+ ),
crate::ui::dock::PanelId::BlockPalette => {
crate::ui::window::block_palette::view(&self.block_palette, width, chrome)
}
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 682787688..4462dd48a 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -817,6 +817,9 @@ pub(super) struct OpenCADStudio {
show_properties: bool,
/// The Layer Manager pallet (LAYERS) is open.
pub(crate) show_layers: bool,
+ /// Whether the Graphic Attributes palette is shown (GRAPHICATTRIBUTES).
+ show_graphic_attributes: bool,
+ graphic_attributes: crate::ui::window::graphic_attributes::GraphicAttributesState,
/// Docked Insert Block panel visibility.
pub(crate) show_block_palette: bool,
/// Node graph overlay over the viewport.
@@ -1616,6 +1619,9 @@ pub enum ColorPickerTab {
/// Where a colour chosen in the standalone palette window should be applied.
#[derive(Debug, Clone)]
pub enum ColorPickTarget {
+ Gradient(u8),
+ GraphicAttributesLine,
+ GraphicAttributesSolid,
DimStyle(DsField),
MLeader(&'static str),
Table(u8, &'static str),
@@ -2155,6 +2161,7 @@ pub enum PasteFocus {
#[derive(Debug, Clone)]
pub enum Message {
+ GraphicAttributes(crate::ui::window::graphic_attributes::GraphicAttributesMsg),
SpaceMouseWake,
SpaceMouseFrame(iced::time::Instant),
SpaceMouseFocus(iced::window::Id, bool),
@@ -2892,6 +2899,8 @@ pub enum Message {
ToggleViewCube,
/// Toggle the Properties panel visibility (PROPERTIES).
ToggleProperties,
+ /// Toggle the Graphic Attributes panel visibility (GRAPHICATTRIBUTES).
+ ToggleGraphicAttributes,
/// Toggle the document file tabs at the top (FILETAB).
ToggleFileTabs,
/// Toggle the layout tabs at the bottom (LAYOUTTAB).
@@ -4309,6 +4318,8 @@ impl OpenCADStudio {
render_mode_preview: None,
show_properties: true,
show_layers: false,
+ show_graphic_attributes: false,
+ graphic_attributes: Default::default(),
show_block_palette: false,
show_node_graph: false,
property_target_override: None,
diff --git a/src/app/update/command.rs b/src/app/update/command.rs
index dd34454fe..9e13601a1 100644
--- a/src/app/update/command.rs
+++ b/src/app/update/command.rs
@@ -394,6 +394,10 @@ impl OpenCADStudio {
// dropdown so the dispatched command's new prompt is
// immediately visible on the overlay.
self.command_line.close_history();
+ if self.awaiting_fill_close_answer() {
+ let input = std::mem::take(&mut self.command_line.input);
+ return self.on_graphic_fill_close_input(&input);
+ }
// A leading `>` was only a "literal spaces" typing hint (see
// CommandSpace) — drop it before the input is interpreted.
// Free-form text prompts keep it: there it is content, not a
@@ -1100,6 +1104,9 @@ impl OpenCADStudio {
if !self.command_line.input.trim().is_empty() {
return self.update(Message::CommandSubmit);
}
+ if self.awaiting_fill_close_answer() {
+ return self.on_graphic_fill_close_input("");
+ }
// A grip edit is not an active CAD command, but its Dynamic Input
// fields use the same keyboard path. Route Enter through CommandSubmit,
// whose grip branch resolves Distance / Angle and finalizes the edit.
@@ -1154,6 +1161,11 @@ impl OpenCADStudio {
if self.ribbon.escape_extension() {
return Task::none();
}
+ if self.awaiting_fill_close_answer() {
+ self.graphic_attributes.pending_fill_close = None;
+ self.command_line.push_info(crate::t!("*Cancel*").as_ref());
+ return Task::none();
+ }
// Esc drops an unconsumed one-shot snap override and closes
// its menu (#337). Falls through — Esc keeps its usual effect.
self.snap_override_popup = None;
@@ -2225,6 +2237,35 @@ impl OpenCADStudio {
Task::none()
}
+ pub(super) fn on_ribbon_lineweight_changed(
+ &mut self,
+ lw: codec::types::LineWeight,
+ ) -> Task {
+ let i = self.active_tab;
+ self.ribbon.close_dropdown();
+ let handles = self.property_target_handles(i);
+ if handles.is_empty() {
+ if self.has_property_selection(i) {
+ return Task::none();
+ }
+ // Persist into the tab's header (CELWEIGHT). #21.
+ self.tabs[i].scene.document.header.current_line_weight = lw.value();
+ self.tabs[i].dirty = true;
+ self.ribbon.active_lineweight = lw;
+ } else {
+ // Lineweight is baked into the cached wire geometry —
+ // re-tessellate so the change shows immediately (issue #231
+ // class).
+ self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
+ if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) {
+ crate::scene::view::dispatch::apply_line_weight(entity, lw);
+ }
+ });
+ self.ribbon.active_lineweight = lw;
+ }
+ Task::none()
+ }
+
pub(super) fn on_ribbon_linetype_changed(&mut self, lt: String) -> Task {
let i = self.active_tab;
self.ribbon.close_dropdown();
@@ -2331,11 +2372,20 @@ impl OpenCADStudio {
pub(super) fn on_prop_hatch_pattern_changed(&mut self, name: String) -> Task {
let i = self.active_tab;
let handles = self.property_target_handles(i);
+ self.apply_hatch_pattern(i, &handles, name)
+ }
+
+ pub(super) fn apply_hatch_pattern(
+ &mut self,
+ i: usize,
+ handles: &[codec::Handle],
+ name: String,
+ ) -> Task {
if !handles.is_empty() {
use crate::scene::model::hatch_patterns;
if let Some(entry) = hatch_patterns::find(&name) {
self.push_undo_snapshot(i, "HATCHEDIT");
- for &handle in &handles {
+ for &handle in handles {
if let Some(codec::EntityType::Hatch(dxf)) =
self.tabs[i].scene.document.get_entity_mut(handle)
{
diff --git a/src/app/update/file.rs b/src/app/update/file.rs
index 33ce63c39..78e51ec7a 100644
--- a/src/app/update/file.rs
+++ b/src/app/update/file.rs
@@ -1326,6 +1326,8 @@ impl OpenCADStudio {
dock.ensure_settings();
dock
},
+ show_graphic_attributes: self.show_graphic_attributes,
+ graphic_fills_on_current_layer: self.graphic_attributes.fill_on_current_layer,
annotation_auto_scale: self.annotation_auto_scale,
ribbon: crate::app::config::RibbonConfig {
collapse: self.ribbon.collapse_mode(),
@@ -1384,6 +1386,13 @@ impl OpenCADStudio {
let mut dock = cfg.dock;
dock.ensure_settings();
self.dock = dock;
+ self.show_graphic_attributes = cfg.show_graphic_attributes;
+ self.graphic_attributes.fill_on_current_layer = cfg.graphic_fills_on_current_layer;
+ if self.show_graphic_attributes {
+ self.dock.dock_below_properties(crate::ui::dock::PanelId::GraphicAttributes);
+ }
+ self.ribbon
+ .set_graphic_attributes(self.show_graphic_attributes);
self.annotation_auto_scale = cfg.annotation_auto_scale.clamp(-4, 4);
self.ribbon.set_collapse_mode(cfg.ribbon.collapse);
self.plot_dialog = cfg.plot;
diff --git a/src/app/update/graphic_attributes.rs b/src/app/update/graphic_attributes.rs
new file mode 100644
index 000000000..737621450
--- /dev/null
+++ b/src/app/update/graphic_attributes.rs
@@ -0,0 +1,1689 @@
+//! Graphic Attributes palette: applies line attributes and associative fills
+//! to the selection, or to the creation defaults when nothing is selected.
+
+use crate::app::{Message, OpenCADStudio};
+use crate::scene::model::hatch_model::{GradientKind, HatchPattern};
+use crate::ui::window::gradient_editor::{GradientColorMode, GradientEditorState, GradientMsg};
+use crate::ui::window::graphic_attributes::{
+ self as palette, GraphicAttribute, GraphicAttributesMsg, HatchEditorState, PendingFillClose,
+};
+use codec::types::{Color as AcadColor, LineWeight, Transparency};
+use codec::{EntityType, Handle};
+use iced::Task;
+use rustc_hash::{FxHashMap, FxHashSet};
+
+/// Which objects of the selection a palette control edits.
+#[derive(Clone, Copy)]
+enum Part {
+ /// Non-HATCH objects and the boundaries of selected associative fills.
+ Line,
+ /// Selected HATCH entities and the fills associated with selected objects.
+ Fill,
+ Both,
+}
+
+/// The first line pattern of the catalogue, used when a fill becomes a hatch.
+fn default_hatch_pattern() -> Option<&'static crate::scene::model::hatch_patterns::PatternEntry> {
+ crate::scene::model::hatch_patterns::catalog()
+ .iter()
+ .find(|entry| matches!(entry.gpu, HatchPattern::Pattern(_)))
+}
+
+/// Pattern name and render pattern of a new fill; `None` for no fill.
+fn fill_pattern(value: GraphicAttribute) -> Option<(String, HatchPattern)> {
+ match value {
+ GraphicAttribute::Solid => Some(("SOLID".into(), HatchPattern::Solid)),
+ GraphicAttribute::Hatch => {
+ default_hatch_pattern().map(|entry| (entry.name.clone(), entry.gpu.clone()))
+ }
+ GraphicAttribute::Gradient => Some((
+ "LINEAR".into(),
+ HatchPattern::Gradient {
+ angle_deg: 0.0,
+ color2: [0.18, 0.18, 0.18, 0.0],
+ kind: GradientKind::Linear,
+ invert: false,
+ shift: 0.0,
+ },
+ )),
+ GraphicAttribute::None | GraphicAttribute::Varies => None,
+ }
+}
+
+/// Convert a selected HATCH to another fill kind in place.
+fn set_hatch_graphic_attribute(
+ hatch: &mut codec::entities::Hatch,
+ value: GraphicAttribute,
+ visible_color: [f32; 4],
+) {
+ match value {
+ GraphicAttribute::Solid => {
+ hatch.pattern = codec::entities::HatchPattern::new("SOLID");
+ hatch.pattern_type = codec::entities::HatchPatternType::Predefined;
+ hatch.is_solid = true;
+ hatch.gradient_color.enabled = false;
+ }
+ GraphicAttribute::Hatch => {
+ let Some(entry) = default_hatch_pattern() else {
+ return;
+ };
+ let mut pattern = crate::scene::model::hatch_patterns::build_dxf_pattern(entry);
+ crate::entities::hatch::scale_pattern_geometry(
+ &mut pattern,
+ hatch.pattern_scale.max(1.0e-6),
+ );
+ crate::entities::hatch::rotate_pattern_geometry(&mut pattern, hatch.pattern_angle);
+ let origin = hatch.pattern_origin();
+ crate::entities::hatch::translate_pattern_geometry(&mut pattern, origin.x, origin.y);
+ hatch.pattern = pattern;
+ hatch.pattern_type = codec::entities::HatchPatternType::Predefined;
+ hatch.is_solid = false;
+ hatch.gradient_color.enabled = false;
+ }
+ GraphicAttribute::Gradient => {
+ let channel = |value: f32| (value * 255.0).round().clamp(0.0, 255.0) as u8;
+ hatch.is_solid = true;
+ hatch.gradient_color.enabled = true;
+ hatch.gradient_color.name = "LINEAR".into();
+ hatch.gradient_color.angle = 0.0;
+ hatch.pattern_angle = 0.0;
+ hatch.gradient_color.shift = 0.0;
+ hatch.gradient_color.is_single_color = false;
+ hatch.gradient_color.colors = vec![
+ codec::entities::hatch::GradientColorEntry {
+ value: 0.0,
+ color: AcadColor::Rgb {
+ r: channel(visible_color[0]),
+ g: channel(visible_color[1]),
+ b: channel(visible_color[2]),
+ },
+ },
+ codec::entities::hatch::GradientColorEntry {
+ value: 1.0,
+ color: AcadColor::Rgb {
+ r: 46,
+ g: 46,
+ b: 46,
+ },
+ },
+ ];
+ }
+ GraphicAttribute::None | GraphicAttribute::Varies => {}
+ }
+}
+
+fn is_open_fill_boundary(entity: &EntityType) -> bool {
+ match entity {
+ EntityType::LwPolyline(polyline) => !polyline.is_closed && polyline.vertices.len() >= 3,
+ EntityType::Polyline(polyline) => {
+ !polyline.flags.is_closed() && polyline.vertices.len() >= 3
+ }
+ EntityType::Polyline2D(polyline) => {
+ !polyline.is_closed() && polyline.vertices.len() >= 3
+ }
+ EntityType::Polyline3D(polyline) => {
+ !polyline.flags.closed && polyline.vertices.len() >= 3
+ }
+ _ => false,
+ }
+}
+
+fn close_fill_boundary(entity: &mut EntityType) -> bool {
+ match entity {
+ EntityType::LwPolyline(polyline) if !polyline.is_closed => {
+ polyline.is_closed = true;
+ true
+ }
+ EntityType::Polyline(polyline) if !polyline.flags.is_closed() => {
+ polyline.flags.set_closed(true);
+ true
+ }
+ EntityType::Polyline2D(polyline) if !polyline.is_closed() => {
+ polyline.flags.set_closed(true);
+ true
+ }
+ EntityType::Polyline3D(polyline) if !polyline.flags.closed => {
+ polyline.flags.closed = true;
+ true
+ }
+ _ => false,
+ }
+}
+
+/// Line attributes with ByLayer / ByBlock resolved to the values they show.
+struct ConcreteGraphicAttributes {
+ color: AcadColor,
+ linetype: String,
+ lineweight: LineWeight,
+ transparency: Transparency,
+}
+
+fn concrete_graphic_attributes(
+ color: AcadColor,
+ linetype: &str,
+ lineweight: LineWeight,
+ transparency: Transparency,
+ layer: Option<&codec::tables::Layer>,
+) -> ConcreteGraphicAttributes {
+ let layer_color = layer.map_or(AcadColor::Index(7), |layer| match layer.color {
+ AcadColor::ByLayer | AcadColor::ByBlock => AcadColor::Index(7),
+ color => color,
+ });
+ let layer_linetype = layer
+ .map(|layer| layer.line_type.as_str())
+ .filter(|value| {
+ !value.is_empty()
+ && !value.eq_ignore_ascii_case("ByLayer")
+ && !value.eq_ignore_ascii_case("ByBlock")
+ })
+ .unwrap_or("Continuous");
+ let layer_lineweight = layer.map_or(LineWeight::Default, |layer| match layer.line_weight {
+ LineWeight::ByLayer | LineWeight::ByBlock => LineWeight::Default,
+ value => value,
+ });
+ let layer_transparency = layer
+ .map(|layer| layer.transparency)
+ .filter(|value| !value.is_by_layer() && !value.is_by_block())
+ .unwrap_or_else(|| Transparency::from_percent(0.0));
+
+ ConcreteGraphicAttributes {
+ color: match color {
+ AcadColor::ByLayer => layer_color,
+ AcadColor::ByBlock => AcadColor::Index(7),
+ value => value,
+ },
+ linetype: if linetype.is_empty() || linetype.eq_ignore_ascii_case("ByLayer") {
+ layer_linetype.to_owned()
+ } else if linetype.eq_ignore_ascii_case("ByBlock") {
+ "Continuous".to_owned()
+ } else {
+ linetype.to_owned()
+ },
+ lineweight: match lineweight {
+ LineWeight::ByLayer => layer_lineweight,
+ LineWeight::ByBlock => LineWeight::Default,
+ value => value,
+ },
+ transparency: if transparency.is_by_layer() {
+ layer_transparency
+ } else if transparency.is_by_block() {
+ Transparency::from_percent(0.0)
+ } else {
+ transparency
+ },
+ }
+}
+
+impl OpenCADStudio {
+ pub(super) fn on_graphic_attributes(&mut self, message: GraphicAttributesMsg) -> Task {
+ use GraphicAttributesMsg as M;
+ if !matches!(
+ message,
+ M::ToggleMenu(_)
+ | M::HatchPatternSearch(_)
+ | M::HatchPatternFocus(_)
+ | M::LineLinetypeScaleInput(_)
+ | M::Gradient(_)
+ ) {
+ self.graphic_attributes.open_menu = None;
+ }
+ match message {
+ M::ToggleMenu(menu) => {
+ let state = &mut self.graphic_attributes;
+ state.open_menu = (state.open_menu != Some(menu)).then_some(menu);
+ Task::none()
+ }
+ M::CloseMenu => Task::none(),
+ M::SetAllByLayer => self.on_graphic_attributes_set_all(true),
+ M::SetAllByBlock => self.on_graphic_attributes_set_all(false),
+ M::RemoveReferences => self.on_graphic_attributes_remove_references(),
+ M::ToggleFillOnCurrentLayer => {
+ self.graphic_attributes.fill_on_current_layer ^= true;
+ self.save_config();
+ Task::none()
+ }
+ M::CreateLayer => {
+ use crate::command::CadCommand;
+ let command = crate::command::FreeTextValuePromptCommand::new(
+ "GRAPHICATTRIBUTELAYER",
+ "Enter a name for the new layer:",
+ );
+ self.command_line.push_info(&command.prompt());
+ self.tabs[self.active_tab].active_cmd = Some(Box::new(command));
+ self.focus_cmd_input()
+ }
+ M::Fill(value) => self.on_graphic_attribute(value),
+ M::LineColor(color) => self.on_line_color(color),
+ M::LineLinetype(linetype) => self.on_line_linetype(linetype),
+ M::LineLineweight(lineweight) => self.on_line_lineweight(lineweight),
+ M::LineLinetypeScale(scale) => self.on_line_linetype_scale(scale),
+ M::LineLinetypeScaleInput(value) => {
+ self.graphic_attributes.linetype_scale_input = Some(value);
+ Task::none()
+ }
+ M::LineLinetypeScaleSubmit => {
+ match self.graphic_attributes.linetype_scale_input.take() {
+ Some(typed) => {
+ let value = crate::app::expr_eval::eval_number(&typed.replace(',', "."));
+ value.map_or_else(Task::none, |scale| self.on_line_linetype_scale(scale))
+ }
+ None => Task::none(),
+ }
+ }
+ M::LineTransparency(transparency) => self.on_line_transparency(transparency),
+ M::FillColor(color) => self.on_solid_fill_color(color),
+ M::FillTransparency(transparency) => self.on_fill_transparency(transparency),
+ M::HatchEditorOpen => self.open_hatch_editor(),
+ M::HatchEditorClose => {
+ self.graphic_attributes.hatch_editor = None;
+ Task::none()
+ }
+ M::HatchPatternSearch(search) => {
+ if let Some(editor) = &mut self.graphic_attributes.hatch_editor {
+ editor.search = search;
+ editor.focus = 0;
+ }
+ Task::none()
+ }
+ M::HatchPatternFocus(index) => {
+ if let Some(editor) = &mut self.graphic_attributes.hatch_editor {
+ if index < crate::ui::properties::filtered_hatch_patterns(&editor.search).len()
+ {
+ editor.focus = index;
+ }
+ }
+ Task::none()
+ }
+ M::HatchPattern(name) => self.apply_graphic_hatch_pattern(name),
+ M::HatchPatternConfirm => {
+ let name = self.graphic_attributes.hatch_editor.as_ref().and_then(|editor| {
+ crate::ui::properties::filtered_hatch_patterns(&editor.search)
+ .get(editor.focus)
+ .map(|entry| entry.name.clone())
+ });
+ match name {
+ Some(name) => self.apply_graphic_hatch_pattern(name),
+ None => Task::none(),
+ }
+ }
+ M::Gradient(message) => self.on_gradient_editor(message),
+ }
+ }
+
+ /// Prompt answers belong to the fill question only while no command runs.
+ pub(super) fn awaiting_fill_close_answer(&self) -> bool {
+ self.graphic_attributes.pending_fill_close.is_some()
+ && self.tabs[self.active_tab].active_cmd.is_none()
+ }
+
+ /// Answer the "close open boundaries first?" prompt; Enter means Yes.
+ pub(super) fn on_graphic_fill_close_input(&mut self, input: &str) -> Task {
+ match input.trim().to_ascii_uppercase().as_str() {
+ "" | "Y" | "YES" => self.on_graphic_fill_close_response(true),
+ "N" | "NO" => self.on_graphic_fill_close_response(false),
+ _ => {
+ self.command_line.push_error(crate::t!("Enter Yes or No.").as_ref());
+ Task::none()
+ }
+ }
+ }
+
+ fn graphic_handles(&self, i: usize, part: Part) -> Vec {
+ let selected = self.tabs[i].scene.selected_handles_in_order();
+ let document = &self.tabs[i].scene.document;
+ match part {
+ Part::Line => palette::line_handles(document, &selected),
+ Part::Fill => palette::fill_handles(document, &selected),
+ Part::Both => {
+ let mut seen = FxHashSet::default();
+ palette::line_handles(document, &selected)
+ .into_iter()
+ .chain(palette::fill_handles(document, &selected))
+ .filter(|handle| seen.insert(*handle))
+ .collect()
+ }
+ }
+ }
+
+ fn unlocked_graphic_handles(&self, i: usize, part: Part) -> Vec {
+ let mut handles = self.graphic_handles(i, part);
+ handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
+ handles
+ }
+
+ /// Edit the unlocked `part` of the selection as one undo step.
+ fn edit_graphic_part(&mut self, part: Part, label: &str, mut edit: impl FnMut(&mut EntityType)) {
+ let i = self.active_tab;
+ let handles = self.unlocked_graphic_handles(i, part);
+ self.apply_property_op(i, label, &handles, |app, handle| {
+ if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) {
+ edit(entity);
+ }
+ });
+ }
+
+ fn has_selection(&self) -> bool {
+ !self.tabs[self.active_tab].scene.selected_handles_in_order().is_empty()
+ }
+
+ fn current_graphic_attributes(&self, i: usize) -> ConcreteGraphicAttributes {
+ let document = &self.tabs[i].scene.document;
+ let handles = self.graphic_handles(i, Part::Both);
+ let draw_depth = self.tabs[i].scene.draw_depth_map();
+ if let Some(entity) =
+ palette::top_handle(&handles, &draw_depth).and_then(|handle| document.get_entity(handle))
+ {
+ let common = entity.common();
+ return concrete_graphic_attributes(
+ common.color,
+ &common.linetype,
+ common.line_weight,
+ common.transparency,
+ document.layers.get(&common.layer),
+ );
+ }
+
+ let header = &document.header;
+ let layer_name = if header.current_layer_name.is_empty() {
+ self.tabs[i].active_layer.as_str()
+ } else {
+ header.current_layer_name.as_str()
+ };
+ concrete_graphic_attributes(
+ header.current_entity_color,
+ &header.current_linetype_name,
+ LineWeight::from_value(header.current_line_weight),
+ document.current_entity_transparency(),
+ document.layers.get(layer_name),
+ )
+ }
+
+ /// Store line attributes as the creation defaults (CECOLOR, CELTYPE,
+ /// CELWEIGHT, CETRANSPARENCY).
+ fn set_current_graphic_attributes(
+ &mut self,
+ color: AcadColor,
+ linetype: String,
+ lineweight: LineWeight,
+ transparency: Transparency,
+ ) {
+ let i = self.active_tab;
+ let linetype_handle = self.tabs[i]
+ .scene
+ .document
+ .line_types
+ .iter()
+ .find(|line_type| line_type.name.eq_ignore_ascii_case(&linetype))
+ .map_or(Handle::NULL, |line_type| line_type.handle);
+ let header = &mut self.tabs[i].scene.document.header;
+ header.current_entity_color = color;
+ header.current_linetype_name = linetype;
+ header.current_linetype_handle = linetype_handle;
+ header.current_line_weight = lineweight.value();
+ self.tabs[i]
+ .scene
+ .document
+ .set_current_entity_transparency(transparency);
+ self.tabs[i].dirty = true;
+ self.sync_ribbon_from_selection();
+ self.refresh_properties();
+ }
+
+ pub(super) fn on_graphic_attributes_set_all(&mut self, by_layer: bool) -> Task {
+ let (color, linetype, lineweight, transparency) = if by_layer {
+ (AcadColor::ByLayer, "ByLayer", LineWeight::ByLayer, Transparency::BY_LAYER)
+ } else {
+ (AcadColor::ByBlock, "ByBlock", LineWeight::ByBlock, Transparency::BY_BLOCK)
+ };
+ if !self.has_selection() {
+ self.set_current_graphic_attributes(color, linetype.to_owned(), lineweight, transparency);
+ return Task::none();
+ }
+ self.edit_graphic_part(Part::Both, &crate::t!("Graphic Attributes"), |entity| {
+ crate::scene::view::dispatch::apply_color(entity, color);
+ crate::scene::view::dispatch::apply_common_prop(entity, "linetype", linetype);
+ crate::scene::view::dispatch::apply_line_weight(entity, lineweight);
+ entity.common_mut().transparency = transparency;
+ });
+ Task::none()
+ }
+
+ pub(super) fn on_graphic_attributes_remove_references(&mut self) -> Task {
+ let i = self.active_tab;
+ if !self.has_selection() {
+ let values = self.current_graphic_attributes(i);
+ self.set_current_graphic_attributes(
+ values.color,
+ values.linetype,
+ values.lineweight,
+ values.transparency,
+ );
+ return Task::none();
+ }
+
+ let document = &self.tabs[i].scene.document;
+ let values: FxHashMap<_, _> = self
+ .unlocked_graphic_handles(i, Part::Both)
+ .into_iter()
+ .filter_map(|handle| {
+ let common = document.get_entity(handle)?.common();
+ Some((
+ handle,
+ concrete_graphic_attributes(
+ common.color,
+ &common.linetype,
+ common.line_weight,
+ common.transparency,
+ document.layers.get(&common.layer),
+ ),
+ ))
+ })
+ .collect();
+ let handles: Vec<_> = values.keys().copied().collect();
+ self.apply_property_op(i, crate::t!("Graphic Attributes"), &handles, |app, handle| {
+ let (Some(values), Some(entity)) = (
+ values.get(&handle),
+ app.tabs[i].scene.document.get_entity_mut(handle),
+ ) else {
+ return;
+ };
+ crate::scene::view::dispatch::apply_color(entity, values.color);
+ crate::scene::view::dispatch::apply_common_prop(entity, "linetype", &values.linetype);
+ crate::scene::view::dispatch::apply_line_weight(entity, values.lineweight);
+ entity.common_mut().transparency = values.transparency;
+ });
+ Task::none()
+ }
+
+ pub(crate) fn create_graphic_attributes_layer(&mut self, raw_name: &str) -> Task {
+ let i = self.active_tab;
+ let name = raw_name.trim();
+ if !crate::scene::valid_block_name(name) {
+ self.command_line
+ .push_error(crate::t!("Graphic Attributes: enter a valid layer name without <>/\\\":;?*|,=`.").as_ref());
+ return Task::none();
+ }
+ if self.tabs[i]
+ .scene
+ .document
+ .layers
+ .iter()
+ .any(|layer| layer.name.eq_ignore_ascii_case(name))
+ {
+ self.command_line.push_error(
+ crate::tf!("Graphic Attributes: layer \"{name}\" already exists.").as_ref(),
+ );
+ return Task::none();
+ }
+
+ let values = self.current_graphic_attributes(i);
+ let layer_name = name.to_owned();
+ let undo = self.begin_layer_undo(
+ i,
+ crate::t!("Create Layer with active settings"),
+ std::slice::from_ref(&layer_name),
+ );
+ let mut layer = codec::tables::Layer::new(name);
+ layer.handle = self.tabs[i].scene.document.allocate_handle();
+ layer.color = values.color;
+ layer.line_type = values.linetype;
+ layer.line_weight = values.lineweight;
+ layer.transparency = values.transparency;
+ if self.tabs[i].scene.document.layers.add(layer).is_err() {
+ self.command_line
+ .push_error(crate::t!("Graphic Attributes: the layer could not be created.").as_ref());
+ return Task::none();
+ }
+ self.tabs[i].dirty = true;
+ self.commit_layer_undo(i, undo);
+ self.refresh_layer_panel();
+ self.command_line.push_output(
+ crate::tf!("Created layer \"{name}\" with the active Graphic Attributes settings.")
+ .as_ref(),
+ );
+ Task::none()
+ }
+
+ fn open_hatch_editor(&mut self) -> Task {
+ self.graphic_attributes.gradient_editor = None;
+ let i = self.active_tab;
+ let document = &self.tabs[i].scene.document;
+ let selected = self.tabs[i].scene.selected_handles_in_order();
+ let current = palette::fill_handles(document, &selected)
+ .into_iter()
+ .find_map(|handle| match document.get_entity(handle)? {
+ EntityType::Hatch(hatch) => Some(hatch.pattern.name.as_str()),
+ _ => None,
+ });
+ let focus = current
+ .and_then(|current| {
+ crate::ui::properties::filtered_hatch_patterns("")
+ .iter()
+ .position(|entry| entry.name.eq_ignore_ascii_case(current))
+ })
+ .unwrap_or(0);
+ self.graphic_attributes.hatch_editor = Some(HatchEditorState {
+ handles: selected,
+ search: String::new(),
+ focus,
+ });
+ iced::widget::operation::focus(iced::widget::Id::new("graphic-hatch-pattern-search"))
+ }
+
+ fn apply_graphic_hatch_pattern(&mut self, name: String) -> Task {
+ self.graphic_attributes.hatch_editor = None;
+ let i = self.active_tab;
+ let handles = self.graphic_handles(i, Part::Fill);
+ self.apply_hatch_pattern(i, &handles, name)
+ }
+
+ fn on_gradient_editor(&mut self, message: GradientMsg) -> Task {
+ match message {
+ GradientMsg::Open => {
+ self.graphic_attributes.hatch_editor = None;
+ self.open_gradient_editor();
+ }
+ GradientMsg::Cancel => self.graphic_attributes.gradient_editor = None,
+ GradientMsg::Apply => return self.apply_gradient_editor(),
+ message => {
+ if let Some(editor) = &mut self.graphic_attributes.gradient_editor {
+ editor.update(message);
+ }
+ }
+ }
+ Task::none()
+ }
+
+ pub(super) fn open_gradient_editor(&mut self) {
+ let i = self.active_tab;
+ let document = &self.tabs[i].scene.document;
+ let selected = self.tabs[i].scene.selected_handles_in_order();
+ let gradient = palette::fill_handles(document, &selected)
+ .into_iter()
+ .find_map(|handle| match document.get_entity(handle)? {
+ EntityType::Hatch(hatch) if hatch.gradient_color.enabled => Some(hatch),
+ _ => None,
+ });
+ if let Some(hatch) = gradient {
+ self.graphic_attributes.gradient_editor =
+ Some(GradientEditorState::from_hatch(selected, hatch));
+ }
+ }
+
+ pub(super) fn apply_gradient_editor(&mut self) -> Task {
+ let Some(state) = self.graphic_attributes.gradient_editor.take() else {
+ return Task::none();
+ };
+ let i = self.active_tab;
+ let document = &self.tabs[i].scene.document;
+ let targets: Vec<_> = palette::fill_handles(document, &state.handles)
+ .into_iter()
+ .filter(|handle| {
+ matches!(document.get_entity(*handle),
+ Some(EntityType::Hatch(hatch)) if hatch.gradient_color.enabled)
+ })
+ .collect();
+ if targets.is_empty() {
+ return Task::none();
+ }
+ if targets
+ .iter()
+ .any(|handle| self.tabs[i].scene.is_layer_locked(*handle))
+ {
+ self.command_line
+ .push_error(crate::t!("Gradient Fill: the fill is on a locked layer.").as_ref());
+ self.graphic_attributes.gradient_editor = Some(state);
+ return Task::none();
+ }
+ let angle = state.angle_radians();
+ let pending = self.begin_undo(i, crate::t!("Edit gradient"), targets.len(), true);
+ for handle in &targets {
+ let Some(mut entity) = self.tabs[i].scene.document.get_entity(*handle).cloned() else {
+ continue;
+ };
+ if let EntityType::Hatch(hatch) = &mut entity {
+ let gradient = &mut hatch.gradient_color;
+ gradient.name = state.kind.dxf_name(state.inverted).into();
+ gradient.angle = angle;
+ gradient.shift = if state.centered { 0.0 } else { 1.0 };
+ gradient.is_single_color = state.color_mode == GradientColorMode::One;
+ gradient.color_tint = state.shade_tint as f64;
+ gradient.colors = vec![
+ codec::entities::hatch::GradientColorEntry {
+ value: 0.0,
+ color: state.color_1,
+ },
+ codec::entities::hatch::GradientColorEntry {
+ value: 1.0,
+ color: state.color_2,
+ },
+ ];
+ hatch.pattern_angle = angle;
+ }
+ self.tabs[i].scene.update_entity(entity);
+ }
+ self.tabs[i].dirty = true;
+ self.refresh_properties();
+ if let Some(pending) = pending {
+ self.commit_undo_delta(i, pending);
+ }
+ Task::none()
+ }
+
+ /// Give every selected object the chosen fill. Open polylines first ask
+ /// whether they should be closed.
+ pub(super) fn on_graphic_attribute(&mut self, value: GraphicAttribute) -> Task {
+ self.graphic_attributes.close_popups();
+ self.graphic_attributes.pending_fill_close = None;
+ if value == GraphicAttribute::Varies {
+ return Task::none();
+ }
+ let i = self.active_tab;
+ let selected = self.tabs[i].scene.selected_handles_in_order();
+ if selected.is_empty() {
+ self.command_line
+ .push_info(crate::t!("Select one or more closed objects first.").as_ref());
+ return Task::none();
+ }
+ if value != GraphicAttribute::None {
+ let open_boundaries: Vec<_> = selected
+ .iter()
+ .copied()
+ .filter(|handle| {
+ !self.tabs[i].scene.is_layer_locked(*handle)
+ && self.tabs[i]
+ .scene
+ .document
+ .get_entity(*handle)
+ .is_some_and(is_open_fill_boundary)
+ })
+ .collect();
+ if !open_boundaries.is_empty() {
+ self.graphic_attributes.pending_fill_close = Some(PendingFillClose {
+ fill: value,
+ open_boundaries,
+ selection: selected,
+ });
+ return Task::none();
+ }
+ }
+ self.apply_graphic_fill(i, value, &selected, &[])
+ }
+
+ pub(super) fn on_graphic_fill_close_response(&mut self, close: bool) -> Task {
+ let Some(pending) = self.graphic_attributes.pending_fill_close.take() else {
+ return Task::none();
+ };
+ let i = self.active_tab;
+ let to_close: Vec<_> = if close {
+ pending
+ .open_boundaries
+ .into_iter()
+ .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
+ .collect()
+ } else {
+ Vec::new()
+ };
+ self.apply_graphic_fill(i, pending.fill, &pending.selection, &to_close)
+ }
+
+ /// Close `to_close` and replace the fills of `selected`, as one undo step.
+ fn apply_graphic_fill(
+ &mut self,
+ i: usize,
+ value: GraphicAttribute,
+ selected: &[Handle],
+ to_close: &[Handle],
+ ) -> Task {
+ let (selected, locked): (Vec, Vec) = selected
+ .iter()
+ .partition(|handle| !self.tabs[i].scene.is_layer_locked(**handle));
+ if !locked.is_empty() {
+ self.command_line.push_info(
+ crate::tf!(
+ "Graphic Attributes: {} object(s) on a locked layer were not changed.",
+ locked.len()
+ )
+ .as_ref(),
+ );
+ }
+ let selected = selected.as_slice();
+ let pending = self.begin_undo(i, crate::t!("Fill change"), selected.len(), true);
+ let mut changed = false;
+ for handle in to_close {
+ let Some(mut entity) = self.tabs[i].scene.document.get_entity(*handle).cloned() else {
+ continue;
+ };
+ if close_fill_boundary(&mut entity) {
+ self.tabs[i].scene.update_entity(entity);
+ changed = true;
+ }
+ }
+ changed |= self.replace_graphic_fills(i, value, selected);
+ if changed {
+ self.tabs[i].dirty = true;
+ self.refresh_properties();
+ }
+ if let Some(pending) = pending {
+ self.commit_undo_delta(i, pending);
+ }
+ Task::none()
+ }
+
+ /// Replace the fills of `selected` by one associative fill per object.
+ /// Returns whether the document changed.
+ fn replace_graphic_fills(
+ &mut self,
+ i: usize,
+ value: GraphicAttribute,
+ selected: &[Handle],
+ ) -> bool {
+ let document = &self.tabs[i].scene.document;
+ let (associated, new_hatches, updated_hatches, skipped) = if value == GraphicAttribute::None
+ {
+ (palette::fill_handles(document, selected), Vec::new(), Vec::new(), 0)
+ } else {
+ let Some((name, pattern)) = fill_pattern(value) else {
+ self.command_line
+ .push_error(crate::t!("Graphic Attributes: no hatch pattern is available.").as_ref());
+ return false;
+ };
+ let working = if self.tabs[i].editing_model_space() {
+ self.tabs[i].ucs_xform().working_plane()
+ } else {
+ crate::command::WorkingPlane::default()
+ };
+ let normal = working.z.normalize_or(glam::DVec3::Z);
+ let storage = crate::entities::curve::ocs_plane(
+ codec::types::Vector3::new(normal.x, normal.y, normal.z),
+ working.origin.dot(normal),
+ );
+ let plane = crate::command::WorkingPlane::new(
+ glam::DVec3::from_array(storage.origin),
+ glam::DVec3::from_array(storage.x_axis),
+ glam::DVec3::from_array(storage.y_axis),
+ );
+ let sources = self.tabs[i].scene.boundary_sources_on_plane(plane, 1.0e-6);
+ let origin = document.hatch_origin();
+ let mut new_hatches = Vec::new();
+ let mut updated_hatches = Vec::new();
+ let mut directly_updated = FxHashSet::default();
+ let mut changed_boundaries = FxHashSet::default();
+ let mut skipped = 0;
+ for handle in selected {
+ let Some(entity) = document.get_entity(*handle) else {
+ continue;
+ };
+ if let EntityType::Hatch(hatch) = entity {
+ let mut hatch = hatch.clone();
+ let visible_color =
+ crate::scene::view::render::render_style_for_common_viewport(
+ document,
+ &hatch.common,
+ None,
+ )
+ .0;
+ set_hatch_graphic_attribute(&mut hatch, value, visible_color);
+ updated_hatches.push(EntityType::Hatch(hatch));
+ directly_updated.insert(*handle);
+ continue;
+ }
+ // Object mode only needs this object's source. Including all
+ // scene sources can associate coincident, unselected objects.
+ let object_sources = sources
+ .get(handle)
+ .map(|source| (*handle, source.clone()))
+ .into_iter()
+ .collect();
+ let command = crate::modules::draw::draw::hatch::HatchCommand::new(
+ Vec::new(),
+ object_sources,
+ vec![*handle],
+ None,
+ plane,
+ )
+ .with_origin(origin)
+ .with_pattern(name.clone(), pattern.clone());
+ if let crate::command::CmdResult::CommitHatch(hatch) = command.finish_selected() {
+ let layer = if self.graphic_attributes.fill_on_current_layer {
+ self.tabs[i].active_layer.clone()
+ } else {
+ entity.common().layer.clone()
+ };
+ new_hatches.push((hatch, layer));
+ changed_boundaries.insert(*handle);
+ } else if !is_open_fill_boundary(entity) {
+ // Open boundaries the user chose not to close are expected
+ // to stay unfilled; anything else is unsupported.
+ skipped += 1;
+ }
+ }
+ // Replace only fills belonging to boundaries that successfully
+ // produced a new hatch. Existing fills on skipped objects remain.
+ let associated: Vec<_> = document
+ .entities()
+ .filter_map(|entity| {
+ let EntityType::Hatch(hatch) = entity else {
+ return None;
+ };
+ (hatch.is_associative
+ && !directly_updated.contains(&entity.common().handle)
+ && hatch
+ .paths
+ .iter()
+ .flat_map(|path| &path.boundary_handles)
+ .any(|handle| changed_boundaries.contains(handle)))
+ .then_some(entity.common().handle)
+ })
+ .collect();
+ (associated, new_hatches, updated_hatches, skipped)
+ };
+ if associated
+ .iter()
+ .chain(updated_hatches.iter().map(|entity| &entity.common().handle))
+ .any(|handle| self.tabs[i].scene.is_layer_locked(*handle))
+ {
+ self.command_line
+ .push_error(crate::t!("Graphic Attributes: an associated fill is on a locked layer.").as_ref());
+ return false;
+ }
+ if skipped == 1 {
+ self.command_line
+ .push_info(crate::t!("Graphic Attributes: 1 unsupported object was not changed.").as_ref());
+ } else if skipped > 1 {
+ self.command_line.push_info(
+ crate::tf!("Graphic Attributes: {skipped} unsupported objects were not changed.")
+ .as_ref(),
+ );
+ }
+ if associated.is_empty() && new_hatches.is_empty() && updated_hatches.is_empty() {
+ return false;
+ }
+ self.tabs[i].scene.erase_entities(&associated);
+ for entity in updated_hatches {
+ self.tabs[i].scene.update_entity(entity);
+ }
+ for (hatch, layer) in new_hatches {
+ self.tabs[i].scene.add_hatch(hatch, Some(&layer), None);
+ }
+ true
+ }
+
+ pub(super) fn on_solid_fill_color(&mut self, color: AcadColor) -> Task {
+ self.edit_graphic_part(Part::Fill, &crate::t!("Fill color"), |entity| {
+ crate::scene::view::dispatch::apply_color(entity, color);
+ });
+ Task::none()
+ }
+
+ pub(super) fn on_fill_transparency(&mut self, transparency: Transparency) -> Task {
+ self.edit_graphic_part(Part::Fill, &crate::t!("Fill transparency"), |entity| {
+ entity.common_mut().transparency = transparency;
+ });
+ Task::none()
+ }
+
+ pub(super) fn on_line_color(&mut self, color: AcadColor) -> Task {
+ if !self.has_selection() {
+ return self.on_ribbon_color_changed(color);
+ }
+ self.edit_graphic_part(Part::Line, &crate::t!("Line color"), |entity| {
+ crate::scene::view::dispatch::apply_color(entity, color);
+ });
+ Task::none()
+ }
+
+ pub(super) fn on_line_linetype(&mut self, linetype: String) -> Task {
+ if !self.has_selection() {
+ return self.on_ribbon_linetype_changed(linetype);
+ }
+ self.edit_graphic_part(Part::Line, &crate::t!("Linetype"), |entity| {
+ crate::scene::view::dispatch::apply_common_prop(entity, "linetype", &linetype);
+ });
+ Task::none()
+ }
+
+ pub(super) fn on_line_lineweight(&mut self, lineweight: LineWeight) -> Task {
+ if !self.has_selection() {
+ return self.on_ribbon_lineweight_changed(lineweight);
+ }
+ self.edit_graphic_part(Part::Line, &crate::t!("Line weight"), |entity| {
+ crate::scene::view::dispatch::apply_line_weight(entity, lineweight);
+ });
+ Task::none()
+ }
+
+ pub(super) fn on_line_linetype_scale(&mut self, scale: f64) -> Task {
+ let Some(scale) = palette::normalized_linetype_scale(scale) else {
+ return Task::none();
+ };
+ if !self.has_selection() {
+ let i = self.active_tab;
+ self.tabs[i].scene.document.header.current_entity_linetype_scale = scale;
+ self.tabs[i].dirty = true;
+ self.refresh_properties();
+ return Task::none();
+ }
+ self.edit_graphic_part(Part::Line, &crate::t!("Linetype scale"), |entity| {
+ entity.common_mut().linetype_scale = scale;
+ });
+ Task::none()
+ }
+
+ pub(super) fn on_line_transparency(&mut self, transparency: Transparency) -> Task {
+ if !self.has_selection() {
+ let i = self.active_tab;
+ if self.tabs[i]
+ .scene
+ .document
+ .set_current_entity_transparency(transparency)
+ {
+ self.tabs[i].dirty = true;
+ self.refresh_properties();
+ }
+ return Task::none();
+ }
+ self.edit_graphic_part(Part::Line, &crate::t!("Line transparency"), |entity| {
+ entity.common_mut().transparency = transparency;
+ });
+ Task::none()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use codec::entities::{Line, LwPolyline};
+ use codec::types::Vector2;
+
+ fn ga(message: GraphicAttributesMsg) -> Message {
+ Message::GraphicAttributes(message)
+ }
+
+ fn fresh() -> OpenCADStudio {
+ let mut app = OpenCADStudio::new_for_test();
+ app.automation_op(r#"{"op":"new"}"#);
+ app
+ }
+
+ fn add_closed_square(app: &mut OpenCADStudio, x: f64) -> Handle {
+ let mut polyline = LwPolyline::new();
+ for (px, py) in [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)] {
+ polyline.add_point(Vector2::new(px + x, py));
+ }
+ polyline.close();
+ let i = app.active_tab;
+ app.tabs[i].scene.add_entity(EntityType::LwPolyline(polyline))
+ }
+
+ fn select(app: &mut OpenCADStudio, handles: &[Handle]) {
+ let i = app.active_tab;
+ app.tabs[i].scene.deselect_all();
+ for handle in handles {
+ app.tabs[i].scene.select_entity(*handle, false);
+ }
+ }
+
+ fn hatches(app: &OpenCADStudio) -> Vec<&codec::entities::Hatch> {
+ app.tabs[app.active_tab]
+ .scene
+ .document
+ .entities()
+ .filter_map(|entity| match entity {
+ EntityType::Hatch(hatch) => Some(hatch),
+ _ => None,
+ })
+ .collect()
+ }
+
+ fn common(app: &OpenCADStudio, handle: Handle) -> &codec::entities::EntityCommon {
+ app.tabs[app.active_tab]
+ .scene
+ .document
+ .get_entity(handle)
+ .expect("entity")
+ .common()
+ }
+
+ fn fill_kind(app: &OpenCADStudio, handle: Handle) -> GraphicAttribute {
+ palette::current(&app.tabs[app.active_tab].scene.document, &[handle])
+ }
+
+ /// A closed square with a fill of `kind`; the fill HATCH is selected.
+ fn filled_square(app: &mut OpenCADStudio, kind: GraphicAttribute) -> (Handle, Handle) {
+ let boundary = add_closed_square(app, 0.0);
+ select(app, &[boundary]);
+ let _ = app.on_graphic_attribute(kind);
+ let hatch = hatches(app)[0].common.handle;
+ select(app, &[hatch]);
+ (boundary, hatch)
+ }
+
+ #[test]
+ fn coincident_boundaries_each_get_their_own_associative_fill() {
+ let mut app = fresh();
+ let boundaries = [add_closed_square(&mut app, 0.0), add_closed_square(&mut app, 0.0)];
+ select(&mut app, &boundaries);
+
+ let _ = app.on_graphic_attribute(GraphicAttribute::Solid);
+ let fills = hatches(&app);
+ assert_eq!(fills.len(), 2);
+ assert!(fills.iter().all(|hatch| hatch.is_solid && hatch.is_associative));
+ assert!(fills.iter().all(|hatch| hatch
+ .paths
+ .iter()
+ .flat_map(|path| &path.boundary_handles)
+ .count()
+ == 1));
+
+ select(&mut app, &boundaries[..1]);
+ let _ = app.on_graphic_attribute(GraphicAttribute::Gradient);
+ assert_eq!(fill_kind(&app, boundaries[0]), GraphicAttribute::Gradient);
+ assert_eq!(fill_kind(&app, boundaries[1]), GraphicAttribute::Solid);
+ assert_eq!(hatches(&app).len(), 2);
+ }
+
+ #[test]
+ fn unsupported_objects_are_skipped_while_the_rest_is_filled() {
+ let mut app = fresh();
+ let i = app.active_tab;
+ let boundaries = [add_closed_square(&mut app, 0.0), add_closed_square(&mut app, 20.0)];
+ select(&mut app, &boundaries);
+ let _ = app.on_graphic_attribute(GraphicAttribute::Gradient);
+
+ let line = app.tabs[i].scene.add_entity(EntityType::Line(Line::new()));
+ select(&mut app, &[boundaries[0], line]);
+ let _ = app.on_graphic_attribute(GraphicAttribute::Solid);
+ assert_eq!(fill_kind(&app, boundaries[0]), GraphicAttribute::Solid);
+ assert_eq!(fill_kind(&app, boundaries[1]), GraphicAttribute::Gradient);
+ assert_eq!(fill_kind(&app, line), GraphicAttribute::None);
+ assert_eq!(hatches(&app).len(), 2);
+ }
+
+ #[test]
+ fn editing_a_shared_gradient_is_one_undo_step() {
+ let mut app = fresh();
+ let i = app.active_tab;
+ let boundaries = [add_closed_square(&mut app, 0.0), add_closed_square(&mut app, 20.0)];
+ select(&mut app, &boundaries);
+ let _ = app.on_graphic_attribute(GraphicAttribute::Gradient);
+
+ app.open_gradient_editor();
+ let editor = app
+ .graphic_attributes
+ .gradient_editor
+ .as_mut()
+ .expect("gradient editor");
+ assert_eq!(editor.handles.len(), 2);
+ editor.kind = crate::scene::model::hatch_model::GradientKind::Curved;
+ editor.inverted = true;
+ editor.angle = "405°".into();
+ editor.centered = false;
+ let undo_before = app.tabs[i].history.undo_stack.len();
+ let _ = app.apply_gradient_editor();
+ assert_eq!(app.tabs[i].history.undo_stack.len(), undo_before + 1);
+
+ let gradients = hatches(&app);
+ assert_eq!(gradients.len(), 2);
+ assert!(gradients.iter().all(|hatch| {
+ hatch.gradient_color.name == "INVCURVED"
+ && (hatch.gradient_color.angle.to_degrees() - 45.0).abs() < 1.0e-9
+ && (hatch.gradient_color.shift - 1.0).abs() < 1.0e-9
+ }));
+ }
+
+ #[test]
+ fn gradient_editor_closes_when_the_selection_changes() {
+ let mut app = fresh();
+ filled_square(&mut app, GraphicAttribute::Gradient);
+ app.open_gradient_editor();
+ assert!(app.graphic_attributes.gradient_editor.is_some());
+
+ app.tabs[app.active_tab].scene.deselect_all();
+ let _ = app.update(ga(GraphicAttributesMsg::CloseMenu));
+ assert!(app.graphic_attributes.gradient_editor.is_none());
+ }
+
+ #[test]
+ fn a_selected_hatch_is_converted_in_place() {
+ let mut app = fresh();
+ let (boundary, hatch) = filled_square(&mut app, GraphicAttribute::Gradient);
+ let document = &app.tabs[app.active_tab].scene.document;
+ assert_eq!(palette::line_handles(document, &[hatch]), vec![boundary]);
+ assert_eq!(fill_kind(&app, hatch), GraphicAttribute::Gradient);
+
+ for kind in [GraphicAttribute::Solid, GraphicAttribute::Hatch, GraphicAttribute::Gradient] {
+ let _ = app.on_graphic_attribute(kind);
+ assert_eq!(fill_kind(&app, hatch), kind);
+ }
+ assert_eq!(hatches(&app).len(), 1);
+
+ let pattern = default_hatch_pattern().expect("pattern hatch").name.clone();
+ let _ = app.update(ga(GraphicAttributesMsg::HatchPattern(pattern.clone())));
+ assert_eq!(hatches(&app)[0].pattern.name, pattern);
+ }
+
+ #[test]
+ fn line_and_fill_controls_only_edit_their_own_part() {
+ let mut app = fresh();
+ let i = app.active_tab;
+ let (_, hatch) = filled_square(&mut app, GraphicAttribute::Solid);
+ let line = app.tabs[i].scene.add_entity(EntityType::Line(Line::new()));
+ select(&mut app, &[hatch, line]);
+
+ let _ = app.on_solid_fill_color(AcadColor::Index(3));
+ let _ = app.on_line_color(AcadColor::Index(4));
+ assert_eq!(common(&app, hatch).color, AcadColor::Index(3));
+ assert_eq!(common(&app, line).color, AcadColor::Index(4));
+
+ let _ = app.on_line_linetype("ByBlock".to_string());
+ assert_eq!(common(&app, line).linetype, "ByBlock");
+
+ let _ = app.on_line_lineweight(LineWeight::Value(50));
+ assert_eq!(common(&app, line).line_weight, LineWeight::Value(50));
+ assert_ne!(common(&app, hatch).line_weight, LineWeight::Value(50));
+
+ let _ = app.on_line_linetype_scale(2.5);
+ assert_eq!(common(&app, line).linetype_scale, 2.5);
+ assert_eq!(common(&app, hatch).linetype_scale, 1.0);
+ let _ = app.update(ga(GraphicAttributesMsg::LineLinetypeScale(f64::NAN)));
+ assert_eq!(common(&app, line).linetype_scale, 2.5);
+
+ let _ = app.on_line_transparency(Transparency::from_percent(0.2));
+ let _ = app.on_fill_transparency(Transparency::from_percent(0.4));
+ assert!((common(&app, line).transparency.as_percent() - 0.2).abs() < 0.01);
+ assert!((common(&app, hatch).transparency.as_percent() - 0.4).abs() < 0.01);
+ let _ = app.on_fill_transparency(Transparency::BY_LAYER);
+ assert!(common(&app, hatch).transparency.is_by_layer());
+ assert!((common(&app, line).transparency.as_percent() - 0.2).abs() < 0.01);
+ }
+
+ #[test]
+ fn choosing_another_fill_closes_the_editors() {
+ let mut app = fresh();
+ filled_square(&mut app, GraphicAttribute::Gradient);
+ app.open_gradient_editor();
+ assert!(app.graphic_attributes.gradient_editor.is_some());
+ let _ = app.on_graphic_attribute(GraphicAttribute::Hatch);
+ assert!(app.graphic_attributes.gradient_editor.is_none());
+
+ let _ = app.update(ga(GraphicAttributesMsg::HatchEditorOpen));
+ assert!(app.graphic_attributes.hatch_editor.is_some());
+ let _ = app.on_graphic_attribute(GraphicAttribute::Gradient);
+ assert!(app.graphic_attributes.hatch_editor.is_none());
+ assert!(app.graphic_attributes.gradient_editor.is_none());
+ }
+
+ #[test]
+ fn removing_the_fill_erases_it_in_one_undo_step() {
+ let mut app = fresh();
+ let i = app.active_tab;
+ let (_, hatch) = filled_square(&mut app, GraphicAttribute::Solid);
+ let undo_before = app.tabs[i].history.undo_stack.len();
+ let _ = app.on_graphic_attribute(GraphicAttribute::None);
+ assert!(app.tabs[i].scene.document.get_entity(hatch).is_none());
+ assert_eq!(app.tabs[i].history.undo_stack.len(), undo_before + 1);
+ }
+
+ #[test]
+ fn graphic_attributes_can_close_an_open_boundary_from_the_command_prompt() {
+ let mut app = fresh();
+ let i = app.active_tab;
+ let mut polyline = LwPolyline::new();
+ for (x, y) in [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)] {
+ polyline.add_point(Vector2::new(x, y));
+ }
+ let boundary = app.tabs[i]
+ .scene
+ .add_entity(EntityType::LwPolyline(polyline));
+ app.tabs[i].scene.select_entity(boundary, false);
+
+ let _ = app.update(ga(GraphicAttributesMsg::Fill(GraphicAttribute::Solid)));
+ assert!(app.graphic_attributes.pending_fill_close.is_some());
+ assert!(!app.tabs[i].scene.document.entities().any(|entity| {
+ matches!(entity, EntityType::Hatch(_))
+ }));
+
+ let _ = app.update(Message::CommandOptionPick("Y".to_string()));
+ assert!(app.graphic_attributes.pending_fill_close.is_none());
+ assert!(matches!(
+ app.tabs[i].scene.document.get_entity(boundary),
+ Some(EntityType::LwPolyline(polyline)) if polyline.is_closed
+ ));
+ assert!(app.tabs[i].scene.document.entities().any(|entity| {
+ matches!(entity, EntityType::Hatch(hatch) if hatch.is_solid && hatch.is_associative)
+ }));
+ }
+
+ #[test]
+ fn graphic_line_controls_update_creation_defaults_without_a_selection() {
+ let mut app = fresh();
+ let i = app.active_tab;
+ app.tabs[i].scene.deselect_all();
+
+ let _ = app.on_line_color(codec::types::Color::Index(2));
+ let _ = app.on_line_linetype("Continuous".to_string());
+ let _ = app.on_line_lineweight(codec::types::LineWeight::Value(35));
+ let _ = app.on_line_linetype_scale(2.5);
+ let _ = app.on_line_transparency(codec::types::Transparency::from_percent(0.3));
+
+ let header = &app.tabs[i].scene.document.header;
+ assert_eq!(
+ header.current_entity_color,
+ codec::types::Color::Index(2)
+ );
+ assert_eq!(header.current_linetype_name, "Continuous");
+ assert_eq!(header.current_line_weight, 35);
+ assert!((header.current_entity_linetype_scale - 2.5).abs() < f64::EPSILON);
+ assert!(
+ (app.tabs[i]
+ .scene
+ .document
+ .current_entity_transparency()
+ .as_percent()
+ - 0.3)
+ .abs()
+ < 0.01
+ );
+ }
+
+ #[test]
+ fn graphic_attributes_bulk_modes_and_remove_references_preserve_visible_values() {
+ let mut app = fresh();
+ let i = app.active_tab;
+ {
+ let layer = app.tabs[i]
+ .scene
+ .document
+ .layers
+ .get_mut("0")
+ .expect("default layer");
+ layer.color = codec::types::Color::Index(3);
+ layer.line_type = "Continuous".to_string();
+ layer.line_weight = codec::types::LineWeight::Value(50);
+ layer.transparency = codec::types::Transparency::from_percent(0.25);
+ }
+ let handle = app.tabs[i].scene.add_entity(EntityType::Line(Line::new()));
+ app.tabs[i].scene.select_entity(handle, false);
+
+ let _ = app.on_graphic_attributes_set_all(false);
+ let common = app.tabs[i]
+ .scene
+ .document
+ .get_entity(handle)
+ .expect("line")
+ .common();
+ assert_eq!(common.color, codec::types::Color::ByBlock);
+ assert_eq!(common.linetype, "ByBlock");
+ assert_eq!(common.line_weight, codec::types::LineWeight::ByBlock);
+ assert!(common.transparency.is_by_block());
+
+ let _ = app.on_graphic_attributes_set_all(true);
+ let _ = app.on_graphic_attributes_remove_references();
+ let common = app.tabs[i]
+ .scene
+ .document
+ .get_entity(handle)
+ .expect("line")
+ .common();
+ assert_eq!(common.color, codec::types::Color::Index(3));
+ assert_eq!(common.linetype, "Continuous");
+ assert_eq!(common.line_weight, codec::types::LineWeight::Value(50));
+ assert!((common.transparency.as_percent() - 0.25).abs() < 0.01);
+ }
+
+ #[test]
+ fn graphic_attributes_create_layer_prompt_keeps_spaces_and_active_settings() {
+ let mut app = fresh();
+ let i = app.active_tab;
+ app.tabs[i].scene.deselect_all();
+ let _ = app.on_line_color(codec::types::Color::Index(2));
+ let _ = app.on_line_linetype("Continuous".to_string());
+ let _ = app.on_line_lineweight(codec::types::LineWeight::Value(35));
+ let _ = app.on_line_transparency(codec::types::Transparency::from_percent(0.3));
+
+ let _ = app.create_graphic_attributes_layer("Invalid\nLayer");
+ assert!(app.tabs[i]
+ .scene
+ .document
+ .layers
+ .get("Invalid\nLayer")
+ .is_none());
+
+ let _ = app.update(ga(GraphicAttributesMsg::CreateLayer));
+ assert_eq!(
+ app.tabs[i]
+ .active_cmd
+ .as_ref()
+ .map(|command| command.name()),
+ Some("GRAPHICATTRIBUTELAYER")
+ );
+ let _ = app.update(Message::CommandInput("Graphic Settings".to_string()));
+ let _ = app.update(Message::CommandFinalize);
+
+ let layer = app.tabs[i]
+ .scene
+ .document
+ .layers
+ .get("Graphic Settings")
+ .expect("created layer");
+ assert_eq!(layer.color, codec::types::Color::Index(2));
+ assert_eq!(layer.line_type, "Continuous");
+ assert_eq!(layer.line_weight, codec::types::LineWeight::Value(35));
+ assert!((layer.transparency.as_percent() - 0.3).abs() < 0.01);
+ }
+
+ fn open_square(app: &mut OpenCADStudio) -> Handle {
+ let i = app.active_tab;
+ let mut polyline = LwPolyline::new();
+ for (x, y) in [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)] {
+ polyline.add_point(Vector2::new(x, y));
+ }
+ let handle = app.tabs[i]
+ .scene
+ .add_entity(EntityType::LwPolyline(polyline));
+ app.tabs[i].scene.select_entity(handle, false);
+ handle
+ }
+
+ fn has_hatch(app: &OpenCADStudio) -> bool {
+ app.tabs[app.active_tab]
+ .scene
+ .document
+ .entities()
+ .any(|entity| matches!(entity, EntityType::Hatch(_)))
+ }
+
+ fn is_closed(app: &OpenCADStudio, handle: Handle) -> bool {
+ matches!(
+ app.tabs[app.active_tab].scene.document.get_entity(handle),
+ Some(EntityType::LwPolyline(polyline)) if polyline.is_closed
+ )
+ }
+
+ fn type_and_enter(app: &mut OpenCADStudio, input: &str) {
+ let _ = app.update(Message::CommandInput(input.to_string()));
+ let _ = app.update(Message::CommandSubmit);
+ }
+
+ #[test]
+ fn closing_an_open_boundary_and_filling_it_is_one_undo_step() {
+ let mut app = fresh();
+ let i = app.active_tab;
+ let boundary = open_square(&mut app);
+ let undo_before = app.tabs[i].history.undo_stack.len();
+
+ let _ = app.update(ga(GraphicAttributesMsg::Fill(GraphicAttribute::Solid)));
+ let _ = app.update(Message::CommandOptionPick("Y".to_string()));
+ assert!(is_closed(&app, boundary));
+ assert!(has_hatch(&app));
+ assert_eq!(app.tabs[i].history.undo_stack.len(), undo_before + 1);
+
+ let _ = app.update(Message::Undo);
+ assert!(!is_closed(&app, boundary));
+ assert!(!has_hatch(&app));
+ }
+
+ #[test]
+ fn typed_answers_to_the_close_prompt() {
+ // Yes, typed and submitted with Enter.
+ let mut app = fresh();
+ let boundary = open_square(&mut app);
+ let _ = app.update(ga(GraphicAttributesMsg::Fill(GraphicAttribute::Solid)));
+ type_and_enter(&mut app, "y");
+ assert!(app.graphic_attributes.pending_fill_close.is_none());
+ assert!(is_closed(&app, boundary) && has_hatch(&app));
+
+ // No leaves the object open and unfilled.
+ let mut app = fresh();
+ let boundary = open_square(&mut app);
+ let _ = app.update(ga(GraphicAttributesMsg::Fill(GraphicAttribute::Solid)));
+ type_and_enter(&mut app, "No");
+ assert!(app.graphic_attributes.pending_fill_close.is_none());
+ assert!(!is_closed(&app, boundary) && !has_hatch(&app));
+
+ // Anything else keeps asking; a bare Enter takes the default Yes.
+ let mut app = fresh();
+ let boundary = open_square(&mut app);
+ let _ = app.update(ga(GraphicAttributesMsg::Fill(GraphicAttribute::Solid)));
+ type_and_enter(&mut app, "maybe");
+ assert!(app.graphic_attributes.pending_fill_close.is_some());
+ assert!(app.tabs[app.active_tab].active_cmd.is_none());
+ let _ = app.update(Message::CommandFinalize);
+ assert!(is_closed(&app, boundary) && has_hatch(&app));
+
+ // Escape cancels the request.
+ let mut app = fresh();
+ let boundary = open_square(&mut app);
+ let _ = app.update(ga(GraphicAttributesMsg::Fill(GraphicAttribute::Solid)));
+ let _ = app.update(Message::CommandEscape);
+ assert!(app.graphic_attributes.pending_fill_close.is_none());
+ assert!(!is_closed(&app, boundary) && !has_hatch(&app));
+ }
+
+ #[test]
+ fn linetype_scale_keeps_iso_pen_width_values() {
+ let mut app = fresh();
+ let i = app.active_tab;
+ let line = app.tabs[i].scene.add_entity(EntityType::Line(Line::new()));
+ app.tabs[i].scene.select_entity(line, false);
+ for scale in [0.13, 0.18, 0.25, 0.35, 0.05] {
+ let _ = app.update(ga(GraphicAttributesMsg::LineLinetypeScale(scale)));
+ let stored = app.tabs[i]
+ .scene
+ .document
+ .get_entity(line)
+ .expect("line")
+ .common()
+ .linetype_scale;
+ assert_eq!(stored, scale);
+ }
+ // Spinner steps do not accumulate floating-point noise.
+ let _ = app.update(ga(GraphicAttributesMsg::LineLinetypeScale(0.25 + 0.1)));
+ let stored = app.tabs[i]
+ .scene
+ .document
+ .get_entity(line)
+ .expect("line")
+ .common()
+ .linetype_scale;
+ assert_eq!(stored, 0.35);
+ for invalid in [0.0, -1.0, f64::INFINITY] {
+ let _ = app.update(ga(GraphicAttributesMsg::LineLinetypeScale(invalid)));
+ }
+ let stored = app.tabs[i]
+ .scene
+ .document
+ .get_entity(line)
+ .expect("line")
+ .common()
+ .linetype_scale;
+ assert_eq!(stored, 0.35);
+ }
+
+ #[test]
+ fn graphic_attributes_starts_hidden_and_toggle_is_persisted() {
+ let mut app = OpenCADStudio::new_for_test();
+ app.apply_config(crate::app::config::AppConfig::default());
+ assert!(!app.show_graphic_attributes);
+ assert!(!app.ribbon.show_graphic_attributes);
+
+ let _ = app.update(Message::ToggleGraphicAttributes);
+ assert!(app.show_graphic_attributes);
+ assert!(app.ribbon.show_graphic_attributes);
+ assert!(app.current_config().show_graphic_attributes);
+
+ let _ = app.update(Message::ToggleGraphicAttributes);
+ assert!(!app.show_graphic_attributes);
+ assert!(!app.ribbon.show_graphic_attributes);
+ assert!(!app.current_config().show_graphic_attributes);
+ }
+
+ fn closed_square_on(app: &mut OpenCADStudio, layer: &str) -> Handle {
+ let i = app.active_tab;
+ if app.tabs[i].scene.document.layers.get(layer).is_none() {
+ let mut new_layer = codec::tables::Layer::new(layer);
+ new_layer.handle = app.tabs[i].scene.document.allocate_handle();
+ let _ = app.tabs[i].scene.document.layers.add(new_layer);
+ }
+ let handle = open_square(app);
+ if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) {
+ if let EntityType::LwPolyline(polyline) = entity {
+ polyline.is_closed = true;
+ }
+ entity.common_mut().layer = layer.to_string();
+ }
+ handle
+ }
+
+ fn fill_layers(app: &OpenCADStudio) -> Vec {
+ app.tabs[app.active_tab]
+ .scene
+ .document
+ .entities()
+ .filter_map(|entity| match entity {
+ EntityType::Hatch(hatch) => Some(hatch.common.layer.clone()),
+ _ => None,
+ })
+ .collect()
+ }
+
+ #[test]
+ fn new_fills_follow_the_object_layer_unless_the_menu_option_is_set() {
+ let mut app = fresh();
+ closed_square_on(&mut app, "Walls");
+ let _ = app.update(ga(GraphicAttributesMsg::Fill(GraphicAttribute::Solid)));
+ assert_eq!(fill_layers(&app), vec!["Walls".to_string()]);
+
+ let mut app = fresh();
+ let current = app.tabs[app.active_tab].active_layer.clone();
+ closed_square_on(&mut app, "Walls");
+ let _ = app.update(ga(GraphicAttributesMsg::ToggleFillOnCurrentLayer));
+ assert!(app.current_config().graphic_fills_on_current_layer);
+ let _ = app.update(ga(GraphicAttributesMsg::Fill(GraphicAttribute::Solid)));
+ assert_eq!(fill_layers(&app), vec![current]);
+ }
+
+ #[test]
+ fn objects_on_locked_layers_get_no_fill() {
+ let mut app = fresh();
+ let i = app.active_tab;
+ closed_square_on(&mut app, "Locked");
+ closed_square_on(&mut app, "Open");
+ app.tabs[i]
+ .scene
+ .document
+ .layers
+ .get_mut("Locked")
+ .expect("locked layer")
+ .lock();
+ let _ = app.update(ga(GraphicAttributesMsg::Fill(GraphicAttribute::Solid)));
+ assert_eq!(fill_layers(&app), vec!["Open".to_string()]);
+ }
+
+ #[test]
+ fn palette_fill_lookup_follows_document_changes() {
+ let mut app = fresh();
+ let i = app.active_tab;
+ let boundary = closed_square_on(&mut app, "0");
+ let selected = vec![boundary];
+ let (fills, kind) = app.graphic_attributes.fills(&app.tabs[i].scene, &selected);
+ assert!(fills.is_empty());
+ assert_eq!(kind, GraphicAttribute::None);
+
+ let _ = app.update(ga(GraphicAttributesMsg::Fill(GraphicAttribute::Solid)));
+ let (fills, kind) = app.graphic_attributes.fills(&app.tabs[i].scene, &selected);
+ assert_eq!(fills.len(), 1);
+ assert_eq!(kind, GraphicAttribute::Solid);
+ }
+
+ #[test]
+ fn showing_the_palette_docks_it_below_properties_once() {
+ use crate::app::config::DockSide;
+ use crate::ui::dock::PanelId;
+ let mut app = OpenCADStudio::new_for_test();
+ app.apply_config(crate::app::config::AppConfig::default());
+ assert_eq!(app.dock.location(PanelId::GraphicAttributes), None);
+
+ let _ = app.update(Message::ToggleGraphicAttributes);
+ let properties = app.dock.location(PanelId::Properties).expect("properties docked");
+ assert_eq!(
+ app.dock.location(PanelId::GraphicAttributes),
+ Some(properties)
+ );
+
+ // A place the user chose is kept when the palette is shown again.
+ app.dock.dock(PanelId::GraphicAttributes, DockSide::Right, 0);
+ let _ = app.update(Message::ToggleGraphicAttributes);
+ let _ = app.update(Message::ToggleGraphicAttributes);
+ assert_eq!(
+ app.dock.location(PanelId::GraphicAttributes),
+ Some((DockSide::Right, 0))
+ );
+ }
+
+ fn output_since(app: &OpenCADStudio, start: usize) -> String {
+ app.command_line.history[start..]
+ .iter()
+ .map(|entry| entry.text.as_str())
+ .collect::>()
+ .join("\n")
+ }
+
+ #[test]
+ fn undo_names_fill_and_linetype_changes() {
+ let mut app = fresh();
+ let boundary = add_closed_square(&mut app, 0.0);
+ select(&mut app, &[boundary]);
+
+ let _ = app.update(ga(GraphicAttributesMsg::Fill(GraphicAttribute::Solid)));
+ let start = app.command_line.history.len();
+ let _ = app.update(Message::Undo);
+ assert!(output_since(&app, start).contains("Undo: Fill change"));
+
+ let _ = app.update(ga(GraphicAttributesMsg::LineLinetype("Continuous".into())));
+ let start = app.command_line.history.len();
+ let _ = app.update(Message::Undo);
+ assert!(output_since(&app, start).contains("Undo: Linetype"));
+ }
+
+ #[test]
+ fn typed_linetype_scale_is_applied_on_enter() {
+ let mut app = fresh();
+ let i = app.active_tab;
+ let line = app.tabs[i].scene.add_entity(EntityType::Line(Line::new()));
+ select(&mut app, &[line]);
+
+ // Enter without typing changes nothing.
+ let _ = app.update(ga(GraphicAttributesMsg::LineLinetypeScaleSubmit));
+ assert_eq!(common(&app, line).linetype_scale, 1.0);
+
+ // Typing alone does not apply; Enter does, with a decimal comma too.
+ let _ = app.update(ga(GraphicAttributesMsg::LineLinetypeScaleInput("0,35".into())));
+ assert_eq!(common(&app, line).linetype_scale, 1.0);
+ let _ = app.update(ga(GraphicAttributesMsg::LineLinetypeScaleSubmit));
+ assert_eq!(common(&app, line).linetype_scale, 0.35);
+ assert!(app.graphic_attributes.linetype_scale_input.is_none());
+
+ // Invalid or non-positive input is dropped.
+ for typed in ["abc", "0", "-2"] {
+ let _ = app.update(ga(GraphicAttributesMsg::LineLinetypeScaleInput(typed.into())));
+ let _ = app.update(ga(GraphicAttributesMsg::LineLinetypeScaleSubmit));
+ assert_eq!(common(&app, line).linetype_scale, 0.35);
+ }
+ }
+
+ #[test]
+ fn clicking_the_linetype_scale_field_selects_it_once() {
+ let mut app = fresh();
+ let field = iced::widget::Id::new(palette::LINETYPE_SCALE_FIELD);
+ let _ = app.update(Message::PropSyncActive(Some(field.clone())));
+ assert!(app.graphic_attributes.linetype_scale_focused);
+
+ // Focus elsewhere drops unsubmitted text.
+ app.graphic_attributes.linetype_scale_input = Some("3".into());
+ let _ = app.update(Message::PropSyncActive(None));
+ assert!(!app.graphic_attributes.linetype_scale_focused);
+ assert!(app.graphic_attributes.linetype_scale_input.is_none());
+ }
+}
diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs
index 645ef10ee..0ab8654a2 100644
--- a/src/app/update/mod.rs
+++ b/src/app/update/mod.rs
@@ -99,6 +99,7 @@ mod context_menu;
mod dialog;
mod dynamic;
mod file;
+mod graphic_attributes;
pub(in crate::app) use file::background_task;
#[cfg(not(target_arch = "wasm32"))]
pub(in crate::app) use file::native_paths_match;
@@ -473,6 +474,10 @@ impl OpenCADStudio {
}
}
self.refresh_gpu_status();
+ if self.graphic_attributes.tracks_selection() {
+ let selected = self.tabs[self.active_tab].scene.selected_handles_in_order();
+ self.graphic_attributes.retain_selection(&selected);
+ }
self.show_next_startup_modal();
self.sync_open_command_history();
// Close the document-level first-touch transaction started by
@@ -486,6 +491,7 @@ impl OpenCADStudio {
.active_cmd
.as_ref()
.map(|c| c.prompt())
+ .or_else(|| self.graphic_attributes.fill_close_prompt())
.or_else(|| self.pending_pick_label());
self.command_line.set_step_prompt(prompt);
// Mirror the step's clickable options so they render as buttons (#304).
@@ -493,6 +499,14 @@ impl OpenCADStudio {
.active_cmd
.as_ref()
.map(|c| c.options())
+ .or_else(|| {
+ self.graphic_attributes.pending_fill_close.as_ref().map(|_| {
+ vec![
+ crate::command::CmdOption::new("Yes", "Y"),
+ crate::command::CmdOption::new("No", "N"),
+ ]
+ })
+ })
.unwrap_or_default();
self.command_line.set_step_options(opts);
// Persist UI preferences whenever a toggle changes them (issue #68).
@@ -2535,6 +2549,9 @@ impl OpenCADStudio {
// finishes the step like Enter. (#304)
self.command_line.input.clear();
self.command_line.close_history();
+ if self.awaiting_fill_close_answer() {
+ return self.on_graphic_fill_close_input(&kw);
+ }
if kw.is_empty() {
return self.feed_command(crate::command::StepInput::Enter);
}
@@ -3877,6 +3894,7 @@ impl OpenCADStudio {
}
Message::ViewportLeftPress => {
+ self.graphic_attributes.open_menu = None;
let sweep = self.sync_active_field_if_any();
Task::batch(vec![sweep, self.on_viewport_left_press()])
}
@@ -3884,6 +3902,7 @@ impl OpenCADStudio {
Message::ViewportLeftRelease => self.on_viewport_left_release(),
Message::ViewportRightPress => {
+ self.graphic_attributes.open_menu = None;
let i = self.active_tab;
self.ribbon.close_dropdown();
// Shift+RMB: the one-shot snap override menu at the cursor —
@@ -3986,7 +4005,10 @@ impl OpenCADStudio {
self.unfocus_widgets()
}
- Message::ViewportMiddlePress => self.on_viewport_middle_press(),
+ Message::ViewportMiddlePress => {
+ self.graphic_attributes.open_menu = None;
+ self.on_viewport_middle_press()
+ }
Message::ViewportMiddleRelease => {
let i = self.active_tab;
@@ -4318,6 +4340,14 @@ impl OpenCADStudio {
self.ribbon.set_properties(self.show_properties);
Task::none()
}
+ Message::ToggleGraphicAttributes => {
+ let id = crate::ui::dock::PanelId::GraphicAttributes;
+ let open = !self.show_graphic_attributes;
+ if open {
+ self.dock.dock_below_properties(id);
+ }
+ self.dock_set_open(id, open)
+ }
Message::ToggleFileTabs => {
self.show_file_tabs ^= true;
self.ribbon.set_file_tabs(self.show_file_tabs);
@@ -6697,31 +6727,7 @@ impl OpenCADStudio {
Task::none()
}
Message::RibbonLinetypeChanged(lt) => self.on_ribbon_linetype_changed(lt),
- Message::RibbonLineweightChanged(lw) => {
- let i = self.active_tab;
- self.ribbon.close_dropdown();
- let handles = self.property_target_handles(i);
- if handles.is_empty() {
- if self.has_property_selection(i) {
- return Task::none();
- }
- // Persist into the tab's header (CELWEIGHT). #21.
- self.tabs[i].scene.document.header.current_line_weight = lw.value();
- self.tabs[i].dirty = true;
- self.ribbon.active_lineweight = lw;
- } else {
- // Lineweight is baked into the cached wire geometry —
- // re-tessellate so the change shows immediately (issue #231
- // class).
- self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
- if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) {
- crate::scene::view::dispatch::apply_line_weight(entity, lw);
- }
- });
- self.ribbon.active_lineweight = lw;
- }
- Task::none()
- }
+ Message::RibbonLineweightChanged(lw) => self.on_ribbon_lineweight_changed(lw),
Message::RibbonStyleChanged { key, name } => self.on_ribbon_style_changed(key, name),
@@ -7132,13 +7138,28 @@ impl OpenCADStudio {
Message::PropAttrCommit(tag) => self.on_prop_attr_commit(tag),
Message::PropPointerPressed => {
- if !self.dock_panel_visible(crate::ui::dock::PanelId::Properties) {
+ if !self.dock_panel_visible(crate::ui::dock::PanelId::Properties)
+ && !self.dock_panel_visible(crate::ui::dock::PanelId::GraphicAttributes)
+ {
return Task::none();
}
crate::ui::properties::sync_active_field_task()
}
Message::PropSyncActive(focused) => {
+ // The Graphic Attributes linetype scale field behaves like a
+ // Properties field: the first click selects its value.
+ let scale_field = iced::widget::Id::new(
+ crate::ui::window::graphic_attributes::LINETYPE_SCALE_FIELD,
+ );
+ if focused.as_ref() == Some(&scale_field) {
+ if !std::mem::replace(&mut self.graphic_attributes.linetype_scale_focused, true) {
+ return iced::widget::operation::select_all(scale_field);
+ }
+ return Task::none();
+ }
+ self.graphic_attributes.linetype_scale_focused = false;
+ self.graphic_attributes.linetype_scale_input = None;
let panel = &mut self.tabs[self.active_tab].properties;
if let Some(id) = focused.as_ref() {
if let Some(key) = panel.prop_field_key_for_id(id) {
@@ -9853,6 +9874,7 @@ impl OpenCADStudio {
Message::PlotDialogOpen => self.on_plot_dialog_open(),
Message::PlotDlg(m) => self.on_plot_dlg(m),
Message::BlockPalette(m) => self.on_block_palette(m),
+ Message::GraphicAttributes(m) => self.on_graphic_attributes(m),
Message::Dock(m) => self.on_dock(m),
Message::PrintAllOpen => self.on_print_all_open(),
Message::PrintAllToggle(name) => {
@@ -11015,6 +11037,13 @@ impl OpenCADStudio {
Task::none()
}
Message::OpenColorWindow(target, color) => {
+ if matches!(
+ target,
+ crate::app::ColorPickTarget::GraphicAttributesLine
+ | crate::app::ColorPickTarget::GraphicAttributesSolid
+ ) {
+ self.graphic_attributes.open_menu = None;
+ }
self.color_pick_target = Some((target, color));
// Always open the shared CAD colour picker on the indexed ACI page.
diff --git a/src/app/update/style.rs b/src/app/update/style.rs
index 8bf8fb7e1..2deac74ec 100644
--- a/src/app/update/style.rs
+++ b/src/app/update/style.rs
@@ -13,6 +13,7 @@ use crate::scene::pick::grip::{find_hit_grip, find_hit_grip_paper, find_hit_grip
use crate::scene::{
self, hover_id, CubeRegion, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX,
};
+use crate::ui::window::graphic_attributes::GraphicAttributesMsg;
use crate::ui::PropertiesPanel;
use codec::types::Color as AcadColor;
use codec::{EntityType as AcadEntityType, Handle};
@@ -1156,6 +1157,17 @@ impl OpenCADStudio {
}
let s = crate::ui::color_select::color_to_aci_string(color);
let edit = match self.color_pick_target.take().map(|(target, _)| target) {
+ Some(crate::app::ColorPickTarget::Gradient(index)) => {
+ Some(Message::GraphicAttributes(GraphicAttributesMsg::Gradient(
+ crate::ui::window::gradient_editor::GradientMsg::Color(index, color),
+ )))
+ }
+ Some(crate::app::ColorPickTarget::GraphicAttributesLine) => Some(
+ Message::GraphicAttributes(GraphicAttributesMsg::LineColor(color)),
+ ),
+ Some(crate::app::ColorPickTarget::GraphicAttributesSolid) => Some(
+ Message::GraphicAttributes(GraphicAttributesMsg::FillColor(color)),
+ ),
Some(crate::app::ColorPickTarget::DimStyle(f)) => Some(Message::DsEdit(f, s)),
Some(crate::app::ColorPickTarget::MLeader(f)) => {
Some(Message::MLeaderStyleEdit { field: f, value: s })
diff --git a/src/command.rs b/src/command.rs
index 788286362..fbe52867d 100644
--- a/src/command.rs
+++ b/src/command.rs
@@ -354,6 +354,46 @@ impl CadCommand for ValuePromptCommand {
}
}
+/// One-value prompt that preserves spaces and letter casing. This is intended
+/// for symbol names entered through the command line, where a space belongs to
+/// the value instead of submitting the current token.
+pub struct FreeTextValuePromptCommand {
+ name: &'static str,
+ prompt: &'static str,
+}
+
+impl FreeTextValuePromptCommand {
+ pub fn new(name: &'static str, prompt: &'static str) -> Self {
+ Self { name, prompt }
+ }
+}
+
+impl CadCommand for FreeTextValuePromptCommand {
+ fn name(&self) -> &'static str {
+ self.name
+ }
+
+ fn prompt(&self) -> String {
+ crate::t!(self.prompt).into_owned()
+ }
+
+ fn input_kind(&self) -> InputKind {
+ InputKind::FreeText
+ }
+
+ fn on_text_input(&mut self, text: &str) -> Option {
+ Some(CmdResult::Dispatch(format!("{} {}", self.name, text.trim())))
+ }
+
+ fn on_point(&mut self, _pt: DVec3) -> CmdResult {
+ CmdResult::NeedPoint
+ }
+
+ fn on_enter(&mut self) -> CmdResult {
+ CmdResult::Dispatch(format!("{} ", self.name))
+ }
+}
+
/// Interactive pick for `UCS FACE` and `UCS OBJECT`.
///
/// Hands the picked entity to the inline `UCS FACE ` /
diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs
index efbe56a04..ae206883d 100644
--- a/src/locale_catalog.rs
+++ b/src/locale_catalog.rs
@@ -5772,6 +5772,39 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"New group" => Some(("dock", "new-group")),
"Browser" => Some(("dock", "browser")),
"Node Graph" => Some(("dock", "node-graph")),
+ "Graphic Attributes" => Some(("graphic-attributes", "title")),
+ "Set all attributes ByLayer" => Some(("graphic-attributes", "set-all-bylayer")),
+ "Set all attributes ByBlock" => Some(("graphic-attributes", "set-all-byblock")),
+ "Remove ByLayer / ByBlock" => Some(("graphic-attributes", "remove-bylayer-byblock")),
+ "Create Layer with active settings" => Some(("graphic-attributes", "create-layer-active-settings")),
+ "Create fills on the current layer" => Some(("graphic-attributes", "fills-on-current-layer")),
+ "Edit hatch" => Some(("graphic-attributes", "edit-hatch")),
+ "Edit gradient" => Some(("graphic-attributes", "edit-gradient")),
+ "The selected object is open. Close it before creating the fill? [Yes / No] " => Some(("graphic-attributes", "close-open-object")),
+ "{count} selected objects are open. Close them before creating the fill? [Yes / No] " => Some(("graphic-attributes", "close-open-objects")),
+ "Enter a name for the new layer:" => Some(("graphic-attributes", "enter-new-layer-name")),
+ "Graphic Attributes: enter a valid layer name without <>/\\\":;?*|,=`." => Some(("graphic-attributes", "invalid-layer-name")),
+ "Graphic Attributes: layer \"{name}\" already exists." => Some(("graphic-attributes", "layer-exists")),
+ "Graphic Attributes: the layer could not be created." => Some(("graphic-attributes", "layer-not-created")),
+ "Created layer \"{name}\" with the active Graphic Attributes settings." => Some(("graphic-attributes", "layer-created")),
+ "Gradient Fill: the fill is on a locked layer." => Some(("graphic-attributes", "gradient-fill-locked")),
+ "Select one or more closed objects first." => Some(("graphic-attributes", "select-closed-objects")),
+ "Graphic Attributes: {} object(s) on a locked layer were not changed." => Some(("graphic-attributes", "locked-objects-skipped")),
+ "Graphic Attributes: no hatch pattern is available." => Some(("graphic-attributes", "no-hatch-pattern")),
+ "Graphic Attributes: an associated fill is on a locked layer." => Some(("graphic-attributes", "associated-fill-locked")),
+ "Graphic Attributes: 1 unsupported object was not changed." => Some(("graphic-attributes", "one-unsupported")),
+ "Graphic Attributes: {skipped} unsupported objects were not changed." => Some(("graphic-attributes", "unsupported-skipped")),
+ "Fill transparency" => Some(("graphic-attributes", "fill-transparency")),
+ "Fill change" => Some(("graphic-attributes", "fill-change")),
+ "Line transparency" => Some(("graphic-attributes", "line-transparency")),
+ "Cyl." => Some(("graphic-attributes", "cylindrical-short")),
+ "Spher." => Some(("graphic-attributes", "spherical-short")),
+ "Hemi." => Some(("graphic-attributes", "hemispherical-short")),
+ "Invert gradient" => Some(("graphic-attributes", "invert-gradient")),
+ "Tone" => Some(("graphic-attributes", "tone")),
+ "Reset angle" => Some(("graphic-attributes", "reset-angle")),
+ "One Color" => Some(("graphic-attributes", "one-color")),
+ "Two Colors" => Some(("graphic-attributes", "two-colors")),
_ => None,
}
}
diff --git a/src/modules/draw/draw/hatch.rs b/src/modules/draw/draw/hatch.rs
index 99fa47e0a..587073aba 100644
--- a/src/modules/draw/draw/hatch.rs
+++ b/src/modules/draw/draw/hatch.rs
@@ -253,6 +253,13 @@ impl HatchCommand {
self
}
+ pub fn with_pattern(mut self, name: impl Into, pattern: HatchPattern) -> Self {
+ self.pattern_override = Some((name.into(), pattern));
+ self
+ }
+
+ pub fn finish_selected(mut self) -> CmdResult { self.on_enter() }
+
fn set_object_selection(&mut self, handles: Vec) {
let mut segments = Vec::new();
for handle in &handles {
diff --git a/src/modules/view/graphic_attributes_palette.rs b/src/modules/view/graphic_attributes_palette.rs
new file mode 100644
index 000000000..03896aa11
--- /dev/null
+++ b/src/modules/view/graphic_attributes_palette.rs
@@ -0,0 +1,14 @@
+use crate::modules::{IconKind, ModuleEvent, ToolDef};
+
+pub const ICON: IconKind = IconKind::Svg(include_bytes!(
+ "../../../assets/icons/graphic_attributes.svg"
+));
+
+pub fn tool() -> ToolDef {
+ ToolDef {
+ id: "GRAPHICATTRIBUTES",
+ label: "Graphic Attributes",
+ icon: ICON,
+ event: ModuleEvent::Command("GRAPHICATTRIBUTES".to_string()),
+ }
+}
diff --git a/src/modules/view/mod.rs b/src/modules/view/mod.rs
index 39387f4fa..eb448e4dc 100644
--- a/src/modules/view/mod.rs
+++ b/src/modules/view/mod.rs
@@ -3,6 +3,7 @@
mod cascade;
mod count_palette;
mod file_tabs;
+mod graphic_attributes_palette;
mod layout_tabs;
pub mod limits;
mod orbit;
@@ -123,6 +124,7 @@ impl CadModule for ViewModule {
RibbonItem::LargeTool(tool_palettes::tool()),
RibbonItem::LargeTool(properties_palette::tool()),
RibbonItem::LargeTool(count_palette::tool()),
+ RibbonItem::LargeTool(graphic_attributes_palette::tool()),
RibbonItem::LargeTool(sheetset::tool()),
],
},
diff --git a/src/scene/entity.rs b/src/scene/entity.rs
index c1419b4e6..19e6210bf 100644
--- a/src/scene/entity.rs
+++ b/src/scene/entity.rs
@@ -103,7 +103,7 @@ fn family_from_stored_line(
/// Preserve the selected hue while moving its HSL lightness towards the
/// persisted one-colour tint/shade target (0 = black, 1 = white).
-fn gradient_tint_color(base: [f32; 4], target: f32) -> [f32; 4] {
+pub(crate) fn gradient_tint_color(base: [f32; 4], target: f32) -> [f32; 4] {
let max = base[0].max(base[1]).max(base[2]);
let min = base[0].min(base[1]).min(base[2]);
let lightness = (max + min) * 0.5;
@@ -1394,10 +1394,12 @@ impl Scene {
style.0 = crate::scene::view::render::adapt_to_bg(style.0, hatch_bg);
m.aci = style.4;
m.line_weight_px = style.3;
- // A gradient's colour is its first stop (already baked into
- // the cached model); only solid / pattern fills take the
- // entity's resolved colour.
- if !matches!(m.pattern, model::hatch_model::HatchPattern::Gradient { .. }) {
+ // Gradient RGB comes from its own colour stops, while
+ // transparency remains an entity property shared by both
+ // stops. Solid/pattern fills take the complete style.
+ if matches!(m.pattern, model::hatch_model::HatchPattern::Gradient { .. }) {
+ m.set_alpha(style.0[3]);
+ } else {
m.color = style.0;
}
if let EntityType::Hatch(dxf) = e {
@@ -2213,7 +2215,7 @@ impl Scene {
.colors
.get(i)
.and_then(|e| e.color.rgb())
- .map(|(r, g, b)| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0])
+ .map(|(r, g, b)| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, color[3]])
};
gradient_color1 = stop(0);
let color1 = gradient_color1.unwrap_or(color);
diff --git a/src/scene/hatch_boundary/tests.rs b/src/scene/hatch_boundary/tests.rs
index 8a03e4da3..f6f5c72c6 100644
--- a/src/scene/hatch_boundary/tests.rs
+++ b/src/scene/hatch_boundary/tests.rs
@@ -4,6 +4,43 @@ use codec::types::{Vector2, Vector3};
use kernel::geom2d::{refine_spline_boundary, triangulate_rings, Curve, NurbsCurve};
use kernel::tessellation::DEFAULT_ANGLE;
+#[test]
+fn gradient_stops_keep_entity_transparency() {
+ let mut hatch = Hatch::new();
+ hatch.is_solid = true;
+ hatch
+ .paths
+ .push(rectangle([0.0, 0.0], [10.0, 10.0], [0.0; 2]));
+ hatch.gradient_color.enabled = true;
+ hatch.gradient_color.name = "LINEAR".into();
+ hatch.gradient_color.colors = vec![
+ codec::entities::hatch::GradientColorEntry {
+ value: 0.0,
+ color: codec::types::Color::Rgb { r: 255, g: 0, b: 0 },
+ },
+ codec::entities::hatch::GradientColorEntry {
+ value: 1.0,
+ color: codec::types::Color::Rgb { r: 0, g: 0, b: 255 },
+ },
+ ];
+
+ let mut model = Scene::hatch_model_from_dxf(&hatch, [0.2, 0.3, 0.4, 0.35]).unwrap();
+ assert!((model.color[3] - 0.35).abs() < f32::EPSILON);
+ let crate::scene::model::hatch_model::HatchPattern::Gradient { color2, .. } = &model.pattern
+ else {
+ panic!("gradient hatch must produce a gradient render pattern");
+ };
+ assert!((color2[3] - 0.35).abs() < f32::EPSILON);
+
+ model.set_alpha(0.6);
+ assert!((model.color[3] - 0.6).abs() < f32::EPSILON);
+ let crate::scene::model::hatch_model::HatchPattern::Gradient { color2, .. } = &model.pattern
+ else {
+ unreachable!();
+ };
+ assert!((color2[3] - 0.6).abs() < f32::EPSILON);
+}
+
fn narrow(clearance: f64, offset: [f64; 2]) -> NurbsCurve {
let line = |a: [f64; 2], b: [f64; 2]| {
[
diff --git a/src/scene/mod.rs b/src/scene/mod.rs
index 46921453f..a895f6dd8 100644
--- a/src/scene/mod.rs
+++ b/src/scene/mod.rs
@@ -37,6 +37,7 @@ pub use page_setup::{apply_default_page_setup, document_page_setups, rotated_mar
mod dwg_native_constraints;
mod entity;
pub use camera_ops::named_view_center;
+pub(crate) use entity::gradient_tint_color;
pub use entity::CreateBlockOptions;
mod group_layer;
#[cfg(test)]
diff --git a/src/scene/model/hatch_model.rs b/src/scene/model/hatch_model.rs
index c6655ed20..0d4863c71 100644
--- a/src/scene/model/hatch_model.rs
+++ b/src/scene/model/hatch_model.rs
@@ -272,6 +272,18 @@ pub struct HatchModel {
pub draw_depth: f32,
}
+impl HatchModel {
+ /// Apply an entity's resolved opacity to every colour stop. Gradient stop
+ /// colours come from the HATCH payload, but transparency remains a common
+ /// entity property and must affect both ends equally.
+ pub(crate) fn set_alpha(&mut self, alpha: f32) {
+ self.color[3] = alpha;
+ if let HatchPattern::Gradient { color2, .. } = &mut self.pattern {
+ color2[3] = alpha;
+ }
+ }
+}
+
impl HatchModel {
pub(crate) fn gradient_frame(
&self,
diff --git a/src/scene/paper.rs b/src/scene/paper.rs
index b713ad29f..bdf30a628 100644
--- a/src/scene/paper.rs
+++ b/src/scene/paper.rs
@@ -475,7 +475,11 @@ impl Scene {
_ => model.clone(),
};
let style = self.render_style(entity);
- m.color = style.0;
+ if matches!(m.pattern, model::hatch_model::HatchPattern::Gradient { .. }) {
+ m.set_alpha(style.0[3]);
+ } else {
+ m.color = style.0;
+ }
m.aci = style.4;
m.line_weight_px = style.3;
if let EntityType::Hatch(dxf) = entity {
@@ -600,7 +604,11 @@ impl Scene {
_ => model.clone(),
};
let style = self.render_style(entity);
- hatch.color = style.0;
+ if matches!(hatch.pattern, model::hatch_model::HatchPattern::Gradient { .. }) {
+ hatch.set_alpha(style.0[3]);
+ } else {
+ hatch.color = style.0;
+ }
hatch.aci = style.4;
hatch.line_weight_px = style.3;
hatch.draw_depth = depth_map.get(&handle.value()).map_or(0.0, |depth| depth[0]);
diff --git a/src/ui/color_select.rs b/src/ui/color_select.rs
index c6efde206..3658a251e 100644
--- a/src/ui/color_select.rs
+++ b/src/ui/color_select.rs
@@ -230,18 +230,7 @@ fn color_selector_with_indicator<'a>(
}
let popup = container(color_list(extras, on_select, on_more))
- .style(|theme: &Theme| {
- let palette = theme.palette();
- container::Style {
- background: Some(Background::Color(palette.background.weak.color)),
- border: Border {
- color: palette.background.neutral.color,
- width: 1.0,
- radius: 2.0.into(),
- },
- ..Default::default()
- }
- })
+ .style(popup_panel_style)
.padding(2);
// `DropDown` keeps the popup outside the surrounding form layout and
@@ -255,7 +244,20 @@ fn color_selector_with_indicator<'a>(
.into()
}
-fn list_row_style(theme: &Theme, status: button::Status) -> button::Style {
+pub(crate) fn popup_panel_style(theme: &Theme) -> container::Style {
+ let palette = theme.palette();
+ container::Style {
+ background: Some(Background::Color(palette.background.weak.color)),
+ border: Border {
+ color: palette.background.neutral.color,
+ width: 1.0,
+ radius: 2.0.into(),
+ },
+ ..Default::default()
+ }
+}
+
+pub(crate) fn list_row_style(theme: &Theme, status: button::Status) -> button::Style {
let palette = theme.palette();
let hovered = matches!(status, button::Status::Hovered);
let text_color = if hovered {
diff --git a/src/ui/dock.rs b/src/ui/dock.rs
index 771c4b908..f5fac7a7d 100644
--- a/src/ui/dock.rs
+++ b/src/ui/dock.rs
@@ -104,6 +104,7 @@ pub const DOCK_HOVER_DELAY: std::time::Duration = std::time::Duration::from_mill
#[serde(rename_all = "snake_case")]
pub enum PanelId {
Properties,
+ GraphicAttributes,
BlockPalette,
ExternalReferences,
/// Outline of the drawing's origin planes, open sketch and solid bodies.
@@ -122,8 +123,9 @@ pub enum PanelId {
impl PanelId {
/// Every panel, in the order the pallet menu lists them.
- pub const ALL: [PanelId; 9] = [
+ pub const ALL: [PanelId; 10] = [
PanelId::Properties,
+ PanelId::GraphicAttributes,
PanelId::BlockPalette,
PanelId::ExternalReferences,
PanelId::Browser,
@@ -138,6 +140,7 @@ impl PanelId {
pub fn title(self) -> &'static str {
match self {
PanelId::Properties => "Properties",
+ PanelId::GraphicAttributes => "Graphic Attributes",
PanelId::BlockPalette => "Block Palette",
PanelId::ExternalReferences => "External References",
PanelId::Browser => "Browser",
@@ -153,6 +156,7 @@ impl PanelId {
pub fn icon(self) -> &'static [u8] {
match self {
PanelId::Properties => include_bytes!("../../assets/icons/properties.svg"),
+ PanelId::GraphicAttributes => include_bytes!("../../assets/icons/graphic_attributes.svg"),
PanelId::BlockPalette => include_bytes!("../../assets/icons/blocks/insert.svg"),
PanelId::ExternalReferences => crate::ui::icons::FOLDER_OPEN,
PanelId::Browser => include_bytes!("../../assets/icons/content_browser.svg"),
@@ -168,6 +172,7 @@ impl PanelId {
fn default_width(self) -> f32 {
match self {
PanelId::Properties => 250.0,
+ PanelId::GraphicAttributes => 250.0,
PanelId::BlockPalette => 260.0,
PanelId::ExternalReferences => 460.0,
PanelId::Browser => 230.0,
@@ -518,6 +523,23 @@ impl DockState {
self.floating.iter_mut().for_each(FloatPanel::heal);
}
+ /// Stack `id` under Properties in its group (or dock it at the end of
+ /// the left edge) unless it already has a place, which is then kept.
+ pub fn dock_below_properties(&mut self, id: PanelId) {
+ if self.is_placed(id) {
+ return;
+ }
+ match self.location(PanelId::Properties) {
+ Some((side, group)) => {
+ let index = self.groups(side)[group].panels.len();
+ self.join_group(id, side, group, index);
+ }
+ None => {
+ self.dock(id, DockSide::Left, usize::MAX);
+ }
+ }
+ }
+
/// Where (if anywhere) a panel is docked: its side and group index.
pub fn location(&self, id: PanelId) -> Option<(DockSide, usize)> {
for side in [DockSide::Left, DockSide::Right] {
@@ -1054,8 +1076,25 @@ pub struct Chrome {
/// and close. Pressing the row starts re-docking the panel. A floating panel
/// gets nothing here: its frame draws a vertical title bar beside it.
pub fn title_bar<'a>(id: PanelId, title: String, chrome: Chrome) -> Element<'a, Message> {
+ title_bar_with(id, title, chrome, None)
+}
+
+/// [`title_bar`] with a pallet's own control (a menu button) before the pin
+/// and close. A floating pallet has no title bar, so the control then sits
+/// alone at the top right.
+pub fn title_bar_with<'a>(
+ id: PanelId,
+ title: String,
+ chrome: Chrome,
+ extra: Option>,
+) -> Element<'a, Message> {
if chrome.floating {
- return Space::new().width(0).height(0).into();
+ return match extra {
+ Some(extra) => row![Space::new().width(Length::Fill), extra]
+ .padding([3, 6])
+ .into(),
+ None => Space::new().width(0).height(0).into(),
+ };
}
// The pin and close only show while the pointer is over the bar; the
// fixed height keeps the bar from jumping when they appear.
@@ -1063,6 +1102,9 @@ pub fn title_bar<'a>(id: PanelId, title: String, chrome: Chrome) -> Element<'a,
.spacing(3)
.height(Length::Fixed(TITLE_BUTTON_H))
.align_y(iced::Center);
+ if let Some(extra) = extra {
+ bar = bar.push(extra);
+ }
if chrome.title_hovered {
bar = bar
.push(pin_button(
@@ -1230,6 +1272,7 @@ mod tests {
let mut state = DockState::default();
state.ensure_settings();
assert_eq!(state.width(PanelId::Properties, 1600.0), 250.0);
+ assert_eq!(state.width(PanelId::GraphicAttributes, 1600.0), 250.0);
assert_eq!(state.width(PanelId::BlockPalette, 1600.0), 260.0);
assert!(!state.auto_collapse(PanelId::Properties));
}
diff --git a/src/ui/properties.rs b/src/ui/properties.rs
index ff7235eea..20e0eb9a8 100644
--- a/src/ui/properties.rs
+++ b/src/ui/properties.rs
@@ -55,6 +55,9 @@ pub fn linetype_display_name(name: &str) -> String {
impl fmt::Display for LinetypeItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if self.name.is_empty() && !self.art.is_empty() {
+ return f.write_str(&self.art);
+ }
let name = linetype_display_name(&self.name);
if self.art.is_empty() {
write!(f, "{name}")
@@ -95,6 +98,7 @@ impl fmt::Display for SelectionGroup {
#[derive(Clone)]
struct HatchPatternPreview {
pattern: crate::scene::model::hatch_model::HatchPattern,
+ padding: f32,
}
impl canvas::Program for HatchPatternPreview {
@@ -112,7 +116,7 @@ impl canvas::Program for HatchPatternPreview {
let mut frame = canvas::Frame::new(renderer, bounds.size());
let palette = theme.palette();
- let pad = 4.0;
+ let pad = self.padding;
let sample = canvas::Path::rectangle(
Point::new(pad, pad),
Size::new(
@@ -201,6 +205,31 @@ fn hatch_preview_scale(pattern: &crate::scene::model::hatch_model::HatchPattern)
}
}
+fn hatch_pattern_preview(
+ pattern: crate::scene::model::hatch_model::HatchPattern,
+ height: f32,
+) -> Element<'static, Message> {
+ canvas(HatchPatternPreview {
+ pattern,
+ padding: 4.0,
+ })
+ .width(Length::Fill)
+ .height(height)
+ .into()
+}
+
+pub(crate) fn compact_hatch_pattern_preview(
+ pattern: crate::scene::model::hatch_model::HatchPattern,
+) -> Element<'static, Message> {
+ canvas(HatchPatternPreview {
+ pattern,
+ padding: 0.5,
+ })
+ .width(Length::Fill)
+ .height(ROW_H)
+ .into()
+}
+
fn hatch_pattern_matches(
entry: &crate::scene::model::hatch_patterns::PatternEntry,
search: &str,
@@ -223,6 +252,113 @@ pub(crate) fn filtered_hatch_patterns(
.collect()
}
+/// Messages of a hatch pattern picker; the Properties panel and the Graphic
+/// Attributes palette each route them to their own state.
+pub(crate) struct PatternPickerMessages {
+ pub search_id: &'static str,
+ pub on_search: fn(String) -> Message,
+ pub on_confirm: Message,
+ pub on_focus: fn(usize) -> Message,
+ pub on_changed: fn(String) -> Message,
+}
+
+pub(crate) fn hatch_pattern_picker_content<'a>(
+ search_value: &'a str,
+ focus: usize,
+ current: &'a str,
+ messages: PatternPickerMessages,
+) -> Element<'a, Message> {
+ let PatternPickerMessages {
+ search_id,
+ on_search,
+ on_confirm,
+ on_focus,
+ on_changed,
+ } = messages;
+ let search = text_input(t!("Search patterns…").as_ref(), search_value)
+ .id(iced::widget::Id::new(search_id))
+ .on_input(on_search)
+ .on_submit(on_confirm)
+ .size(FONT_SZ)
+ .padding([5, 7])
+ .width(Length::Fill);
+
+ let mut grid = column![].spacing(6);
+ let visible = filtered_hatch_patterns(search_value);
+ for (row_index, pair) in visible.chunks(2).enumerate() {
+ let mut cards = row![].spacing(6);
+ for (column_index, entry) in pair.iter().enumerate() {
+ let index = row_index * 2 + column_index;
+ let selected = current.eq_ignore_ascii_case(&entry.name);
+ let focused = focus == index;
+ let name = entry.name.clone();
+ let preview = hatch_pattern_preview(entry.gpu.clone(), PATTERN_PREVIEW_H);
+ let card = button(
+ column![
+ preview,
+ container(text(crate::ui::text_util::elide(&entry.name, 20)).size(FONT_SZ))
+ .width(Length::Fill)
+ .align_x(iced::Center),
+ ]
+ .spacing(3),
+ )
+ .on_press(on_changed(name))
+ .style(move |theme: &Theme, status| {
+ let palette = theme.palette();
+ let hovered = matches!(status, button::Status::Hovered | button::Status::Pressed);
+ let pair = if selected {
+ palette.primary.weak
+ } else if hovered || focused {
+ palette.background.strong
+ } else {
+ palette.background.weak
+ };
+ button::Style {
+ background: Some(Background::Color(pair.color)),
+ text_color: pair.text,
+ border: Border {
+ color: if selected || focused {
+ palette.primary.base.color
+ } else {
+ palette.background.neutral.color
+ },
+ width: if selected || focused { 2.0 } else { 1.0 },
+ radius: 4.0.into(),
+ },
+ ..Default::default()
+ }
+ })
+ .padding(5)
+ .width(PATTERN_CARD_W);
+ cards = cards.push(mouse_area(card).on_enter(on_focus(index)));
+ }
+ grid = grid.push(cards);
+ }
+
+ let results: Element<'_, Message> = if visible.is_empty() {
+ container(
+ text(t!("No matching patterns"))
+ .size(FONT_SZ)
+ .style(hint_text_style),
+ )
+ .padding(12)
+ .width(Length::Fill)
+ .center_x(Length::Fill)
+ .into()
+ } else {
+ scrollable(grid)
+ .height(Length::Fill)
+ .width(Length::Fill)
+ .into()
+ };
+ container(column![search, results].spacing(7))
+ .style(container::bordered_box)
+ .padding(8)
+ .width(PATTERN_PICKER_W)
+ .height(Length::Fixed(PATTERN_PICKER_H))
+ .into()
+}
+
/// All standard CAD lineweight options for the combobox.
pub fn lw_options() -> Vec {
[
@@ -1504,99 +1640,24 @@ impl PropertiesPanel {
return prop_row_widget(label, head.into());
}
- let search = text_input(t!("Search patterns…").as_ref(), &self.hatch_pattern_search)
- .id(iced::widget::Id::new("hatch-pattern-search"))
- .on_input(Message::PropHatchPatternSearchChanged)
- .on_submit(Message::PropHatchPatternConfirm)
- .size(FONT_SZ)
- .padding([5, 7])
- .width(Length::Fill);
-
- let mut grid = column![].spacing(6);
- let visible = filtered_hatch_patterns(&self.hatch_pattern_search);
- for (row_index, pair) in visible.chunks(2).enumerate() {
- let mut cards = row![].spacing(6);
- for (column_index, entry) in pair.iter().enumerate() {
- let index = row_index * 2 + column_index;
- let selected = current.eq_ignore_ascii_case(&entry.name);
- let focused = self.hatch_pattern_focus == index;
- let name = entry.name.clone();
- let preview = canvas(HatchPatternPreview {
- pattern: entry.gpu.clone(),
- })
- .width(Length::Fill)
- .height(PATTERN_PREVIEW_H);
- let card = button(
- column![
- preview,
- container(text(crate::ui::text_util::elide(&entry.name, 20)).size(FONT_SZ))
- .width(Length::Fill)
- .align_x(iced::Center),
- ]
- .spacing(3),
- )
- .on_press(Message::PropHatchPatternChanged(name))
- .style(move |theme: &Theme, status| {
- let palette = theme.palette();
- let hovered =
- matches!(status, button::Status::Hovered | button::Status::Pressed);
- let pair = if selected {
- palette.primary.weak
- } else if hovered || focused {
- palette.background.strong
- } else {
- palette.background.weak
- };
- button::Style {
- background: Some(Background::Color(pair.color)),
- text_color: pair.text,
- border: Border {
- color: if selected || focused {
- palette.primary.base.color
- } else {
- palette.background.neutral.color
- },
- width: if selected || focused { 2.0 } else { 1.0 },
- radius: 4.0.into(),
- },
- ..Default::default()
- }
- })
- .padding(5)
- .width(PATTERN_CARD_W);
- cards =
- cards.push(mouse_area(card).on_enter(Message::PropHatchPatternFocus(index)));
- }
- grid = grid.push(cards);
- }
-
- let results: Element<'_, Message> = if visible.is_empty() {
- container(
- text(t!("No matching patterns"))
- .size(FONT_SZ)
- .style(hint_text_style),
- )
- .padding(12)
- .width(Length::Fill)
- .center_x(Length::Fill)
- .into()
- } else {
- scrollable(grid)
- .height(Length::Fill)
- .width(Length::Fill)
- .into()
- };
- let popup = container(column![search, results].spacing(7))
- .style(container::bordered_box)
- .padding(8)
- .width(PATTERN_PICKER_W)
- .height(Length::Fixed(PATTERN_PICKER_H));
+ let popup = hatch_pattern_picker_content(
+ &self.hatch_pattern_search,
+ self.hatch_pattern_focus,
+ current,
+ PatternPickerMessages {
+ search_id: "hatch-pattern-search",
+ on_search: Message::PropHatchPatternSearchChanged,
+ on_confirm: Message::PropHatchPatternConfirm,
+ on_focus: Message::PropHatchPatternFocus,
+ on_changed: Message::PropHatchPatternChanged,
+ },
+ );
prop_row_widget(
label,
crate::ui::color_select::drop_down_below(
head.into(),
- popup.into(),
+ popup,
Some(Length::Fixed(PATTERN_PICKER_W)),
Length::Fixed(PATTERN_PICKER_H),
Message::PropHatchPatternPickerToggle(current.to_string()),
@@ -2158,7 +2219,7 @@ fn text_input_style(theme: &Theme, status: text_input::Status) -> text_input::St
}
}
-fn combo_input_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
+pub(crate) fn combo_input_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
text_input_style(theme, status)
}
diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs
index 3552e17f1..149f4e9a1 100644
--- a/src/ui/ribbon/mod.rs
+++ b/src/ui/ribbon/mod.rs
@@ -62,6 +62,8 @@ pub struct Ribbon {
pub show_count_palette: bool,
/// Sheet Set Manager palette shown (SSMSTATE).
pub show_sheet_set: bool,
+ /// Graphic Attributes panel visibility — drives its ribbon button highlight.
+ pub show_graphic_attributes: bool,
/// File tabs (FILETAB) visibility — drives the File Tabs button highlight.
pub show_file_tabs: bool,
/// Layout tabs (LAYOUTTAB) visibility — drives the Layout Tabs button highlight.
@@ -181,6 +183,7 @@ impl Ribbon {
show_properties: true,
show_count_palette: false,
show_sheet_set: false,
+ show_graphic_attributes: false,
show_file_tabs: true,
show_layout_tabs: true,
open_dropdown: None,
@@ -346,6 +349,9 @@ impl Ribbon {
pub fn set_sheet_set(&mut self, on: bool) {
self.show_sheet_set = on;
}
+ pub fn set_graphic_attributes(&mut self, on: bool) {
+ self.show_graphic_attributes = on;
+ }
pub fn set_file_tabs(&mut self, on: bool) {
self.show_file_tabs = on;
}
@@ -364,6 +370,7 @@ impl Ribbon {
show_ucs_icon: self.show_ucs_icon,
show_properties: self.show_properties,
show_count_palette: self.show_count_palette,
+ show_graphic_attributes: self.show_graphic_attributes,
show_block_palette,
show_sheet_set: self.show_sheet_set,
show_file_tabs: self.show_file_tabs,
diff --git a/src/ui/ribbon/widgets.rs b/src/ui/ribbon/widgets.rs
index 8133405f4..989ea879b 100644
--- a/src/ui/ribbon/widgets.rs
+++ b/src/ui/ribbon/widgets.rs
@@ -38,6 +38,7 @@ pub(super) struct ToggleState {
pub show_ucs_icon: bool,
pub show_properties: bool,
pub show_count_palette: bool,
+ pub show_graphic_attributes: bool,
pub show_block_palette: bool,
pub show_sheet_set: bool,
pub show_file_tabs: bool,
@@ -370,6 +371,7 @@ pub(super) fn is_active_tool(id: &str, active_tool: &Option, state: &Tog
"UCSICON" => state.show_ucs_icon,
"PROPERTIES" => state.show_properties,
"COUNTLIST" => state.show_count_palette,
+ "GRAPHICATTRIBUTES" => state.show_graphic_attributes,
"BLOCKPALETTE" => state.show_block_palette,
"SHEETSET" => state.show_sheet_set,
"FILETAB" => state.show_file_tabs,
diff --git a/src/ui/window/gradient_editor.rs b/src/ui/window/gradient_editor.rs
new file mode 100644
index 000000000..8c5622785
--- /dev/null
+++ b/src/ui/window/gradient_editor.rs
@@ -0,0 +1,498 @@
+//! Gradient flyout of the Graphic Attributes palette.
+
+use crate::app::Message;
+use crate::scene::model::hatch_model::GradientKind;
+use crate::ui::window::graphic_attributes::GraphicAttributesMsg;
+use codec::types::Color as AcadColor;
+use iced::widget::{
+ button, checkbox, column, container, image, mouse_area, row, slider, text, text_input, tooltip,
+ Space,
+};
+use iced::{Background, Border, Color, Element, Fill, Length, Theme};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum GradientColorMode {
+ One,
+ Two,
+}
+
+#[derive(Debug, Clone)]
+pub struct GradientEditorState {
+ pub handles: Vec,
+ pub color_mode: GradientColorMode,
+ pub color_1: AcadColor,
+ pub color_2: AcadColor,
+ pub shade_tint: f32,
+ pub kind: GradientKind,
+ pub inverted: bool,
+ pub angle: String,
+ pub centered: bool,
+ /// Large preview, rebuilt only when a setting it depends on changes.
+ preview: std::cell::RefCell