From 1a6279e84164d3d356f41bfcd02d495e4293f3f7 Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Thu, 24 Sep 2026 16:40:51 +0100 Subject: [PATCH] feat(blocks): implement WBLOCK dialog and unify dialog buttons and radio builders --- locales/ar-SA/opencadstudio.ftl | 10 + locales/bg-BG/opencadstudio.ftl | 10 + locales/cs-CZ/opencadstudio.ftl | 10 + locales/de-DE/opencadstudio.ftl | 10 + locales/el-GR/opencadstudio.ftl | 10 + locales/en-US/opencadstudio.ftl | 11 + locales/es-ES/opencadstudio.ftl | 10 + locales/fi-FI/opencadstudio.ftl | 10 + locales/fr-FR/opencadstudio.ftl | 10 + locales/hi-IN/opencadstudio.ftl | 10 + locales/hu-HU/opencadstudio.ftl | 10 + locales/it-IT/opencadstudio.ftl | 10 + locales/ja-JP/opencadstudio.ftl | 10 + locales/ko-KR/opencadstudio.ftl | 10 + locales/nl-NL/opencadstudio.ftl | 10 + locales/pl-PL/opencadstudio.ftl | 10 + locales/pt-BR/opencadstudio.ftl | 10 + locales/ru-RU/opencadstudio.ftl | 10 + locales/tr-TR/opencadstudio.ftl | 10 + locales/zh-CN/opencadstudio.ftl | 10 + locales/zh-TW/opencadstudio.ftl | 10 + src/app/commands/blocks.rs | 94 +++++- src/app/mod.rs | 22 ++ src/app/update/command.rs | 2 + src/app/update/mod.rs | 315 +++++++++++++++++++ src/app/view/modal.rs | 7 + src/locale_catalog.rs | 11 + src/modules/insert/wblock.rs | 106 +++++++ src/ui/style/form.rs | 36 ++- src/ui/style/point_style.rs | 4 +- src/ui/window/block_definition.rs | 52 +--- src/ui/window/mod.rs | 1 + src/ui/window/wblock.rs | 492 ++++++++++++++++++++++++++++++ 33 files changed, 1308 insertions(+), 45 deletions(-) create mode 100644 src/ui/window/wblock.rs diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index cadbbe11d..d9b145dcb 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -2316,6 +2316,15 @@ common = .specify-on-screen = تحديد على الشاشة .to-layout = مع التخطيط .us-survey-feet = أقدام مسح أمريكية + .entire-drawing = الرسم بالكامل + .delete-from-drawing = حذف من الرسم + .destination = الوجهة + .file-name-and-path = اسم الملف والمسار: + .insert-units-label = وحدات الإدراج: + .wblock-err-no-block-name = يرجى تحديد أو إدخال اسم الكتلة. + .wblock-err-block-not-found = الكتلة "__ocs_fmt_0__" غير موجودة في الرسم. + .wblock-err-no-path = يرجى تحديد اسم الملف والمسار. + .wblock-help-info = يقوم WBLOCK بكتابة كائنات أو كتلة أو الرسم بالكامل إلى ملف رسم جديد. errors = .save-warning = تحذير الحفظ .unable-to-save-drawing = تعذر حفظ الرسم @@ -3824,6 +3833,7 @@ insert = .xref = XREF: __ocs_fmt_0__ .block-select-objects = BLOCK حدد الكائنات: .block-specify-insertion-base-point = BLOCK حدد نقطة قاعدة الإدراج: + .wblock-specify-insertion-base-point = WBLOCK حدد نقطة أساس الإدراج: model = .create = إنشاء .boolean = عمليات منطقية diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index cedb34c04..dbb285280 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -2303,6 +2303,15 @@ common = .specify-on-screen = Указване на екрана .to-layout = с оформлението .us-survey-feet = Геодезически фута (САЩ) + .entire-drawing = Цял чертеж + .delete-from-drawing = Изтриване от чертежа + .destination = Местоназначение + .file-name-and-path = Име и път на файла: + .insert-units-label = Мерни единици за вмъкване: + .wblock-err-no-block-name = Моля, изберете или въведете име на блок. + .wblock-err-block-not-found = Блокът "__ocs_fmt_0__" не съществува в чертежа. + .wblock-err-no-path = Моля, задайте име и път на файла. + .wblock-help-info = WBLOCK записва обекти, блок или целия чертеж в нов чертожен файл. errors = .save-warning = Предупреждение при запазване .unable-to-save-drawing = Чертежът не може да бъде запазен @@ -3811,6 +3820,7 @@ insert = .xref = XREF: __ocs_fmt_0__ .block-select-objects = BLOCK Изберете обекти: .block-specify-insertion-base-point = BLOCK Укажете базова точка на вмъкване: + .wblock-specify-insertion-base-point = WBLOCK Задайте базова точка на вмъкване: model = .create = Създаване .boolean = Булеви операции diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index cd454c24d..577daa7f7 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -2303,6 +2303,15 @@ common = .specify-on-screen = Zadat na obrazovce .to-layout = rozvržení .us-survey-feet = Zeměměřické stopy (USA) + .entire-drawing = Celý výkres + .delete-from-drawing = Vymazat z výkresu + .destination = Cíl + .file-name-and-path = Název souboru a cesta: + .insert-units-label = Jednotky vložení: + .wblock-err-no-block-name = Vyberte nebo zadejte název bloku. + .wblock-err-block-not-found = Blok "__ocs_fmt_0__" ve výkresu neexistuje. + .wblock-err-no-path = Zadejte název souboru a cestu. + .wblock-help-info = WBLOCK zapíše objekty, blok nebo celý výkres do nového výkresového souboru. errors = .save-warning = Uložit varování .unable-to-save-drawing = Nákres nelze uložit @@ -3809,6 +3818,7 @@ insert = .xref = XREF: __ocs_fmt_0__ .block-select-objects = BLOCK Vyberte objekty: .block-specify-insertion-base-point = BLOCK Zadejte základní bod vložení: + .wblock-specify-insertion-base-point = WBLOCK Určete základní bod vložení: model = .create = Vytvořit .boolean = Booleovské operace diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 59b292a4e..179e5ad89 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -2304,6 +2304,15 @@ common = .specify-on-screen = Am Bildschirm bestimmen .to-layout = an Layout .us-survey-feet = US-Vermessungsfuß + .entire-drawing = Gesamte Zeichnung + .delete-from-drawing = Aus Zeichnung löschen + .destination = Ziel + .file-name-and-path = Dateiname und Pfad: + .insert-units-label = Einfüge-Einheiten: + .wblock-err-no-block-name = Bitte wählen Sie einen Blocknamen aus oder geben Sie ihn ein. + .wblock-err-block-not-found = Block "__ocs_fmt_0__" ist in der Zeichnung nicht vorhanden. + .wblock-err-no-path = Bitte geben Sie einen Dateinamen und Pfad an. + .wblock-help-info = WBLOCK schreibt Objekte, einen Block oder die gesamte Zeichnung in eine neue Zeichnungsdatei. errors = .save-warning = Warnhinweise speichern .unable-to-save-drawing = Zeichnung konnte nicht gespeichert werden @@ -3810,6 +3819,7 @@ insert = .xref = XREF: __ocs_fmt_0__ .block-select-objects = BLOCK Objekte auswählen: .block-specify-insertion-base-point = BLOCK Einfügebasispunkt angeben: + .wblock-specify-insertion-base-point = WBLOCK Basispunkt für Einfügung angeben: model = .create = Erstellen .boolean = Boolesche Operationen diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 6c3de1640..b3c9add94 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -2303,6 +2303,15 @@ common = .specify-on-screen = Καθορισμός στην οθόνη .to-layout = με τη διάταξη .us-survey-feet = Τοπογραφικά πόδια ΗΠΑ + .entire-drawing = Ολόκληρο σχέδιο + .delete-from-drawing = Διαγραφή από το σχέδιο + .destination = Προορισμός + .file-name-and-path = Όνομα και διαδρομή αρχείου: + .insert-units-label = Μονάδες εισαγωγής: + .wblock-err-no-block-name = Επιλέξτε ή πληκτρολογήστε ένα όνομα μπλοκ. + .wblock-err-block-not-found = Το μπλοκ "__ocs_fmt_0__" δεν υπάρχει στο σχέδιο. + .wblock-err-no-path = Καθορίστε ένα όνομα και διαδρομή αρχείου. + .wblock-help-info = Η εντολή WBLOCK εγγράφει αντικείμενα, ένα μπλοκ ή ολόκληρο το σχέδιο σε ένα νέο αρχείο σχεδίου. errors = .save-warning = Προειδοποίηση αποθήκευσης .unable-to-save-drawing = Αδυναμία αποθήκευσης σχεδίου @@ -3812,6 +3821,7 @@ insert = .xref = XREF: __ocs_fmt_0__ .block-select-objects = BLOCK Επιλέξτε αντικείμενα: .block-specify-insertion-base-point = BLOCK Καθορίστε σημείο βάσης εισαγωγής: + .wblock-specify-insertion-base-point = WBLOCK Καθορίστε σημείο βάσης εισαγωγής: model = .create = Δημιουργία .boolean = Δυαδική πράξη diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index e94c1ed7a..109c504f8 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -2304,6 +2304,16 @@ common = .specify-on-screen = Specify On-screen .to-layout = to layout .us-survey-feet = US Survey Feet + .entire-drawing = Entire drawing + .delete-from-drawing = Delete from drawing + .destination = Destination + .file-name-and-path = File name and path: + .insert-units-label = Insert units: + .wblock-err-no-block-name = Please select or enter a block name. + .wblock-err-block-not-found = Block "__ocs_fmt_0__" does not exist in drawing. + .wblock-err-no-path = Please specify a file name and path. + .wblock-err-current-drawing = Cannot write to the current drawing file. + .wblock-help-info = WBLOCK writes objects, a block, or the entire drawing to a new drawing file. errors = .save-warning = Save Warning .unable-to-save-drawing = Unable to Save Drawing @@ -3813,6 +3823,7 @@ insert = .xref = XREF: __ocs_fmt_0__ .block-select-objects = BLOCK Select objects: .block-specify-insertion-base-point = BLOCK Specify insertion base point: + .wblock-specify-insertion-base-point = WBLOCK Specify insertion base point: model = .create = Create .boolean = Boolean diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 844d251ef..1ac9bbb7c 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -2304,6 +2304,15 @@ common = .specify-on-screen = Precisar en pantalla .to-layout = a la presentación .us-survey-feet = Pies de agrimensura de EE. UU. + .entire-drawing = Dibujo completo + .delete-from-drawing = Suprimir del dibujo + .destination = Destino + .file-name-and-path = Nombre de archivo y ruta: + .insert-units-label = Unidades de inserción: + .wblock-err-no-block-name = Seleccione o escriba un nombre de bloque. + .wblock-err-block-not-found = El bloque "__ocs_fmt_0__" no existe en el dibujo. + .wblock-err-no-path = Especifique un nombre de archivo y una ruta. + .wblock-help-info = WBLOCK escribe objetos, un bloque o el dibujo completo en un nuevo archivo de dibujo. errors = .save-warning = Advertencia de guardado .unable-to-save-drawing = No se puede guardar el dibujo @@ -3812,6 +3821,7 @@ insert = .xref = XREF: __ocs_fmt_0__ .block-select-objects = BLOCK Designar objetos: .block-specify-insertion-base-point = BLOCK Precise punto base de inserción: + .wblock-specify-insertion-base-point = WBLOCK Precise punto base de inserción: model = .create = Crear .boolean = Booleanas diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index f509bd21a..e0dbb5dfb 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -2303,6 +2303,15 @@ common = .specify-on-screen = Määritä näytöltä .to-layout = asetteluun .us-survey-feet = US Survey -jalat + .entire-drawing = Koko piirustus + .delete-from-drawing = Poista piirustuksesta + .destination = Kohde + .file-name-and-path = Tiedoston nimi ja polku: + .insert-units-label = Lisäysyksiköt: + .wblock-err-no-block-name = Valitse tai anna lohkon nimi. + .wblock-err-block-not-found = Lohkoa "__ocs_fmt_0__" ei ole piirustuksessa. + .wblock-err-no-path = Määritä tiedoston nimi ja polku. + .wblock-help-info = WBLOCK kirjoittaa kohteet, lohkon tai koko piirustuksen uuteen piirustustiedostoon. errors = .save-warning = Tallenna varoitus .unable-to-save-drawing = Piirustusta ei voi tallentaa @@ -3811,6 +3820,7 @@ insert = .xref = XREF: __ocs_fmt_0__ .block-select-objects = BLOCK Valitse kohteet: .block-specify-insertion-base-point = BLOCK Määritä sijoituksen peruspiste: + .wblock-specify-insertion-base-point = WBLOCK Määritä lisäyksen peruspiste: model = .create = Luo .boolean = Boolen operaatiot diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 2c9308c48..d538ca9e5 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -2304,6 +2304,15 @@ common = .specify-on-screen = Spécifier à l'écran .to-layout = sur la présentation .us-survey-feet = Pieds d'arpentage US + .entire-drawing = Dessin entier + .delete-from-drawing = Supprimer du dessin + .destination = Destination + .file-name-and-path = Nom de fichier et chemin d'accès : + .insert-units-label = Unités d'insertion : + .wblock-err-no-block-name = Veuillez sélectionner ou entrer un nom de bloc. + .wblock-err-block-not-found = Le bloc « __ocs_fmt_0__ » n'existe pas dans le dessin. + .wblock-err-no-path = Veuillez spécifier un nom de fichier et un chemin d'accès. + .wblock-help-info = WBLOCK écrit des objets, un bloc ou l'ensemble du dessin dans un nouveau fichier de dessin. errors = .save-warning = Enregistrer l' avertissement .unable-to-save-drawing = Impossible de sauvegarder le dessin @@ -3812,6 +3821,7 @@ insert = .xref = XREF: __ocs_fmt_0__ .block-select-objects = BLOCK Sélectionnez les objets : .block-specify-insertion-base-point = BLOCK Spécifiez le point de base d'insertion : + .wblock-specify-insertion-base-point = WBLOCK Spécifiez le point de base d'insertion : model = .create = Créer .boolean = Opérations booléennes diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index b6c73882a..32af12c50 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -2292,6 +2292,15 @@ common = .specify-on-screen = स्क्रीन पर निर्दिष्ट करें .to-layout = लेआउट के अनुसार .us-survey-feet = यूएस सर्वेक्षण फीट + .entire-drawing = संपूर्ण रेखाचित्र + .delete-from-drawing = रेखाचित्र से हटाएं + .destination = गंतव्य + .file-name-and-path = फ़ाइल का नाम और पथ: + .insert-units-label = सम्मिलन इकाइयाँ: + .wblock-err-no-block-name = कृपया एक ब्लॉक नाम चुनें या दर्ज करें। + .wblock-err-block-not-found = ब्लॉक "__ocs_fmt_0__" रेखाचित्र में मौजूद नहीं है। + .wblock-err-no-path = कृपया फ़ाइल का नाम और पथ निर्दिष्ट करें। + .wblock-help-info = WBLOCK ऑब्जेक्ट, एक ब्लॉक, या संपूर्ण रेखाचित्र को एक नई रेखाचित्र फ़ाइल में लिखता है। errors = .save-warning = चेतावनी सहेजें .unable-to-save-drawing = ड्रॉइंग सहेजी नहीं जा सकी @@ -3800,6 +3809,7 @@ insert = .xref = XREF: __ocs_fmt_0__ .block-select-objects = BLOCK ऑब्जेक्ट चुनें: .block-specify-insertion-base-point = BLOCK सम्मिलन आधार बिंदु निर्दिष्ट करें: + .wblock-specify-insertion-base-point = WBLOCK सम्मिलन आधार बिंदु निर्दिष्ट करें: model = .create = बनाएँ .boolean = बूलियन संक्रियाएँ diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index fe848f2b9..73628dd4a 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -2303,6 +2303,15 @@ common = .specify-on-screen = Megadás a képernyőn .to-layout = az elrendezéshez .us-survey-feet = USA geodéziai láb + .entire-drawing = Teljes rajz + .delete-from-drawing = Törlés a rajzból + .destination = Cél + .file-name-and-path = Fájlnév és elérési út: + .insert-units-label = Beillesztési mértékegységek: + .wblock-err-no-block-name = Válasszon vagy adjon meg egy blokknevet. + .wblock-err-block-not-found = A(z) "__ocs_fmt_0__" blokk nem létezik a rajzban. + .wblock-err-no-path = Adja meg a fájlnevet és az elérési utat. + .wblock-help-info = A WBLOCK objektumokat, egy blokkot vagy a teljes rajzot új rajzfájlba menti. errors = .save-warning = Figyelmeztetés mentése .unable-to-save-drawing = Nem sikerült menteni a rajzot @@ -3811,6 +3820,7 @@ insert = .xref = XREF: __ocs_fmt_0__ .block-select-objects = BLOCK Válasszon objektumokat: .block-specify-insertion-base-point = BLOCK Adja meg a beillesztési bázispontot: + .wblock-specify-insertion-base-point = WBLOCK Adja meg a beillesztési bázispontot: model = .create = Létrehozás .boolean = Logikai műveletek diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 10aafa0c6..c92c3f40a 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -2303,6 +2303,15 @@ common = .specify-on-screen = Specifica sullo schermo .to-layout = al layout .us-survey-feet = Piedi topografici USA + .entire-drawing = Intero disegno + .delete-from-drawing = Elimina dal disegno + .destination = Destinazione + .file-name-and-path = Nome file e percorso: + .insert-units-label = Unità di inserimento: + .wblock-err-no-block-name = Selezionare o immettere il nome di un blocco. + .wblock-err-block-not-found = Il blocco "__ocs_fmt_0__" non esiste nel disegno. + .wblock-err-no-path = Specificare un nome file e un percorso. + .wblock-help-info = WBLOCK scrive gli oggetti, un blocco o l'intero disegno in un nuovo file di disegno. errors = .save-warning = Salva avviso .unable-to-save-drawing = Impossibile salvare il disegno @@ -3811,6 +3820,7 @@ insert = .xref = XREF: __ocs_fmt_0__ .block-select-objects = BLOCK Selezionare oggetti: .block-specify-insertion-base-point = BLOCK Specificare punto base di inserimento: + .wblock-specify-insertion-base-point = WBLOCK Specificare il punto base di inserimento: model = .create = Crea .boolean = Operazioni booleane diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 7c5fc024d..6c46e9cf1 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -2294,6 +2294,15 @@ common = .specify-on-screen = 画面上で指定 .to-layout = レイアウトに合わせる .us-survey-feet = 米国測量フィート + .entire-drawing = 図面全体 + .delete-from-drawing = 図面から削除 + .destination = 保存先 + .file-name-and-path = ファイル名とパス: + .insert-units-label = 挿入単位: + .wblock-err-no-block-name = ブロック名を選択または入力してください。 + .wblock-err-block-not-found = ブロック "__ocs_fmt_0__" は図面に存在しません。 + .wblock-err-no-path = ファイル名とパスを指定してください。 + .wblock-help-info = WBLOCK はオブジェクト、ブロック、または図面全体を新しい図面ファイルに書き出します。 errors = .save-warning = 保存の警告 .unable-to-save-drawing = 図面を保存できません @@ -3800,6 +3809,7 @@ insert = .xref = XREF: __ocs_fmt_0__ .block-select-objects = BLOCK オブジェクトを選択: .block-specify-insertion-base-point = BLOCK 挿入基点を指定: + .wblock-specify-insertion-base-point = WBLOCK 挿入基点を指定: model = .create = 作成 .boolean = ブール演算 diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index fa67e6afb..ee36b001b 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -2303,6 +2303,15 @@ common = .specify-on-screen = 화면상에 지정 .to-layout = 배치에 맞춤 .us-survey-feet = 미국 측량 피트 + .entire-drawing = 도면 전체 + .delete-from-drawing = 도면에서 삭제 + .destination = 대상 + .file-name-and-path = 파일 이름 및 경로: + .insert-units-label = 삽입 단위: + .wblock-err-no-block-name = 블록 이름을 선택하거나 입력하십시오. + .wblock-err-block-not-found = 블록 "__ocs_fmt_0__"이(가) 도면에 존재하지 않습니다. + .wblock-err-no-path = 파일 이름과 경로를 지정하십시오. + .wblock-help-info = WBLOCK은 객체, 블록 또는 도면 전체를 새 도면 파일로 내보냅니다. errors = .save-warning = 경고 저장 .unable-to-save-drawing = 도면을 저장할 수 없습니다 @@ -3811,6 +3820,7 @@ insert = .xref = XREF: __ocs_fmt_0__ .block-select-objects = BLOCK 객체 선택: .block-specify-insertion-base-point = BLOCK 삽입 기준점 지정: + .wblock-specify-insertion-base-point = WBLOCK 삽입 기준점 지정: model = .create = 생성 .boolean = 불리언 연산 diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 51160a44c..f162e7301 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -2304,6 +2304,15 @@ common = .specify-on-screen = Op scherm opgeven .to-layout = aan lay-out .us-survey-feet = US Survey Feet + .entire-drawing = Volledige tekening + .delete-from-drawing = Verwijderen uit tekening + .destination = Bestemming + .file-name-and-path = Bestandsnaam en pad: + .insert-units-label = Invoegeenheden: + .wblock-err-no-block-name = Selecteer of voer een bloknaam in. + .wblock-err-block-not-found = Blok "__ocs_fmt_0__" bestaat niet in de tekening. + .wblock-err-no-path = Geef een bestandsnaam en pad op. + .wblock-help-info = WBLOCK schrijft objecten, een blok of de volledige tekening naar een nieuw tekeningbestand. errors = .save-warning = Waarschuwing opslaan .unable-to-save-drawing = Kan tekening niet opslaan @@ -3810,6 +3819,7 @@ insert = .xref = XREF: __ocs_fmt_0__ .block-select-objects = BLOCK Objecten selecteren: .block-specify-insertion-base-point = BLOCK Invoegbasispunt opgeven: + .wblock-specify-insertion-base-point = WBLOCK Geef basisinvoegpunt op: model = .create = Maken .boolean = Booleaanse bewerkingen diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 6bf699cdf..1937bc168 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -2303,6 +2303,15 @@ common = .specify-on-screen = Określ na ekranie .to-layout = do układu .us-survey-feet = Stopy geodezyjne USA + .entire-drawing = Cały rysunek + .delete-from-drawing = Usuń z rysunku + .destination = Cel + .file-name-and-path = Nazwa pliku i ścieżka: + .insert-units-label = Jednostki wstawiania: + .wblock-err-no-block-name = Wybierz lub wprowadź nazwę bloku. + .wblock-err-block-not-found = Blok „__ocs_fmt_0__” nie istnieje w rysunku. + .wblock-err-no-path = Podaj nazwę pliku i ścieżkę. + .wblock-help-info = WBLOCK zapisuje obiekty, blok lub cały rysunek do nowego pliku rysunku. errors = .save-warning = Zapisz ostrzeżenie .unable-to-save-drawing = Nie można zapisać rysunku @@ -3811,6 +3820,7 @@ insert = .xref = XREF: __ocs_fmt_0__ .block-select-objects = BLOCK Wybierz obiekty: .block-specify-insertion-base-point = BLOCK Określ bazowy punkt wstawienia: + .wblock-specify-insertion-base-point = WBLOCK Określ punkt bazowy wstawienia: model = .create = Utwórz .boolean = Operacje boolowskie diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index f07f8a290..7612ea5f1 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -2304,6 +2304,15 @@ common = .specify-on-screen = Especificar na tela .to-layout = ao layout .us-survey-feet = Pés topográficos dos EUA + .entire-drawing = Desenho inteiro + .delete-from-drawing = Excluir do desenho + .destination = Destino + .file-name-and-path = Nome do arquivo e caminho: + .insert-units-label = Unidades de inserção: + .wblock-err-no-block-name = Selecione ou insira o nome de um bloco. + .wblock-err-block-not-found = O bloco "__ocs_fmt_0__" não existe no desenho. + .wblock-err-no-path = Especifique um nome de arquivo e caminho. + .wblock-help-info = O WBLOCK grava objetos, um bloco ou o desenho inteiro em um novo arquivo de desenho. errors = .save-warning = Aviso de salvamento .unable-to-save-drawing = Não foi possível salvar o desenho @@ -3812,6 +3821,7 @@ insert = .xref = XREF: __ocs_fmt_0__ .block-select-objects = BLOCK Selecionar objetos: .block-specify-insertion-base-point = BLOCK Especificar ponto base de inserção: + .wblock-specify-insertion-base-point = WBLOCK Especifique o ponto base de inserção: model = .create = Criar .boolean = Operações booleanas diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 79b40d786..ab6a4a674 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -2307,6 +2307,15 @@ common = .specify-on-screen = Указать на экране .to-layout = с листом .us-survey-feet = Геодезические футы США + .entire-drawing = Весь чертеж + .delete-from-drawing = Удалить из чертежа + .destination = Место назначения + .file-name-and-path = Имя файла и путь: + .insert-units-label = Единицы вставки: + .wblock-err-no-block-name = Выберите или введите имя блока. + .wblock-err-block-not-found = Блок "__ocs_fmt_0__" отсутствует в чертеже. + .wblock-err-no-path = Укажите имя файла и путь к нему. + .wblock-help-info = WBLOCK записывает объекты, блок или весь чертеж в новый файл чертежа. errors = .save-warning = Спасти предостережение .unable-to-save-drawing = Не удалось сохранить чертёж @@ -3813,6 +3822,7 @@ insert = .xref = XREF: __ocs_fmt_0__ .block-select-objects = BLOCK Выберите объекты: .block-specify-insertion-base-point = BLOCK Укажите базовую точку вставки: + .wblock-specify-insertion-base-point = WBLOCK Укажите базовую точку вставки: model = .create = Создание .boolean = Булевы операции diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 0469280c5..42c37f657 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -2288,6 +2288,15 @@ common = .specify-on-screen = Ekranda Belirt .to-layout = düzene .us-survey-feet = ABD Ölçüm Fiti + .entire-drawing = Tüm çizim + .delete-from-drawing = Çizimden sil + .destination = Hedef + .file-name-and-path = Dosya adı ve yolu: + .insert-units-label = Ekleme birimleri: + .wblock-err-no-block-name = Lütfen bir blok adı seçin veya girin. + .wblock-err-block-not-found = "__ocs_fmt_0__" bloğu çizimde mevcut değil. + .wblock-err-no-path = Lütfen bir dosya adı ve yolu belirtin. + .wblock-help-info = WBLOCK nesneleri, bir bloğu veya tüm çizimi yeni bir çizim dosyasına yazar. errors = .save-warning = Kaydetme Uyarısı .unable-to-save-drawing = Çizim Kaydedilemiyor @@ -3772,6 +3781,7 @@ insert = .xref = XREF: __ocs_fmt_0__ .block-select-objects = BLOCK Nesneleri seçin: .block-specify-insertion-base-point = BLOCK Ekleme taban noktasını belirtin: + .wblock-specify-insertion-base-point = WBLOCK Ekleme taban noktasını belirleyin: model = .create = Oluştur .boolean = Boole işlemleri diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 4021c3982..1dccd7669 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -2291,6 +2291,15 @@ common = .specify-on-screen = 在屏幕上指定 .to-layout = 匹配到布局 .us-survey-feet = 美国测量英尺 + .entire-drawing = 整个图形 + .delete-from-drawing = 从图形中删除 + .destination = 目标 + .file-name-and-path = 文件名和路径: + .insert-units-label = 插入单位: + .wblock-err-no-block-name = 请选择或输入块名称。 + .wblock-err-block-not-found = 图形中不存在块“__ocs_fmt_0__”。 + .wblock-err-no-path = 请指定文件名和路径。 + .wblock-help-info = WBLOCK 将对象、块或整个图形写入新的图形文件。 errors = .save-warning = 保存警告 .unable-to-save-drawing = 无法保存图形 @@ -3795,6 +3804,7 @@ insert = .xref = XREF: __ocs_fmt_0__ .block-select-objects = BLOCK 选择对象: .block-specify-insertion-base-point = BLOCK 指定插入基点: + .wblock-specify-insertion-base-point = WBLOCK 指定插入基点: model = .create = 创建 .boolean = 布尔运算 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 0e231fa6f..8edf0dacf 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -2303,6 +2303,15 @@ common = .specify-on-screen = 在螢幕上指定 .to-layout = 與配置相符 .us-survey-feet = 美國測量英呎 + .entire-drawing = 整個圖面 + .delete-from-drawing = 從圖面中刪除 + .destination = 目標 + .file-name-and-path = 檔名和路徑: + .insert-units-label = 插入單位: + .wblock-err-no-block-name = 請選取或輸入圖塊名稱。 + .wblock-err-block-not-found = 圖面中不存在圖塊「__ocs_fmt_0__」。 + .wblock-err-no-path = 請指定檔名和路徑。 + .wblock-help-info = WBLOCK 將物件、圖塊或整個圖面寫入新的圖面檔案。 errors = .save-warning = 儲存警告 .unable-to-save-drawing = 無法儲存圖形 @@ -3807,6 +3816,7 @@ insert = .xref = XREF: __ocs_fmt_0__ .block-select-objects = BLOCK 選取物件: .block-specify-insertion-base-point = BLOCK 指定插入基準點: + .wblock-specify-insertion-base-point = WBLOCK 指定插入基準點: model = .create = 建立 .boolean = 布林運算 diff --git a/src/app/commands/blocks.rs b/src/app/commands/blocks.rs index 0015f45c7..1cfd6df3b 100644 --- a/src/app/commands/blocks.rs +++ b/src/app/commands/blocks.rs @@ -460,6 +460,41 @@ impl OpenCADStudio { self.active_modal = Some(crate::app::ModalKind::BlockDefinition); } + cmd if cmd.starts_with("WBLOCK_POINT_PICKED ") => { + let parts: Vec = cmd["WBLOCK_POINT_PICKED ".len()..] + .split_whitespace() + .filter_map(|s| s.parse::().ok()) + .collect(); + if parts.len() == 3 { + if let Some(state) = self.wblock.as_mut() { + state.base_point_x = format!("{:.4}", parts[0]); + state.base_point_y = format!("{:.4}", parts[1]); + state.base_point_z = format!("{:.4}", parts[2]); + } + } + self.tabs[i].active_cmd = None; + self.active_modal = Some(crate::app::ModalKind::WriteBlock); + } + + "WBLOCK_POINT_CANCELLED" => { + self.tabs[i].active_cmd = None; + self.active_modal = Some(crate::app::ModalKind::WriteBlock); + } + + "WBLOCK_OBJECTS_GATHERED" => { + let handles: Vec<_> = self.tabs[i] + .scene + .selected_entities() + .into_iter() + .map(|(h, _)| h) + .collect(); + if let Some(state) = self.wblock.as_mut() { + state.selected_handles = handles; + } + self.tabs[i].active_cmd = None; + self.active_modal = Some(crate::app::ModalKind::WriteBlock); + } + "INSERT" => { let blocks = self.tabs[i].scene.custom_block_names(); if blocks.is_empty() { @@ -663,9 +698,60 @@ impl OpenCADStudio { "PDFATTACH" => { return Some(Task::done(Message::PdfAttachPick)); } - cmd if cmd == "WBLOCK" || cmd == "WB" || cmd.starts_with("WBLOCK ") => { - let arg = cmd.splitn(2, ' ').nth(1).unwrap_or("").trim(); - if arg.is_empty() { + "XATTACH" => { + // Launch the file picker; XAttachPickResult will start the command. + return Some(Task::done(Message::XAttachPick)); + } + "WBLOCK" | "WB" => { + let handles: Vec<_> = self.tabs[i] + .scene + .selected_entities() + .into_iter() + .map(|(h, _)| h) + .collect(); + let existing_blocks = self.tabs[i].scene.custom_block_names(); + let default_unit = self.tabs[i].scene.document.header.insertion_units; + let default_folder = self.tabs[i] + .current_path + .as_ref() + .and_then(|p| p.parent()) + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_default(); + let default_path = if default_folder.is_empty() { + "new_block.dwg".to_string() + } else { + format!("{}/new_block.dwg", default_folder.replace('\\', "/")) + }; + self.wblock = Some( + crate::ui::window::wblock::WblockState::new( + existing_blocks, + handles, + default_unit, + default_path, + ), + ); + self.active_modal = Some(crate::app::ModalKind::WriteBlock); + } + + cmd if cmd == "-WBLOCK" + || cmd == "-WB" + || cmd.starts_with("-WBLOCK ") + || cmd.starts_with("-WB ") + || cmd.starts_with("WBLOCK ") + || cmd.starts_with("WB ") => + { + let rest = if let Some(r) = cmd.strip_prefix("-WBLOCK") { + r.trim() + } else if let Some(r) = cmd.strip_prefix("-WB") { + r.trim() + } else if let Some(r) = cmd.strip_prefix("WBLOCK") { + r.trim() + } else if let Some(r) = cmd.strip_prefix("WB") { + r.trim() + } else { + "" + }; + if rest.is_empty() { // No argument: use selected entities (*) if any, else ask. let sel: Vec<_> = self.tabs[i].scene.selected.iter().copied().collect(); if sel.is_empty() { @@ -676,7 +762,7 @@ impl OpenCADStudio { return Some(Task::done(Message::WblockSave("*".to_string()))); } } else { - return Some(Task::done(Message::WblockSave(arg.to_string()))); + return Some(Task::done(Message::WblockSave(rest.to_string()))); } } diff --git a/src/app/mod.rs b/src/app/mod.rs index f37777827..9f5e455c2 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -495,6 +495,8 @@ pub(super) struct OpenCADStudio { block_definition: Option, /// Working copy of the Attach External Reference dialog; None while closed. xref_attach: Option, + /// Working copy of the Write Block (WBLOCK) dialog; `None` while it is closed. + wblock: Option, /// Working copy of the structured feature-control-frame editor. geometric_tolerance: Option, /// PICKDRAG (#226): false (default) = press-drag lassoes; true = @@ -1883,6 +1885,7 @@ pub enum ModalKind { DrawingUnits, BlockDefinition, XrefAttach, + WriteBlock, GeometricTolerance, DraftingSettings, AutoConstrainSettings, @@ -2880,6 +2883,24 @@ pub enum Message { BlockDefConfirmRedefine(bool), BlockDefDismissError, BlockDefHelp, + /// Write Block (WBLOCK) dialog messages + WblockSourceMode(crate::ui::window::wblock::WblockSourceMode), + WblockBlockName(String), + WblockBlockSelect(String), + WblockPickPoint, + WblockBaseX(String), + WblockBaseY(String), + WblockBaseZ(String), + WblockSelectObjects, + WblockQuickSelect, + WblockObjectMode(crate::ui::window::wblock::WblockObjectMode), + WblockFilePath(String), + WblockBrowsePath, + WblockBrowsePathResult(Option), + WblockUnit(i16), + WblockApply, + WblockDismissError, + WblockHelp, /// One structured feature-control-frame field changed. ToleranceDialogField(crate::ui::window::geometric_tolerance::Field), /// One structured feature-control-frame option changed. @@ -3962,6 +3983,7 @@ impl OpenCADStudio { drawing_units: None, block_definition: None, xref_attach: None, + wblock: None, geometric_tolerance: None, pick_drag_rect: false, perf_hud: false, diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 20770f8df..78b9be13c 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1163,6 +1163,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { self.reset_modal_geometry(); if self.block_definition.is_some() { self.active_modal = Some(crate::app::ModalKind::BlockDefinition); + } else if self.wblock.is_some() { + self.active_modal = Some(crate::app::ModalKind::WriteBlock); } return Task::none(); } diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 0b80312f5..9e3e52580 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -209,6 +209,7 @@ impl OpenCADStudio { Some(GeometricTolerance) => self.geometric_tolerance = None, Some(BlockDefinition) => self.block_definition = None, Some(XrefAttach) => self.xref_attach = None, + Some(WriteBlock) => self.wblock = None, Some(Hyperlink) => { self.hyperlink_editor_handles.clear(); self.hyperlink_editor_url.clear(); @@ -346,6 +347,14 @@ impl OpenCADStudio { } } } + if self.active_modal == Some(super::ModalKind::WriteBlock) { + if let Some(ref mut state) = self.wblock { + if state.error_message.is_some() { + state.error_message = None; + return Task::none(); + } + } + } return self.update(Message::CloseModal); } if self.active_modal == Some(super::ModalKind::BlockDefinition) { @@ -362,6 +371,13 @@ impl OpenCADStudio { } } } + if self.active_modal == Some(super::ModalKind::WriteBlock) { + if matches!(msg, Message::CommandFinalize) + || matches!(&msg, Message::ShortcutPressed(key) if key.rsplit('+').next() == Some("ENTER") || key.rsplit('+').next() == Some("RETURN")) + { + return self.update(Message::WblockApply); + } + } if is_modal_blocked_key_msg(&msg) { return Task::none(); } @@ -4950,6 +4966,294 @@ impl OpenCADStudio { } self.commit_block_definition(false) } + Message::WblockSourceMode(mode) => { + if let Some(state) = self.wblock.as_mut() { + state.source_mode = mode; + } + Task::none() + } + Message::WblockBlockName(name) => { + if let Some(state) = self.wblock.as_mut() { + state.block_name = name; + } + Task::none() + } + Message::WblockBlockSelect(name) => { + if let Some(state) = self.wblock.as_mut() { + state.block_name = name.clone(); + let trimmed = name.trim(); + if !trimmed.is_empty() { + let current_path = std::path::Path::new(&state.file_path); + let file_stem = current_path.file_stem().and_then(|s| s.to_str()); + let is_default_or_block = file_stem == Some("new_block") + || state.existing_blocks.iter().any(|b| Some(b.as_str()) == file_stem); + if is_default_or_block { + let new_file_name = format!("{}.dwg", trimmed); + if let Some(parent) = current_path.parent() { + if !parent.as_os_str().is_empty() { + state.file_path = parent.join(new_file_name).to_string_lossy().to_string(); + } else { + state.file_path = new_file_name; + } + } else { + state.file_path = new_file_name; + } + } + } + } + Task::none() + } + Message::WblockPickPoint => { + self.active_modal = None; + let cmd = crate::modules::insert::wblock::WblockPickBasePointCommand; + self.command_line.push_info(&cmd.prompt()); + self.tabs[self.active_tab].active_cmd = Some(Box::new(cmd)); + Task::none() + } + Message::WblockBaseX(val) => { + if let Some(state) = self.wblock.as_mut() { + state.base_point_x = val; + } + Task::none() + } + Message::WblockBaseY(val) => { + if let Some(state) = self.wblock.as_mut() { + state.base_point_y = val; + } + Task::none() + } + Message::WblockBaseZ(val) => { + if let Some(state) = self.wblock.as_mut() { + state.base_point_z = val; + } + Task::none() + } + Message::WblockSelectObjects => { + self.active_modal = None; + use crate::modules::draw::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::plain("WBLOCK", "WBLOCK_OBJECTS_GATHERED"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[self.active_tab].active_cmd = Some(Box::new(cmd)); + Task::none() + } + Message::WblockQuickSelect => { + self.active_modal = None; + self.on_qselect_open() + } + Message::WblockObjectMode(mode) => { + if let Some(state) = self.wblock.as_mut() { + state.object_mode = mode; + } + Task::none() + } + Message::WblockFilePath(path) => { + if let Some(state) = self.wblock.as_mut() { + state.file_path = path; + } + Task::none() + } + Message::WblockBrowsePath => { + let default_name = if let Some(state) = self.wblock.as_ref() { + let p = std::path::Path::new(&state.file_path); + p.file_name() + .and_then(|f| f.to_str()) + .unwrap_or("new_block.dwg") + .to_string() + } else { + "new_block.dwg".to_string() + }; + Task::perform( + async move { + let path = crate::sys::file_dialog() + .set_title(crate::t!("Save Block As").as_ref()) + .set_file_name(&default_name) + .add_filter(crate::t!("DWG Files").as_ref(), &["dwg"]) + .add_filter(crate::t!("DXF Files").as_ref(), &["dxf"]) + .save_file() + .await + .map(|h| crate::sys::handle_path(&h)); + path + }, + |path| Message::WblockBrowsePathResult(path), + ) + } + Message::WblockBrowsePathResult(opt_path) => { + if let Some(path) = opt_path { + if let Some(state) = self.wblock.as_mut() { + state.file_path = path.to_string_lossy().to_string(); + } + } + Task::none() + } + Message::WblockUnit(unit) => { + if let Some(state) = self.wblock.as_mut() { + state.unit = unit; + } + Task::none() + } + Message::WblockDismissError => { + if let Some(state) = self.wblock.as_mut() { + state.error_message = None; + } + Task::none() + } + Message::WblockHelp => { + self.command_line.push_info( + crate::t!("WBLOCK writes objects, a block, or the entire drawing to a new drawing file.").as_ref(), + ); + Task::none() + } + Message::WblockApply => { + let Some(state) = self.wblock.as_mut() else { + return Task::none(); + }; + use crate::ui::window::wblock::{WblockObjectMode, WblockSourceMode}; + match state.source_mode { + WblockSourceMode::Block => { + let name = state.block_name.trim(); + if name.is_empty() { + state.error_message = + Some(crate::t!("Please select or enter a block name.").into_owned()); + return Task::none(); + } + let i = self.active_tab; + if self.tabs[i].scene.document.block_records.get(name).is_none() { + state.error_message = Some( + crate::tf!("Block \"{}\" does not exist in drawing.", name) + .into_owned(), + ); + return Task::none(); + } + } + WblockSourceMode::Objects => { + if state.selected_handles.is_empty() { + state.error_message = Some( + crate::t!("No objects selected. You must select objects to define a block.") + .into_owned(), + ); + return Task::none(); + } + } + WblockSourceMode::EntireDrawing => {} + } + + let path_str = state.file_path.trim().to_string(); + if path_str.is_empty() { + state.error_message = + Some(crate::t!("Please specify a file name and path.").into_owned()); + return Task::none(); + } + + let mut path = std::path::PathBuf::from(path_str); + if path.extension().is_none() { + path.set_extension("dwg"); + } + + let i = self.active_tab; + if let Some(current_path) = self.tabs[i].current_path.as_ref() { + if current_path == &path { + state.error_message = Some( + crate::t!("Cannot write to the current drawing file.").into_owned(), + ); + return Task::none(); + } + } + + let state = self.wblock.take().unwrap(); + self.active_modal = None; + let document = self.tabs[i].scene.document_for_save(); + let source_mode = state.source_mode; + let block_name = state.block_name.trim().to_string(); + let handles = state.selected_handles.clone(); + let base_point = state.parse_base_point(); + let unit = state.unit; + let object_mode = state.object_mode; + + if source_mode == WblockSourceMode::Objects { + match object_mode { + WblockObjectMode::Retain => {} + WblockObjectMode::Delete => { + self.push_undo_snapshot(i, "WBLOCK"); + self.tabs[i].scene.erase_entities(&handles); + self.tabs[i].dirty = true; + self.tabs[i].scene.bump_geometry(); + self.refresh_properties(); + } + WblockObjectMode::Convert => { + let block_name_for_conv = path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("WBLOCK") + .to_string(); + self.push_undo_snapshot(i, "WBLOCK"); + let ucs = self.tabs[i].ucs_xform(); + let world_to_block = ucs.to_ucs_transform_at(base_point); + let block_to_world = ucs.to_wcs_transform_at(base_point); + let options = crate::scene::CreateBlockOptions { + name: block_name_for_conv, + handles: handles.clone(), + base_point, + world_to_block, + block_to_world, + mode: crate::ui::window::block_definition::BlockObjectMode::Convert, + annotative: false, + match_orientation: false, + scale_uniformly: false, + allow_exploding: true, + unit, + description: String::new(), + hyperlink_url: String::new(), + hyperlink_desc: String::new(), + redefine: true, + }; + let _ = self.tabs[i].scene.create_block_with_options(options); + self.tabs[i].dirty = true; + self.tabs[i].scene.bump_geometry(); + self.refresh_properties(); + } + } + } + + let worker_path = path.clone(); + let display_name = match source_mode { + WblockSourceMode::Block => block_name.clone(), + WblockSourceMode::EntireDrawing => "*".to_string(), + WblockSourceMode::Objects => "*".to_string(), + }; + let worker_display = display_name.clone(); + + file::background_task( + move || { + let out_doc = match source_mode { + WblockSourceMode::Block => { + let mut d = crate::modules::insert::wblock::extract_block_to_doc( + &document, + &block_name, + ) + .map_err(|e| e.to_string())?; + d.header.insertion_units = unit; + d + } + WblockSourceMode::EntireDrawing => { + let mut d = document.clone(); + d.header.insertion_units = unit; + d + } + WblockSourceMode::Objects => { + crate::modules::insert::wblock::extract_entities_to_doc_with_base( + &document, + &handles, + base_point, + unit, + ) + .map_err(|e| e.to_string())? + } + }; + crate::io::save(&out_doc, &worker_path).map_err(|e| e.to_string()) + }, + move |result| Message::WblockWriteFinished(worker_display, path, result), + ) + } Message::ToleranceDialogField(field) => { if let Some(state) = self.geometric_tolerance.as_mut() { state.apply_field(field); @@ -5904,6 +6208,8 @@ impl OpenCADStudio { self.reset_modal_geometry(); if self.block_definition.is_some() { self.active_modal = Some(super::ModalKind::BlockDefinition); + } else if self.wblock.is_some() { + self.active_modal = Some(super::ModalKind::WriteBlock); } Task::none() } @@ -6110,6 +6416,15 @@ impl OpenCADStudio { .collect(); block_def.error_message = None; self.active_modal = Some(super::ModalKind::BlockDefinition); + } else if let Some(ref mut wblock) = self.wblock { + wblock.selected_handles = self.tabs[i] + .scene + .selected_entities() + .into_iter() + .map(|(h, _)| h) + .collect(); + wblock.error_message = None; + self.active_modal = Some(super::ModalKind::WriteBlock); } Task::none() } diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 777052a73..51c36a53d 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -25,6 +25,7 @@ impl OpenCADStudio { Some(K::DrawingUnits) => crate::t!("Drawing Units").into_owned(), Some(K::BlockDefinition) => crate::t!("Block Definition").into_owned(), Some(K::XrefAttach) => crate::t!("Attach External Reference").into_owned(), + Some(K::WriteBlock) => crate::t!("Write Block").into_owned(), Some(K::GeometricTolerance) => crate::t!("Geometric Tolerance").into_owned(), Some(K::DraftingSettings) => crate::t!("Drafting Settings").into_owned(), Some(K::AutoConstrainSettings) => crate::t!("Constraint Settings").into_owned(), @@ -514,6 +515,12 @@ impl OpenCADStudio { crate::ui::window::xref_attach::view_window(state, flow) }) } + super::super::ModalKind::WriteBlock => { + let state = self.wblock.as_ref()?; + sized_flow(ex, 440, 395, |flow| { + crate::ui::window::wblock::view_window(state, flow) + }) + } super::super::ModalKind::GeometricTolerance => { let state = self.geometric_tolerance.as_ref()?; sized_flow(ex, 670, 530, |flow| { diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 9b7388e1c..bb32171c4 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -3,6 +3,17 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static str)> { match source { + "Entire drawing" => Some(("common", "entire-drawing")), + "Delete from drawing" => Some(("common", "delete-from-drawing")), + "Destination" => Some(("common", "destination")), + "File name and path:" => Some(("common", "file-name-and-path")), + "Insert units:" => Some(("common", "insert-units-label")), + "Please select or enter a block name." => Some(("common", "wblock-err-no-block-name")), + "Block \"{}\" does not exist in drawing." => Some(("common", "wblock-err-block-not-found")), + "Please specify a file name and path." => Some(("common", "wblock-err-no-path")), + "Cannot write to the current drawing file." => Some(("common", "wblock-err-current-drawing")), + "WBLOCK writes objects, a block, or the entire drawing to a new drawing file." => Some(("common", "wblock-help-info")), + "WBLOCK Specify insertion base point:" => Some(("insert", "wblock-specify-insertion-base-point")), "Auto Constrain" => Some(("common", "auto-constrain")), "Delete Constraints" => Some(("common", "delete-constraints")), "Dimensional" => Some(("common", "dimensional")), diff --git a/src/modules/insert/wblock.rs b/src/modules/insert/wblock.rs index 6177a1fcc..abbaef5e9 100644 --- a/src/modules/insert/wblock.rs +++ b/src/modules/insert/wblock.rs @@ -157,3 +157,109 @@ pub fn normalize_to_origin(out: &mut CadDocument) { crate::scene::view::dispatch::apply_transform(entity, &shift); } } + +/// One-shot point picker for the Write Block dialog's "Pick point" button. +pub struct WblockPickBasePointCommand; + +impl crate::command::CadCommand for WblockPickBasePointCommand { + fn name(&self) -> &'static str { + "WBLOCK" + } + + fn prompt(&self) -> String { + crate::t!("WBLOCK Specify insertion base point:").into_owned() + } + + fn on_point(&mut self, pt: glam::DVec3) -> crate::command::CmdResult { + crate::command::CmdResult::Dispatch(format!("WBLOCK_POINT_PICKED {} {} {}", pt.x, pt.y, pt.z)) + } + + fn on_enter(&mut self) -> crate::command::CmdResult { + crate::command::CmdResult::Dispatch("WBLOCK_POINT_CANCELLED".to_string()) + } + + fn on_escape(&mut self) -> crate::command::CmdResult { + crate::command::CmdResult::Dispatch("WBLOCK_POINT_CANCELLED".to_string()) + } +} + +/// Build a standalone `CadDocument` from an explicit list of entity handles, +/// applying `base_point` translation and `unit` insertion units. +pub fn extract_entities_to_doc_with_base( + src: &CadDocument, + handles: &[acadrust::Handle], + base_point: glam::DVec3, + unit: i16, +) -> Result { + let mut out = CadDocument::new(); + out.header.insertion_units = unit; + extract_entities_into(src, handles, &mut out)?; + if base_point != glam::DVec3::ZERO { + let shift = crate::command::EntityTransform::Affine( + acadrust::types::Transform::from_translation(acadrust::types::Vector3::new( + -base_point.x, + -base_point.y, + -base_point.z, + )), + ); + for entity in out.entities_mut() { + crate::scene::view::dispatch::apply_transform(entity, &shift); + } + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + use acadrust::entities::Line; + use acadrust::types::Vector3; + + #[test] + fn test_extract_entities_with_base_point_and_units() { + let mut doc = CadDocument::new(); + let mut line = Line::new(); + line.start = Vector3::new(10.0, 20.0, 0.0); + line.end = Vector3::new(30.0, 40.0, 0.0); + let h = doc.add_entity(EntityType::Line(line)).unwrap(); + + let out = extract_entities_to_doc_with_base( + &doc, + &[h], + glam::DVec3::new(10.0, 20.0, 0.0), + 4, // Millimeters + ) + .expect("extract should succeed"); + + assert_eq!(out.header.insertion_units, 4); + assert_eq!(out.entities().count(), 1); + let e = out.entities().next().unwrap(); + if let EntityType::Line(l) = e { + assert!((l.start.x - 0.0).abs() < 1e-6); + assert!((l.start.y - 0.0).abs() < 1e-6); + assert!((l.end.x - 20.0).abs() < 1e-6); + assert!((l.end.y - 20.0).abs() < 1e-6); + } else { + panic!("Expected Line entity"); + } + } + + #[test] + fn test_extract_block_to_doc() { + let mut doc = CadDocument::new(); + let mut line = Line::new(); + line.start = Vector3::new(1.0, 2.0, 0.0); + line.end = Vector3::new(3.0, 4.0, 0.0); + let h = doc.add_entity(EntityType::Line(line)).unwrap(); + + let mut block_record = acadrust::tables::BlockRecord::new("MY_BLOCK".to_string()); + block_record.entity_handles.push(h); + let _ = doc.block_records.add(block_record); + + let out = extract_block_to_doc(&doc, "MY_BLOCK").expect("block extract should succeed"); + assert_eq!(out.entities().count(), 1); + + assert!(extract_block_to_doc(&doc, "NON_EXISTENT").is_err()); + } +} + diff --git a/src/ui/style/form.rs b/src/ui/style/form.rs index 050fa0eb4..71edfe5ed 100644 --- a/src/ui/style/form.rs +++ b/src/ui/style/form.rs @@ -7,10 +7,44 @@ use crate::app::Message; use crate::ui::style::common::muted_style; -use iced::widget::{button, container, pick_list, row, text, text_input, Space}; +use iced::widget::{button, container, pick_list, radio, row, text, text_input, Space}; use iced::{Background, Border, Element, Length, Theme}; use std::borrow::Cow; +/// Standard dialog footer action button (OK, Cancel, Apply, Close, Help, etc.), +/// matching the Drafting Settings / Options appearance: 12px text, [6, 18] padding, +/// themed primary or secondary style. +pub fn dialog_button<'a>( + label: impl text::IntoFragment<'a>, + on_press: Message, + primary: bool, +) -> button::Button<'a, Message> { + button(text(label).size(12)) + .on_press(on_press) + .padding([6, 18]) + .style(if primary { + button::primary + } else { + button::secondary + }) +} + +/// Unified radio button helper ensuring consistent 16px size for exact pixel-grid concentric alignment. +pub fn form_radio<'a, T>( + label: impl Into, + value: T, + selected: Option, + on_click: impl Fn(T) -> Message + 'a, +) -> radio::Radio<'a, Message> +where + T: Copy + Eq + 'a, +{ + radio(label, value, selected, on_click) + .size(16) + .spacing(6) + .text_size(11) +} + /// Width of the label column in a `label : control` row. pub const LABEL_WIDTH: f32 = 92.0; diff --git a/src/ui/style/point_style.rs b/src/ui/style/point_style.rs index b6feb50dc..a5c817f44 100644 --- a/src/ui/style/point_style.rs +++ b/src/ui/style/point_style.rs @@ -168,7 +168,7 @@ pub fn view_window<'a>( Some(relative), Message::PointStyleSizeRelative, ) - .size(15) + .size(16) .text_size(13), radio( t!("Set Size in Absolute Units"), @@ -176,7 +176,7 @@ pub fn view_window<'a>( Some(relative), Message::PointStyleSizeRelative, ) - .size(15) + .size(16) .text_size(13), ] .spacing(6); diff --git a/src/ui/window/block_definition.rs b/src/ui/window/block_definition.rs index f1c296f11..64b71a9fc 100644 --- a/src/ui/window/block_definition.rs +++ b/src/ui/window/block_definition.rs @@ -5,7 +5,7 @@ use std::fmt; use acadrust::Handle; use iced::widget::{ - button, checkbox, column, combo_box, container, mouse_area, pick_list, radio, row, stack, text, + button, checkbox, column, combo_box, container, mouse_area, pick_list, row, stack, text, text_editor, text_input, Space, }; use iced::{Background, Border, Element, Fill, Length, Theme}; @@ -14,7 +14,7 @@ use crate::app::Message; use crate::modules::draw::units; use crate::t; use crate::ui::style::common::muted_style; -use crate::ui::style::form::{button_style, field_style}; +use crate::ui::style::form::{button_style, dialog_button, field_style, form_radio}; static ICON_PICK_POINT: &[u8] = include_bytes!("../../../assets/icons/blocks/pick_point.svg"); static ICON_SELECT_OBJECTS: &[u8] = @@ -353,33 +353,24 @@ pub fn view_window<'a>( let btns_row = row![sel_obj_btn, qsel_btn].spacing(5).align_y(iced::Center); let radios = column![ - radio( + form_radio( t!("Retain"), BlockObjectMode::Retain, Some(state.object_mode), Message::BlockDefObjectMode, - ) - .size(15) - .spacing(5) - .text_size(11), - radio( + ), + form_radio( t!("Convert to block"), BlockObjectMode::Convert, Some(state.object_mode), Message::BlockDefObjectMode, - ) - .size(15) - .spacing(5) - .text_size(11), - radio( + ), + form_radio( t!("Delete"), BlockObjectMode::Delete, Some(state.object_mode), Message::BlockDefObjectMode, - ) - .size(15) - .spacing(5) - .text_size(11), + ), ] .spacing(2); @@ -580,20 +571,11 @@ pub fn view_window<'a>( // ── Footer Bar ──────────────────────────────────────────────────────────── let actions = row![ Space::new().width(Fill), - button(text(t!("OK")).size(11)) - .on_press(Message::BlockDefApply) - .style(button_style(true)) - .padding([4, 16]), - button(text(t!("Cancel")).size(11)) - .on_press(Message::CloseModal) - .style(button_style(false)) - .padding([4, 12]), - button(text(t!("Help")).size(11)) - .on_press(Message::BlockDefHelp) - .style(button_style(false)) - .padding([4, 12]), + dialog_button(t!("OK"), Message::BlockDefApply, true), + dialog_button(t!("Cancel"), Message::CloseModal, false), + dialog_button(t!("Help"), Message::BlockDefHelp, false), ] - .spacing(6) + .spacing(8) .align_y(iced::Center); // ── Error Banner ────────────────────────────────────────────────────────── @@ -664,15 +646,9 @@ pub fn view_window<'a>( .align_x(iced::alignment::Horizontal::Center), Space::new().height(16), row![ - button(text(t!("Yes")).size(12)) - .on_press(Message::BlockDefConfirmRedefine(true)) - .padding([6, 20]) - .style(button_style(false)), + dialog_button(t!("Yes"), Message::BlockDefConfirmRedefine(true), false), // Default / safe focus on "No" - button(text(t!("No")).size(12)) - .on_press(Message::BlockDefConfirmRedefine(false)) - .padding([6, 22]) - .style(button_style(true)), + dialog_button(t!("No"), Message::BlockDefConfirmRedefine(false), true), ] .spacing(12) .align_y(iced::Center), diff --git a/src/ui/window/mod.rs b/src/ui/window/mod.rs index 1dcd3c445..5adbfda83 100644 --- a/src/ui/window/mod.rs +++ b/src/ui/window/mod.rs @@ -27,3 +27,4 @@ pub mod named_parameters; pub mod xref_attach; pub mod xref_help; pub mod xref_manager; +pub mod wblock; diff --git a/src/ui/window/wblock.rs b/src/ui/window/wblock.rs new file mode 100644 index 000000000..81d7f07ba --- /dev/null +++ b/src/ui/window/wblock.rs @@ -0,0 +1,492 @@ +//! Write Block (WBLOCK) dialog window — write a block, selected entities, or entire drawing to an external DWG/DXF file. + +use std::fmt; + +use acadrust::Handle; +use iced::widget::{ + button, column, combo_box, container, pick_list, row, text, text_input, Space, +}; +use iced::{Background, Border, Element, Fill, Length, Theme}; + +use crate::app::Message; +use crate::modules::draw::units; +use crate::t; +use crate::ui::style::form::{button_style, dialog_button, field_style, form_radio}; + +static ICON_PICK_POINT: &[u8] = include_bytes!("../../../assets/icons/blocks/pick_point.svg"); +static ICON_SELECT_OBJECTS: &[u8] = + include_bytes!("../../../assets/icons/blocks/select_objects.svg"); +static ICON_QUICK_SELECT: &[u8] = include_bytes!("../../../assets/icons/blocks/quick_select.svg"); +static ICON_WARN: &[u8] = include_bytes!("../../../assets/icons/ui/warning_triangle.svg"); + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum WblockSourceMode { + Block, + EntireDrawing, + Objects, +} + +impl Default for WblockSourceMode { + fn default() -> Self { + WblockSourceMode::Objects + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum WblockObjectMode { + Retain, + Convert, + Delete, +} + +impl Default for WblockObjectMode { + fn default() -> Self { + WblockObjectMode::Retain + } +} + +/// Dropdown entry for the Unit picker. +#[derive(Clone, PartialEq, Eq)] +pub struct UnitChoice { + pub code: i16, + pub label: &'static str, +} + +impl fmt::Display for UnitChoice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = crate::i18n::translate(self.label); + f.write_str(name.as_ref()) + } +} + +/// The Write Block dialog's state. +pub struct WblockState { + pub source_mode: WblockSourceMode, + pub block_name: String, + pub existing_blocks: Vec, + pub block_combo: combo_box::State, + + pub base_point_x: String, + pub base_point_y: String, + pub base_point_z: String, + + pub selected_handles: Vec, + pub object_mode: WblockObjectMode, + + pub file_path: String, + pub unit: i16, + + pub error_message: Option, +} + +impl Clone for WblockState { + fn clone(&self) -> Self { + Self { + source_mode: self.source_mode, + block_name: self.block_name.clone(), + existing_blocks: self.existing_blocks.clone(), + block_combo: self.block_combo.clone(), + base_point_x: self.base_point_x.clone(), + base_point_y: self.base_point_y.clone(), + base_point_z: self.base_point_z.clone(), + selected_handles: self.selected_handles.clone(), + object_mode: self.object_mode, + file_path: self.file_path.clone(), + unit: self.unit, + error_message: self.error_message.clone(), + } + } +} + +impl WblockState { + pub fn new( + existing_blocks: Vec, + pre_selected: Vec, + default_unit: i16, + default_path: String, + ) -> Self { + let default_block = existing_blocks.first().cloned().unwrap_or_default(); + Self { + source_mode: WblockSourceMode::Objects, + block_name: default_block, + block_combo: combo_box::State::new(existing_blocks.clone()), + existing_blocks, + base_point_x: "0.00".to_string(), + base_point_y: "0.00".to_string(), + base_point_z: "0.00".to_string(), + selected_handles: pre_selected, + object_mode: WblockObjectMode::Retain, + file_path: default_path, + unit: default_unit, + error_message: None, + } + } + + pub fn parse_base_point(&self) -> glam::DVec3 { + let x = self.base_point_x.trim().parse::().unwrap_or(0.0); + let y = self.base_point_y.trim().parse::().unwrap_or(0.0); + let z = self.base_point_z.trim().parse::().unwrap_or(0.0); + glam::DVec3::new(x, y, z) + } +} + +fn group<'a>( + title: impl Into, + body: impl Into>, + height: impl Into, +) -> Element<'a, Message> { + container( + column![ + text(title.into()) + .size(11) + .style(|theme: &Theme| text::Style { + color: Some(theme.palette().background.strongest.text.scale_alpha(0.85)), + }), + body.into(), + ] + .spacing(4), + ) + .padding([5, 8]) + .width(Fill) + .height(height) + .style(|theme: &Theme| container::Style { + border: Border { + width: 1.0, + radius: 4.0.into(), + color: theme.palette().background.strong.color, + }, + ..Default::default() + }) + .into() +} + +pub fn view_window<'a>( + state: &'a WblockState, + sizing: crate::ui::modal::ModalSizing, +) -> Element<'a, Message> { + let is_objects = state.source_mode == WblockSourceMode::Objects; + let is_block = state.source_mode == WblockSourceMode::Block; + + // ── Source: Block Radio & Dropdown ───────────────────────────────────────── + let block_radio = form_radio( + t!("Block:"), + WblockSourceMode::Block, + Some(state.source_mode), + Message::WblockSourceMode, + ); + + let block_dropdown: Element<'a, Message> = if is_block { + combo_box( + &state.block_combo, + "", + if state.block_name.is_empty() { + None + } else { + Some(&state.block_name) + }, + |chosen: String| Message::WblockBlockSelect(chosen), + ) + .on_input(Message::WblockBlockName) + .size(11) + .padding([2, 5]) + .width(Fill) + .into() + } else { + crate::ui::read_only::field(&state.block_name, 11.0, Fill) + }; + + let block_row = row![block_radio, block_dropdown] + .spacing(8) + .width(Fill) + .align_y(iced::Center); + + let entire_drawing_radio = form_radio( + t!("Entire drawing"), + WblockSourceMode::EntireDrawing, + Some(state.source_mode), + Message::WblockSourceMode, + ); + + let objects_radio = form_radio( + t!("Objects"), + WblockSourceMode::Objects, + Some(state.source_mode), + Message::WblockSourceMode, + ); + + // ── Source: Base Point Sub-group ────────────────────────────────────────── + let pick_pt_icon = crate::ui::icons::semantic(ICON_PICK_POINT, 14.0); + let mut pick_pt_btn = button( + row![pick_pt_icon, text(t!("Pick point")).size(11)] + .spacing(6) + .align_y(iced::Center), + ) + .padding([3, 10]); + + if is_objects { + pick_pt_btn = pick_pt_btn + .on_press(Message::WblockPickPoint) + .style(button_style(false)); + } else { + pick_pt_btn = pick_pt_btn.style(button_style(false)); + } + + let coord_row = |axis: &'static str, val: &'a str, on_input: fn(String) -> Message| { + let input: Element<'a, Message> = if is_objects { + text_input("", val) + .on_input(on_input) + .on_submit(Message::WblockApply) + .style(field_style) + .size(11) + .padding([2, 5]) + .width(Fill) + .into() + } else { + crate::ui::read_only::field(val, 11.0, Fill) + }; + row![ + text(axis) + .size(11) + .style(|theme: &Theme| text::Style { + color: Some(theme.palette().background.strongest.text.scale_alpha(0.85)), + }) + .width(Length::Fixed(16.0)), + input, + ] + .spacing(4) + .align_y(iced::Center) + }; + + let base_point_group = group( + t!("Base point").into_owned(), + column![ + pick_pt_btn, + Space::new().height(3), + coord_row("X:", &state.base_point_x, Message::WblockBaseX), + coord_row("Y:", &state.base_point_y, Message::WblockBaseY), + coord_row("Z:", &state.base_point_z, Message::WblockBaseZ), + ] + .spacing(3), + Length::Fixed(142.0), + ); + + // ── Source: Objects Sub-group ───────────────────────────────────────────── + let sel_obj_icon = crate::ui::icons::semantic(ICON_SELECT_OBJECTS, 14.0); + let mut sel_obj_btn = button( + row![sel_obj_icon, text(t!("Select objects")).size(11)] + .spacing(6) + .align_y(iced::Center), + ) + .padding([3, 8]); + + let qsel_icon = crate::ui::icons::semantic(ICON_QUICK_SELECT, 14.0); + let mut qsel_btn = button(qsel_icon).padding([3, 6]); + + if is_objects { + sel_obj_btn = sel_obj_btn + .on_press(Message::WblockSelectObjects) + .style(button_style(false)); + qsel_btn = qsel_btn + .on_press(Message::WblockQuickSelect) + .style(button_style(false)); + } else { + sel_obj_btn = sel_obj_btn.style(button_style(false)); + qsel_btn = qsel_btn.style(button_style(false)); + } + + let obj_btns_row = row![sel_obj_btn, qsel_btn] + .spacing(5) + .align_y(iced::Center); + + let object_radios = column![ + form_radio( + t!("Retain"), + WblockObjectMode::Retain, + if is_objects { Some(state.object_mode) } else { None }, + Message::WblockObjectMode, + ), + form_radio( + t!("Convert to block"), + WblockObjectMode::Convert, + if is_objects { Some(state.object_mode) } else { None }, + Message::WblockObjectMode, + ), + form_radio( + t!("Delete from drawing"), + WblockObjectMode::Delete, + if is_objects { Some(state.object_mode) } else { None }, + Message::WblockObjectMode, + ), + ] + .spacing(3); + + let count = state.selected_handles.len(); + let status_row: Element<'a, Message> = if !is_objects { + Space::new().height(14).into() + } else if count == 0 { + row![ + crate::ui::icons::semantic(ICON_WARN, 13.0), + text(t!("No objects selected")) + .size(10) + .style(|theme: &Theme| text::Style { + color: Some(theme.palette().warning.base.color), + }), + ] + .spacing(5) + .align_y(iced::Center) + .into() + } else { + row![text(crate::tf!("{count} objects selected")) + .size(10) + .style(|theme: &Theme| text::Style { + color: Some(theme.palette().primary.base.color), + }),] + .spacing(5) + .align_y(iced::Center) + .into() + }; + + let objects_subgroup = group( + t!("Objects").into_owned(), + column![obj_btns_row, Space::new().height(2), object_radios, Space::new().height(2), status_row,] + .spacing(2), + Length::Fixed(142.0), + ); + + let sub_panels = row![base_point_group, objects_subgroup] + .spacing(8) + .width(Fill); + + let source_group = group( + t!("Source").into_owned(), + column![ + block_row, + entire_drawing_radio, + objects_radio, + Space::new().height(4), + sub_panels, + ] + .spacing(4), + Length::Shrink, + ); + + // ── Destination Group ───────────────────────────────────────────────────── + let file_input = text_input("", &state.file_path) + .on_input(Message::WblockFilePath) + .on_submit(Message::WblockApply) + .style(field_style) + .size(11) + .padding([3, 6]) + .width(Fill); + + let browse_btn = button( + container(text("...").size(11)) + .width(Fill) + .align_x(iced::alignment::Horizontal::Center), + ) + .width(Length::Fixed(28.0)) + .on_press(Message::WblockBrowsePath) + .style(button::secondary) + .padding([3, 0]); + + let path_row = row![file_input, browse_btn].spacing(5).align_y(iced::Center); + + let unit_options: Vec = units::all() + .map(|(code, label)| UnitChoice { code, label }) + .collect(); + + let selected_unit = unit_options + .iter() + .find(|choice| choice.code == state.unit) + .cloned(); + + let unit_picker = pick_list(selected_unit, unit_options, |choice| choice.to_string()) + .on_select(|choice| Message::WblockUnit(choice.code)) + .text_size(11) + .padding([2, 5]) + .width(Fill); + + let units_row = row![ + text(t!("Insert units:")) + .size(11) + .style(|theme: &Theme| text::Style { + color: Some(theme.palette().background.strongest.text.scale_alpha(0.85)), + }), + Space::new().width(8), + unit_picker, + Space::new().width(Length::Fixed(33.0)), + ] + .spacing(4) + .align_y(iced::Center); + + let destination_group = group( + t!("Destination").into_owned(), + column![ + text(t!("File name and path:")) + .size(11) + .style(|theme: &Theme| text::Style { + color: Some(theme.palette().background.strongest.text.scale_alpha(0.85)), + }), + path_row, + Space::new().height(4), + units_row, + ] + .spacing(3), + Length::Shrink, + ); + + // ── Footer Bar ──────────────────────────────────────────────────────────── + let actions = row![ + Space::new().width(Fill), + dialog_button(t!("OK"), Message::WblockApply, true), + dialog_button(t!("Cancel"), Message::CloseModal, false), + dialog_button(t!("Help"), Message::WblockHelp, false), + ] + .spacing(8) + .align_y(iced::Center); + + // ── Error Banner ────────────────────────────────────────────────────────── + let mut main_column = column![source_group, destination_group, actions,] + .spacing(8) + .padding([8, 10]) + .width(sizing.width); + + if let Some(err) = &state.error_message { + let err_banner = container( + row![ + crate::ui::icons::semantic(ICON_WARN, 16.0), + text(err.clone()) + .size(11) + .style(|theme: &Theme| text::Style { + color: Some(theme.palette().background.strongest.text), + }), + Space::new().width(Fill), + button(text("×").size(12)) + .on_press(Message::WblockDismissError) + .style(button_style(false)) + .padding([1, 6]), + ] + .spacing(8) + .align_y(iced::Center), + ) + .padding([6, 10]) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color( + theme.palette().danger.base.color.scale_alpha(0.18), + )), + border: Border { + width: 1.0, + radius: 4.0.into(), + color: theme.palette().danger.base.color, + }, + ..Default::default() + }); + + main_column = column![err_banner, main_column] + .spacing(6) + .width(sizing.width) + .padding(10); + } + + main_column.into() +}