diff --git a/Cargo.lock b/Cargo.lock index a7a156ff..e899b9ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,7 +72,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.4.1" -source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=3742f38#3742f382e6f359178ec7bed6b7719c7ac85f3538" +source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=cf46d04#cf46d047211d1adcb37a079812bc38baa45c98f6" dependencies = [ "ahash 0.8.12", "anyhow", @@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=927f888#927f88839bbffe024579e14f2acd4cc87ed4a05a" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=b3f3122#b3f3122176e3f3860dd3bbc23f12c92b56f06095" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 40099097..25afbec4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,8 @@ glam = { version = "0.33", features = ["bytemuck"] } rfd = "0.17" clap = { version = "4", features = ["derive"] } env_logger = "0.11" -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "3742f38", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "927f888", features = ["acis", "offset"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "cf46d04", features = ["serde"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "b3f3122", features = ["acis", "offset"] } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } diff --git a/crates/ocs_plugin_api/Cargo.toml b/crates/ocs_plugin_api/Cargo.toml index dd1ab036..44dafeb8 100644 --- a/crates/ocs_plugin_api/Cargo.toml +++ b/crates/ocs_plugin_api/Cargo.toml @@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] } # Pulled in only by the `host` feature, which adds the `acadrust`-typed # `HostApi` runtime surface. The default crate stays dependency-free so engine # crates and external tooling can depend on the manifest/ribbon contract cheaply. -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "3742f38", optional = true, features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "cf46d04", optional = true, features = ["serde"] } # Runtime IPC and serialization (host feature only). interprocess = { version = "2", optional = true } @@ -37,7 +37,7 @@ serde_json = "1" serde = { version = "1", features = ["derive"] } cargo-lock = "11" # acadrust is scanned at build time to generate the embedded type registry. -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "3742f38", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "cf46d04", features = ["serde"] } [dev-dependencies] serde_json = "1" diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml index 2b46eae8..945b460f 100644 --- a/crates/ocs_web_worker/Cargo.toml +++ b/crates/ocs_web_worker/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib"] [dependencies] -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "3742f38", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "cf46d04", features = ["serde"] } bincode = "1.3" serde = { version = "1", features = ["derive"] } console_error_panic_hook = "0.1" diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index bf4bee89..4298b6aa 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -1416,11 +1416,19 @@ draw = .usage-attdisp-on-off-normal = الاستخدام: ATTDISP ON | OFF | NORMAL .usage-polysolid-width-height-select-a-polyline = الاستخدام: POLYSOLID (حدد خطًا متعددًا أولًا) .usage-pyramid-radius-height-sides-default-4-si = الاستخدام: PYRAMID [sides] (الافتراضي 4 أضلاع) - .wipeout-specify-first-point-or-polyline = WIPEOUT حدد النقطة الأولى أو [خط متعدد]: + .wipeout-specify-first-point-or-polyline = WIPEOUT حدد النقطة الأولى أو [الإطارات/خط متعدد] <خط متعدد>: .wipeout-specify-next-point-or-undo-close-n-poi = WIPEOUT حدد النقطة التالية أو [تراجع/إغلاق] (__ocs_arg_n__ نقاط): .wipeout-polyline-select-a-closed-planar-polyli = WIPEOUT خط متعدد حدد خطًا متعددًا مستويًا مغلقًا: .wipeout-rectangular-specify-first-corner = WIPEOUT مستطيل حدد الزاوية الأولى: .wipeout-rectangular-specify-opposite-corner = WIPEOUT مستطيل حدد الزاوية المقابلة: + .wipeout-frames-enter-frame-mode = WIPEOUT الإطارات أدخل وضع الإطار [إيقاف/تشغيل/عرضبدونطباعة] <__ocs_arg_mode__>: + .wipeout-polyline-erase-source = WIPEOUT خط متعدد حذف الخط المتعدد المصدر؟ [نعم/لا] <لا>: + .wipeout-polyline-invalid = WIPEOUT خط متعدد: حدد خطًا متعددًا ثنائي الأبعاد مستقيمًا ومغلقًا ومستويًا بـ 3 رؤوس غير متقاطعة على الأقل. + .setvar-frame-range = SETVAR: يتطلب __ocs_fmt_0__ القيمة 0 أو 1 أو 2. + .frames = الإطارات + .display-but-not-plot = عرض بدون طباعة + .frames-on-not-plotted = الإطارات ظاهرة، غير مطبوعة + .image-adjust = ضبط الصورة .x-ray = أشعة سينية .xline-specify-a-point = XLINE حدد نقطة: .xline-specify-through-point = XLINE حدد نقطة العبور: diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index edeabd69..c929fb81 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -1403,11 +1403,19 @@ draw = .usage-attdisp-on-off-normal = Употреба: ATTDISP ON | OFF | NORMAL .usage-polysolid-width-height-select-a-polyline = Употреба: POLYSOLID <ширина> <височина> (първо избери полилиния) .usage-pyramid-radius-height-sides-default-4-si = Употреба: PYRAMID <радиус> <височина> [страни] (4 страни по подразбиране) - .wipeout-specify-first-point-or-polyline = WIPEOUT Посочи първа точка или [Полилиния]: + .wipeout-specify-first-point-or-polyline = WIPEOUT Посочи първа точка или [Рамки/Полилиния] <Полилиния>: .wipeout-specify-next-point-or-undo-close-n-poi = WIPEOUT Посочи следваща точка или [Отмени/Затвори] (__ocs_arg_n__ точки): .wipeout-polyline-select-a-closed-planar-polyli = WIPEOUT Polyline Избери затворена равнинна полилиния: .wipeout-rectangular-specify-first-corner = WIPEOUT Rectangular Посочи първи ъгъл: .wipeout-rectangular-specify-opposite-corner = WIPEOUT Rectangular Посочи противоположен ъгъл: + .wipeout-frames-enter-frame-mode = WIPEOUT Рамки Въведи режим на рамката [Изкл./Вкл./ПоказвайБезПечат] <__ocs_arg_mode__>: + .wipeout-polyline-erase-source = WIPEOUT Полилиния Да се изтрие ли изходната полилиния? [Да/Не] <Не>: + .wipeout-polyline-invalid = WIPEOUT Полилиния: избери права, затворена, равнинна 2D полилиния с поне 3 непресичащи се върха. + .setvar-frame-range = SETVAR: __ocs_fmt_0__ изисква 0, 1 или 2. + .frames = Рамки + .display-but-not-plot = Показвай, но не печатай + .frames-on-not-plotted = Рамките са включени, не се печатат + .image-adjust = Настройка на изображението .x-ray = Рентген .xline-specify-a-point = XLINE Посочи точка: .xline-specify-through-point = XLINE Посочи точка на преминаване: diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index d134b68c..c03c335c 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -1403,11 +1403,19 @@ draw = .usage-attdisp-on-off-normal = Použití: ATTDISP ON | OFF | NORMAL .usage-polysolid-width-height-select-a-polyline = Použití: POLYSOLID (nejprve vyberte křivku) .usage-pyramid-radius-height-sides-default-4-si = Použití: PYRAMID [sides] (výchozí 4 strany) - .wipeout-specify-first-point-or-polyline = WIPEOUT Zadejte první bod nebo [Polyline]: + .wipeout-specify-first-point-or-polyline = WIPEOUT Zadejte první bod nebo [Rámečky/Křivka] : .wipeout-specify-next-point-or-undo-close-n-poi = WIPEOUT Zadejte další bod nebo [Undo/Close] (body __ocs_arg_n__): .wipeout-polyline-select-a-closed-planar-polyli = WIPEOUT Polyline Vyberte uzavřenou rovinnou křivku: .wipeout-rectangular-specify-first-corner = WIPEOUT Obdélníkový Zadejte první roh: .wipeout-rectangular-specify-opposite-corner = WIPEOUT Obdélníkový Zadejte protilehlý roh: + .wipeout-frames-enter-frame-mode = WIPEOUT Rámečky Zadejte režim rámečku [Vypnuto/Zapnuto/ZobrazitNetisknout] <__ocs_arg_mode__>: + .wipeout-polyline-erase-source = WIPEOUT Křivka Vymazat zdrojovou křivku? [Ano/Ne] : + .wipeout-polyline-invalid = WIPEOUT Křivka: vyberte přímou, uzavřenou, rovinnou 2D křivku s alespoň 3 neprotínajícími se vrcholy. + .setvar-frame-range = SETVAR: __ocs_fmt_0__ vyžaduje 0, 1 nebo 2. + .frames = Rámečky + .display-but-not-plot = Zobrazit, ale netisknout + .frames-on-not-plotted = Rámečky zapnuté, netisknuté + .image-adjust = Úprava obrazu .x-ray = X-Ray .xline-specify-a-point = XLINE Určete bod: .xline-specify-through-point = XLINE Určete průchozí bod: diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 82e16e56..22b4becf 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -1404,11 +1404,19 @@ draw = .usage-attdisp-on-off-normal = Verwendung: ATTDISP ON | OFF | NORMAL .usage-polysolid-width-height-select-a-polyline = Verwendung: POLYSOLID (select a polyline first) .usage-pyramid-radius-height-sides-default-4-si = Verwendung: PYRAMID [sides] (default 4 sides) - .wipeout-specify-first-point-or-polyline = WIPEOUT Geben Sie den ersten Punkt oder [Polylinie] an: + .wipeout-specify-first-point-or-polyline = WIPEOUT Geben Sie den ersten Punkt oder [Rahmen/Polylinie] an : .wipeout-specify-next-point-or-undo-close-n-poi = WIPEOUT Nächsten Punkt angeben oder [Zurück/Schließen] (__ocs_arg_n__ Punkte): .wipeout-polyline-select-a-closed-planar-polyli = WIPEOUT Polylinie Wählen Sie eine geschlossene planare Polylinie: .wipeout-rectangular-specify-first-corner = WIPEOUT Rechteckig Geben Sie die erste Ecke an: .wipeout-rectangular-specify-opposite-corner = WIPEOUT Rechteckig Angeben der gegenüberliegenden Ecke: + .wipeout-frames-enter-frame-mode = WIPEOUT Rahmen Rahmenmodus eingeben [Aus/Ein/AnzeigenAberNichtPlotten] <__ocs_arg_mode__>: + .wipeout-polyline-erase-source = WIPEOUT Polylinie Quellpolylinie löschen? [Ja/Nein] : + .wipeout-polyline-invalid = WIPEOUT Polylinie: Wählen Sie eine gerade, geschlossene, planare 2D-Polylinie mit mindestens 3 sich nicht schneidenden Scheitelpunkten. + .setvar-frame-range = SETVAR: __ocs_fmt_0__ erfordert 0, 1 oder 2. + .frames = Rahmen + .display-but-not-plot = Anzeigen, aber nicht plotten + .frames-on-not-plotted = Rahmen ein, nicht geplottet + .image-adjust = Bildanpassung .x-ray = Röntgenstrahlen .xline-specify-a-point = XLINE Geben Sie einen Punkt an: .xline-specify-through-point = XLINE Geben Sie durch Punkt an: diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index f4bf7ed2..00c715c4 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -1405,11 +1405,19 @@ draw = .usage-attdisp-on-off-normal = Usage: ATTDISP ON | OFF | NORMAL .usage-polysolid-width-height-select-a-polyline = Usage: POLYSOLID (select a polyline first) .usage-pyramid-radius-height-sides-default-4-si = Usage: PYRAMID [sides] (default 4 sides) - .wipeout-specify-first-point-or-polyline = WIPEOUT Specify first point or [Polyline]: + .wipeout-specify-first-point-or-polyline = WIPEOUT Specify first point or [Frames/Polyline] : .wipeout-specify-next-point-or-undo-close-n-poi = WIPEOUT Specify next point or [Undo/Close] (__ocs_arg_n__ points): .wipeout-polyline-select-a-closed-planar-polyli = WIPEOUT Polyline Select a closed planar polyline: .wipeout-rectangular-specify-first-corner = WIPEOUT Rectangular Specify first corner: .wipeout-rectangular-specify-opposite-corner = WIPEOUT Rectangular Specify opposite corner: + .wipeout-frames-enter-frame-mode = WIPEOUT Frames Enter frame mode [Off/On/DisplayButNotPlot] <__ocs_arg_mode__>: + .wipeout-polyline-erase-source = WIPEOUT Polyline Erase source polyline? [Yes/No] : + .wipeout-polyline-invalid = WIPEOUT Polyline: select a straight, closed, planar 2D polyline with at least 3 non-intersecting vertices. + .setvar-frame-range = SETVAR: __ocs_fmt_0__ requires 0, 1, or 2. + .frames = Frames + .display-but-not-plot = Display but not plot + .frames-on-not-plotted = Frames On, Not Plotted + .image-adjust = Image Adjust .x-ray = X-Ray .xline-specify-a-point = XLINE Specify a point: .xline-specify-through-point = XLINE Specify through point: diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 52111a54..10e4c952 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -1404,11 +1404,19 @@ draw = .usage-attdisp-on-off-normal = Uso: ATTDISP ON | OFF | NORMAL .usage-polysolid-width-height-select-a-polyline = Uso: POLYSOLID (seleccione primero una polilínea) .usage-pyramid-radius-height-sides-default-4-si = Uso: PYRAMID [lados] (4 lados de forma predeterminada) - .wipeout-specify-first-point-or-polyline = WIPEOUT Precise el primer punto o [Polilínea]: + .wipeout-specify-first-point-or-polyline = WIPEOUT Precise el primer punto o [Marcos/Polilínea] : .wipeout-specify-next-point-or-undo-close-n-poi = WIPEOUT Precise el punto siguiente o [Deshacer/Cerrar] (__ocs_arg_n__ puntos): .wipeout-polyline-select-a-closed-planar-polyli = WIPEOUT Polilínea Seleccione una polilínea plana cerrada: .wipeout-rectangular-specify-first-corner = WIPEOUT Rectangular Precise la primera esquina: .wipeout-rectangular-specify-opposite-corner = WIPEOUT Rectangular Precise la esquina opuesta: + .wipeout-frames-enter-frame-mode = WIPEOUT Marcos Indique el modo de marco [Desactivado/Activado/MostrarSinTrazar] <__ocs_arg_mode__>: + .wipeout-polyline-erase-source = WIPEOUT Polilínea ¿Borrar la polilínea de origen? [Sí/No] : + .wipeout-polyline-invalid = WIPEOUT Polilínea: seleccione una polilínea 2D recta, cerrada y plana con al menos 3 vértices que no se intersequen. + .setvar-frame-range = SETVAR: __ocs_fmt_0__ requiere 0, 1 o 2. + .frames = Marcos + .display-but-not-plot = Mostrar pero no trazar + .frames-on-not-plotted = Marcos activados, no trazados + .image-adjust = Ajuste de imagen .x-ray = Rayos X .xline-specify-a-point = XLINE Precise un punto: .xline-specify-through-point = XLINE Precise el punto de paso: diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 41803063..c6e99861 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -1403,11 +1403,19 @@ draw = .usage-attdisp-on-off-normal = Käyttö: ATTDISP ON | OFF | NORMAL .usage-polysolid-width-height-select-a-polyline = Käyttö: POLYSOLID (valitse ensin polyline) .usage-pyramid-radius-height-sides-default-4-si = Käyttö: PYRAMID [sides] (oletus 4 sivua) - .wipeout-specify-first-point-or-polyline = WIPEOUT Määritä ensimmäinen piste tai [Polyline]: + .wipeout-specify-first-point-or-polyline = WIPEOUT Määritä ensimmäinen piste tai [Kehykset/Moniviiva] : .wipeout-specify-next-point-or-undo-close-n-poi = WIPEOUT Määritä seuraava piste tai [Kumoa/Sulje] (__ocs_arg_n__-pisteet): .wipeout-polyline-select-a-closed-planar-polyli = WIPEOUT Polyline Valitse suljettu tasoviiva: .wipeout-rectangular-specify-first-corner = WIPEOUT Suorakulmainen Määritä ensimmäinen kulma: .wipeout-rectangular-specify-opposite-corner = WIPEOUT Suorakulmainen Määritä vastakkainen kulma: + .wipeout-frames-enter-frame-mode = WIPEOUT Kehykset Anna kehystila [Pois/Päällä/NäytäEiTulostusta] <__ocs_arg_mode__>: + .wipeout-polyline-erase-source = WIPEOUT Moniviiva Poistetaanko lähdemoniviiva? [Kyllä/Ei] : + .wipeout-polyline-invalid = WIPEOUT Moniviiva: valitse suora, suljettu ja tasomainen 2D-moniviiva, jossa on vähintään 3 toisiaan leikkaamatonta kärkeä. + .setvar-frame-range = SETVAR: __ocs_fmt_0__ vaatii arvon 0, 1 tai 2. + .frames = Kehykset + .display-but-not-plot = Näytä, mutta älä tulosta + .frames-on-not-plotted = Kehykset päällä, ei tulosteta + .image-adjust = Kuvan säätö .x-ray = X-Ray .xline-specify-a-point = XLINE Määritä piste: .xline-specify-through-point = XLINE Määritä pisteen kautta: diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index b3e4f5eb..5021ef57 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -1404,11 +1404,19 @@ draw = .usage-attdisp-on-off-normal = Utilisation : ATTDISP ON | OFF | NORMAL .usage-polysolid-width-height-select-a-polyline = Utilisation : POLYSOLID (select a polyline first) .usage-pyramid-radius-height-sides-default-4-si = Utilisation : PYRAMID [sides] (default 4 sides) - .wipeout-specify-first-point-or-polyline = WIPEOUT Spécifier le premier point ou [Polyligne]: + .wipeout-specify-first-point-or-polyline = WIPEOUT Spécifier le premier point ou [Cadres/Polyligne] : .wipeout-specify-next-point-or-undo-close-n-poi = WIPEOUT Spécifiez le point suivant ou [Désactiver/Fermer] (__ocs_arg_n__ points): .wipeout-polyline-select-a-closed-planar-polyli = WIPEOUT Polyligne Sélectionnez une polyligne plane fermée : .wipeout-rectangular-specify-first-corner = WIPEOUT Rectangulaire Précisez le premier coin : .wipeout-rectangular-specify-opposite-corner = WIPEOUT Rectangulaire Spécifiez le coin opposé: + .wipeout-frames-enter-frame-mode = WIPEOUT Cadres Entrez le mode de cadre [Désactivé/Activé/AfficherSansTracer] <__ocs_arg_mode__> : + .wipeout-polyline-erase-source = WIPEOUT Polyligne Effacer la polyligne source ? [Oui/Non] : + .wipeout-polyline-invalid = WIPEOUT Polyligne : sélectionnez une polyligne 2D droite, fermée et plane comportant au moins 3 sommets sans intersection. + .setvar-frame-range = SETVAR : __ocs_fmt_0__ exige 0, 1 ou 2. + .frames = Cadres + .display-but-not-plot = Afficher sans tracer + .frames-on-not-plotted = Cadres activés, non tracés + .image-adjust = Réglage de l’image .x-ray = Rayon X .xline-specify-a-point = XLINE Spécifiez un point: .xline-specify-through-point = XLINE Spécifiez par le point: diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 3f710439..b0ede3fe 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -1392,11 +1392,19 @@ draw = .usage-attdisp-on-off-normal = उपयोग: ATTDISP ON | OFF | NORMAL .usage-polysolid-width-height-select-a-polyline = उपयोग: POLYSOLID (select a polyline first) .usage-pyramid-radius-height-sides-default-4-si = उपयोग: PYRAMID [sides] (default 4 sides) - .wipeout-specify-first-point-or-polyline = WIPEOUT प्रथम बिंदु या [पॉलिलाइन] निर्दिष्ट करें: + .wipeout-specify-first-point-or-polyline = WIPEOUT पहला बिंदु या [फ्रेम/पॉलिलाइन] <पॉलिलाइन> निर्दिष्ट करें: .wipeout-specify-next-point-or-undo-close-n-poi = WIPEOUT अगला बिंदु निर्दिष्ट करें या [पूर्ववत करें / बंद करें] (__ocs_arg_n__ बिंदु): .wipeout-polyline-select-a-closed-planar-polyli = WIPEOUT पॉलीलाइन एक बंद प्लानर पॉलीलाइन चुनें: .wipeout-rectangular-specify-first-corner = WIPEOUT आयताकार पहले कोने निर्दिष्ट करें: .wipeout-rectangular-specify-opposite-corner = WIPEOUT आयताकार विपरीत कोने निर्दिष्ट करें: + .wipeout-frames-enter-frame-mode = WIPEOUT फ्रेम फ्रेम मोड दर्ज करें [बंद/चालू/दिखाएंपरप्लॉटनकरें] <__ocs_arg_mode__>: + .wipeout-polyline-erase-source = WIPEOUT पॉलिलाइन स्रोत पॉलिलाइन मिटाएं? [हां/नहीं] <नहीं>: + .wipeout-polyline-invalid = WIPEOUT पॉलिलाइन: कम से कम 3 न कटने वाले शीर्षों वाली सीधी, बंद, समतल 2D पॉलिलाइन चुनें। + .setvar-frame-range = SETVAR: __ocs_fmt_0__ के लिए 0, 1 या 2 आवश्यक है। + .frames = फ्रेम + .display-but-not-plot = दिखाएं, पर प्लॉट न करें + .frames-on-not-plotted = फ्रेम चालू, प्लॉट नहीं होंगे + .image-adjust = छवि समायोजन .x-ray = एक्स-रे .xline-specify-a-point = XLINE एक बिंदु निर्दिष्ट करें: .xline-specify-through-point = XLINE बिंदु के माध्यम से निर्दिष्ट करें: diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 2b7b9d0b..d2f8938c 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -1403,11 +1403,19 @@ draw = .usage-attdisp-on-off-normal = Használat: ATTDISP ON | OFF | NORMAL .usage-polysolid-width-height-select-a-polyline = Használat: POLYSOLID (először válasszon ki egy vonalláncot) .usage-pyramid-radius-height-sides-default-4-si = Használat: PYRAMID [sides] (alapértelmezett 4 oldal) - .wipeout-specify-first-point-or-polyline = WIPEOUT Adja meg az első pontot vagy a [Polyline]-t: + .wipeout-specify-first-point-or-polyline = WIPEOUT Adja meg az első pontot vagy [Keretek/Vonallánc] : .wipeout-specify-next-point-or-undo-close-n-poi = WIPEOUT Következő pont megadása vagy [Visszavonás/Bezárás] (__ocs_arg_n__ pont): .wipeout-polyline-select-a-closed-planar-polyli = WIPEOUT Vonallánc Válasszon ki egy zárt sík vonalláncot: .wipeout-rectangular-specify-first-corner = WIPEOUT téglalap Adja meg az első sarkot: .wipeout-rectangular-specify-opposite-corner = WIPEOUT Rectangular Adja meg a szemközti sarkot: + .wipeout-frames-enter-frame-mode = WIPEOUT Keretek Adja meg a keretmódot [Ki/Be/MegjelenítNemNyomtat] <__ocs_arg_mode__>: + .wipeout-polyline-erase-source = WIPEOUT Vonallánc Törli a forrás vonalláncot? [Igen/Nem] : + .wipeout-polyline-invalid = WIPEOUT Vonallánc: válasszon egyenes, zárt, síkbeli 2D vonalláncot legalább 3, egymást nem metsző csúcsponttal. + .setvar-frame-range = SETVAR: a __ocs_fmt_0__ értéke 0, 1 vagy 2 lehet. + .frames = Keretek + .display-but-not-plot = Megjelenítés nyomtatás nélkül + .frames-on-not-plotted = Keretek bekapcsolva, nem nyomtatva + .image-adjust = Képbeállítás .x-ray = X-Ray .xline-specify-a-point = XLINE Adjon meg egy pontot: .xline-specify-through-point = XLINE Pont megadása: diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 03b94d3d..0b22f4f3 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -1403,11 +1403,19 @@ draw = .usage-attdisp-on-off-normal = Utilizzo: ATTDISP ON | OFF | NORMAL .usage-polysolid-width-height-select-a-polyline = Utilizzo: POLYSOLID (selezionare prima una polilinea) .usage-pyramid-radius-height-sides-default-4-si = Utilizzo: PYRAMID [sides] (predefinito 4 lati) - .wipeout-specify-first-point-or-polyline = WIPEOUT Specificare il primo punto o [Polyline]: + .wipeout-specify-first-point-or-polyline = WIPEOUT Specificare il primo punto o [Cornici/Polilinea] : .wipeout-specify-next-point-or-undo-close-n-poi = WIPEOUT Specificare il punto successivo o [Annulla/Chiudi] (punti __ocs_arg_n__): .wipeout-polyline-select-a-closed-planar-polyli = WIPEOUT Polilinea Selezionare una polilinea planare chiusa: .wipeout-rectangular-specify-first-corner = WIPEOUT Rettangolare Specificare il primo angolo: .wipeout-rectangular-specify-opposite-corner = WIPEOUT Rettangolare Specificare l'angolo opposto: + .wipeout-frames-enter-frame-mode = WIPEOUT Cornici Immettere la modalità cornice [Disattivata/Attivata/VisualizzaSenzaStampare] <__ocs_arg_mode__>: + .wipeout-polyline-erase-source = WIPEOUT Polilinea Cancellare la polilinea di origine? [Sì/No] : + .wipeout-polyline-invalid = WIPEOUT Polilinea: selezionare una polilinea 2D diritta, chiusa e planare con almeno 3 vertici non intersecanti. + .setvar-frame-range = SETVAR: __ocs_fmt_0__ richiede 0, 1 o 2. + .frames = Cornici + .display-but-not-plot = Visualizza ma non stampa + .frames-on-not-plotted = Cornici attive, non stampate + .image-adjust = Regolazione immagine .x-ray = X-Ray .xline-specify-a-point = XLINE Specificare un punto: .xline-specify-through-point = XLINE Specificare il punto di passaggio: diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index f19e343f..3c4b0050 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -1394,11 +1394,19 @@ draw = .usage-attdisp-on-off-normal = 使用法: ATTDISP ON | OFF | NORMAL .usage-polysolid-width-height-select-a-polyline = 使用法: POLYSOLID (先にポリラインを選択) .usage-pyramid-radius-height-sides-default-4-si = 使用法: PYRAMID [sides] (既定は 4 側面) - .wipeout-specify-first-point-or-polyline = WIPEOUT 最初の点を指定または [Polyline]: + .wipeout-specify-first-point-or-polyline = WIPEOUT 最初の点を指定または [フレーム/ポリライン] <ポリライン>: .wipeout-specify-next-point-or-undo-close-n-poi = WIPEOUT 次の点を指定または [Undo/Close](__ocs_arg_n__ 点): .wipeout-polyline-select-a-closed-planar-polyli = WIPEOUT Polyline 閉じた平面ポリラインを選択: .wipeout-rectangular-specify-first-corner = WIPEOUT Rectangular 最初のコーナーを指定: .wipeout-rectangular-specify-opposite-corner = WIPEOUT Rectangular 反対側のコーナーを指定: + .wipeout-frames-enter-frame-mode = WIPEOUT フレーム フレームモードを入力 [オフ/オン/表示して印刷しない] <__ocs_arg_mode__>: + .wipeout-polyline-erase-source = WIPEOUT ポリライン 元のポリラインを削除しますか? [はい/いいえ] <いいえ>: + .wipeout-polyline-invalid = WIPEOUT ポリライン: 交差しない3つ以上の頂点を持つ、直線の閉じた平面2Dポリラインを選択してください。 + .setvar-frame-range = SETVAR: __ocs_fmt_0__ には 0、1、2 のいずれかが必要です。 + .frames = フレーム + .display-but-not-plot = 表示するが印刷しない + .frames-on-not-plotted = フレームオン、印刷なし + .image-adjust = 画像調整 .x-ray = X 線 .xline-specify-a-point = XLINE 点を指定: .xline-specify-through-point = XLINE 通過点を指定: diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 7d7aeb2f..ef320dd7 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -1403,11 +1403,19 @@ draw = .usage-attdisp-on-off-normal = 사용법: ATTDISP ON | OFF | NORMAL .usage-polysolid-width-height-select-a-polyline = 사용법: POLYSOLID (폴리라인을 먼저 선택하세요) .usage-pyramid-radius-height-sides-default-4-si = 사용법: PYRAMID [sides] (기본 4면) - .wipeout-specify-first-point-or-polyline = WIPEOUT 첫 번째 점 또는 [Polyline] 지정: + .wipeout-specify-first-point-or-polyline = WIPEOUT 첫 번째 점 또는 [프레임/폴리선] <폴리선> 지정: .wipeout-specify-next-point-or-undo-close-n-poi = WIPEOUT 다음 점 지정 또는 [실행 취소/닫기](__ocs_arg_n__ 점): .wipeout-polyline-select-a-closed-planar-polyli = WIPEOUT 폴리선 닫힌 평면형 폴리선을 선택합니다. .wipeout-rectangular-specify-first-corner = WIPEOUT 직사각형 첫 번째 구석 지정: .wipeout-rectangular-specify-opposite-corner = WIPEOUT 직사각형 반대쪽 모서리 지정: + .wipeout-frames-enter-frame-mode = WIPEOUT 프레임 프레임 모드 입력 [끄기/켜기/표시하고플롯안함] <__ocs_arg_mode__>: + .wipeout-polyline-erase-source = WIPEOUT 폴리선 원본 폴리선을 지우시겠습니까? [예/아니오] <아니오>: + .wipeout-polyline-invalid = WIPEOUT 폴리선: 교차하지 않는 꼭짓점이 3개 이상인 직선, 닫힌, 평면 2D 폴리선을 선택하세요. + .setvar-frame-range = SETVAR: __ocs_fmt_0__에는 0, 1 또는 2가 필요합니다. + .frames = 프레임 + .display-but-not-plot = 표시하지만 플롯하지 않음 + .frames-on-not-plotted = 프레임 켜기, 플롯 안 함 + .image-adjust = 이미지 조정 .x-ray = X-레이 .xline-specify-a-point = XLINE 점 지정: .xline-specify-through-point = XLINE 통과점 지정: diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 3606e275..0d667a5f 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -1404,11 +1404,19 @@ draw = .usage-attdisp-on-off-normal = Gebruik: ATTDISP ON | OFF | NORMAL .usage-polysolid-width-height-select-a-polyline = Gebruik: POLYSOLID (select a polyline first) .usage-pyramid-radius-height-sides-default-4-si = Gebruik: PYRAMID [sides] (default 4 sides) - .wipeout-specify-first-point-or-polyline = WIPEOUT Geef het eerste punt of [Polylijn]: + .wipeout-specify-first-point-or-polyline = WIPEOUT Geef het eerste punt of [Kaders/Polylijn] : .wipeout-specify-next-point-or-undo-close-n-poi = WIPEOUT Geef volgend punt op of [Ongedaan maken/Sluiten] (__ocs_arg_n__ punten): .wipeout-polyline-select-a-closed-planar-polyli = WIPEOUT Polylijn Selecteer een gesloten planaire polylijn: .wipeout-rectangular-specify-first-corner = WIPEOUT Rechthoekige eerste hoek opgeven: .wipeout-rectangular-specify-opposite-corner = WIPEOUT Rechthoekig Geef tegenoverhoek aan: + .wipeout-frames-enter-frame-mode = WIPEOUT Kaders Voer kadermodus in [Uit/Aan/WeergevenNietPlotten] <__ocs_arg_mode__>: + .wipeout-polyline-erase-source = WIPEOUT Polylijn Bronpolylijn wissen? [Ja/Nee] : + .wipeout-polyline-invalid = WIPEOUT Polylijn: selecteer een rechte, gesloten, vlakke 2D-polylijn met ten minste 3 niet-kruisende hoekpunten. + .setvar-frame-range = SETVAR: __ocs_fmt_0__ vereist 0, 1 of 2. + .frames = Kaders + .display-but-not-plot = Weergeven maar niet plotten + .frames-on-not-plotted = Kaders aan, niet geplot + .image-adjust = Beeldaanpassing .x-ray = X-Ray .xline-specify-a-point = XLINE Geef een punt op: .xline-specify-through-point = XLINE Geef door punt: diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 740041e0..e158bcc9 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -1403,11 +1403,19 @@ draw = .usage-attdisp-on-off-normal = Zastosowanie: ATTDISP ON | OFF | NORMAL .usage-polysolid-width-height-select-a-polyline = Sposób użycia: POLYSOLID (najpierw wybierz polilinię) .usage-pyramid-radius-height-sides-default-4-si = Zastosowanie: PYRAMID [sides] (domyślnie 4 strony) - .wipeout-specify-first-point-or-polyline = WIPEOUT Podaj pierwszy punkt lub [Polyline]: + .wipeout-specify-first-point-or-polyline = WIPEOUT Podaj pierwszy punkt lub [Ramki/Polilinia] : .wipeout-specify-next-point-or-undo-close-n-poi = WIPEOUT Określ następny punkt lub [Cofnij/Zamknij] (punkty __ocs_arg_n__): .wipeout-polyline-select-a-closed-planar-polyli = WIPEOUT Polilinia Wybierz zamkniętą planarną polilinię: .wipeout-rectangular-specify-first-corner = WIPEOUT Prostokątny Określ pierwszy narożnik: .wipeout-rectangular-specify-opposite-corner = WIPEOUT Prostokątny Określ przeciwny narożnik: + .wipeout-frames-enter-frame-mode = WIPEOUT Ramki Wprowadź tryb ramki [Wyłącz/Włącz/WyświetlBezDrukowania] <__ocs_arg_mode__>: + .wipeout-polyline-erase-source = WIPEOUT Polilinia Usunąć polilinię źródłową? [Tak/Nie] : + .wipeout-polyline-invalid = WIPEOUT Polilinia: wybierz prostą, zamkniętą, płaską polilinię 2D z co najmniej 3 nieprzecinającymi się wierzchołkami. + .setvar-frame-range = SETVAR: __ocs_fmt_0__ wymaga wartości 0, 1 lub 2. + .frames = Ramki + .display-but-not-plot = Wyświetlaj, ale nie drukuj + .frames-on-not-plotted = Ramki włączone, niedrukowane + .image-adjust = Dopasowanie obrazu .x-ray = X-Ray .xline-specify-a-point = XLINE Określ punkt: .xline-specify-through-point = XLINE Określ punkt przelotowy: diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 662cba1f..76593d51 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -1404,11 +1404,19 @@ draw = .usage-attdisp-on-off-normal = Uso: ATTDISP ON | OFF | NORMAL .usage-polysolid-width-height-select-a-polyline = Uso: POLYSOLID (selecione primeiro uma polilinha) .usage-pyramid-radius-height-sides-default-4-si = Uso: PYRAMID [lados] (padrão: 4 lados) - .wipeout-specify-first-point-or-polyline = WIPEOUT Especifique o primeiro ponto ou [Polilinha]: + .wipeout-specify-first-point-or-polyline = WIPEOUT Especifique o primeiro ponto ou [Molduras/Polilinha] : .wipeout-specify-next-point-or-undo-close-n-poi = WIPEOUT Especifique o próximo ponto ou [Desfazer/Fechar] (__ocs_arg_n__ pontos): .wipeout-polyline-select-a-closed-planar-polyli = WIPEOUT Polilinha Selecione uma polilinha planar fechada: .wipeout-rectangular-specify-first-corner = WIPEOUT Retangular Especifique o primeiro canto: .wipeout-rectangular-specify-opposite-corner = WIPEOUT Retangular Especifique o canto oposto: + .wipeout-frames-enter-frame-mode = WIPEOUT Molduras Informe o modo da moldura [Desativado/Ativado/ExibirSemPlotar] <__ocs_arg_mode__>: + .wipeout-polyline-erase-source = WIPEOUT Polilinha Apagar a polilinha de origem? [Sim/Não] : + .wipeout-polyline-invalid = WIPEOUT Polilinha: selecione uma polilinha 2D reta, fechada e planar com pelo menos 3 vértices sem interseção. + .setvar-frame-range = SETVAR: __ocs_fmt_0__ requer 0, 1 ou 2. + .frames = Molduras + .display-but-not-plot = Exibir, mas não plotar + .frames-on-not-plotted = Molduras ativadas, não plotadas + .image-adjust = Ajuste de imagem .x-ray = Raio X .xline-specify-a-point = XLINE Especifique um ponto: .xline-specify-through-point = XLINE Especifique o ponto de passagem: diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 132b5fc4..d6a8c048 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -1407,11 +1407,19 @@ draw = .usage-attdisp-on-off-normal = Использование: ATTDISP ON | OFF | NORMAL .usage-polysolid-width-height-select-a-polyline = Использование: POLYSOLID (select a polyline first) .usage-pyramid-radius-height-sides-default-4-si = Использование: PYRAMID [sides] (default 4 sides) - .wipeout-specify-first-point-or-polyline = WIPEOUT Укажите первую точку или [Полилайн]: + .wipeout-specify-first-point-or-polyline = WIPEOUT Укажите первую точку или [Рамки/Полилиния] <Полилиния>: .wipeout-specify-next-point-or-undo-close-n-poi = WIPEOUT Укажите следующую точку или [Отменить / Замкнуть] (точек: __ocs_arg_n__): .wipeout-polyline-select-a-closed-planar-polyli = WIPEOUT Выберите замкнутый плоский полилин: .wipeout-rectangular-specify-first-corner = WIPEOUT Прямоугольный Укажите первый угол: .wipeout-rectangular-specify-opposite-corner = WIPEOUT Прямоугольный Укажите противоположный угол: + .wipeout-frames-enter-frame-mode = WIPEOUT Рамки Введите режим рамки [Выкл/Вкл/ПоказатьНеПечатать] <__ocs_arg_mode__>: + .wipeout-polyline-erase-source = WIPEOUT Полилиния Удалить исходную полилинию? [Да/Нет] <Нет>: + .wipeout-polyline-invalid = WIPEOUT Полиния: выберите прямую, замкнутую, плоскую 2D-полилинию с не менее чем 3 непересекающимися вершинами. + .setvar-frame-range = SETVAR: __ocs_fmt_0__ допускает только 0, 1 или 2. + .frames = Рамки + .display-but-not-plot = Показывать, но не печатать + .frames-on-not-plotted = Рамки включены, не печатаются + .image-adjust = Настройка изображения .x-ray = Рентгеновский .xline-specify-a-point = XLINE Укажите точку: .xline-specify-through-point = XLINE Укажите через точку: diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 5d765ea8..5188fe73 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -1387,11 +1387,19 @@ draw = .usage-attdisp-on-off-normal = Kullanım: ATTDISP ON | OFF | NORMAL .usage-polysolid-width-height-select-a-polyline = Kullanım: POLYSOLID (önce bir çoklu çizgi seçin) .usage-pyramid-radius-height-sides-default-4-si = Kullanım: PYRAMID [sides] (varsayılan 4 kenar) - .wipeout-specify-first-point-or-polyline = WIPEOUT İlk noktayı belirtin veya [Çoklu Çizgi]: + .wipeout-specify-first-point-or-polyline = WIPEOUT İlk noktayı belirtin veya [Çerçeveler/Çoklu Çizgi] <Çoklu Çizgi>: .wipeout-specify-next-point-or-undo-close-n-poi = WIPEOUT Sonraki noktayı belirtin veya [Geri Al/Kapat] (__ocs_arg_n__ nokta): .wipeout-polyline-select-a-closed-planar-polyli = WIPEOUT Çoklu Çizgi Kapalı düzlemsel bir çoklu çizgi seçin: .wipeout-rectangular-specify-first-corner = WIPEOUT Dikdörtgen İlk köşeyi belirtin: .wipeout-rectangular-specify-opposite-corner = WIPEOUT Dikdörtgen Karşı köşeyi belirtin: + .wipeout-frames-enter-frame-mode = WIPEOUT Çerçeveler Çerçeve modunu girin [Kapalı/Açık/GösterAmaÇizdirme] <__ocs_arg_mode__>: + .wipeout-polyline-erase-source = WIPEOUT Çoklu Çizgi Kaynak çoklu çizgi silinsin mi? [Evet/Hayır] : + .wipeout-polyline-invalid = WIPEOUT Çoklu Çizgi: en az 3 kesişmeyen köşesi olan düz, kapalı, düzlemsel bir 2B çoklu çizgi seçin. + .setvar-frame-range = SETVAR: __ocs_fmt_0__ için 0, 1 veya 2 gerekir. + .frames = Çerçeveler + .display-but-not-plot = Göster ama çizdirme + .frames-on-not-plotted = Çerçeveler Açık, Çizdirilmez + .image-adjust = Görüntü Ayarı .x-ray = X Işını .xline-specify-a-point = XLINE Bir nokta belirtin: .xline-specify-through-point = XLINE Geçiş noktasını belirtin: diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 2c801034..6f195b6f 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -1391,11 +1391,19 @@ draw = .usage-attdisp-on-off-normal = 用法: ATTDISP ON | OFF | NORMAL .usage-polysolid-width-height-select-a-polyline = 用法: POLYSOLID (select a polyline first) .usage-pyramid-radius-height-sides-default-4-si = 用法: PYRAMID [sides] (default 4 sides) - .wipeout-specify-first-point-or-polyline = WIPEOUT 指定第一点或 [多段线]: + .wipeout-specify-first-point-or-polyline = WIPEOUT 指定第一点或 [边框/多段线] <多段线>: .wipeout-specify-next-point-or-undo-close-n-poi = WIPEOUT 指定下一点或 [放弃/闭合](__ocs_arg_n__ 个点): .wipeout-polyline-select-a-closed-planar-polyli = WIPEOUT 多段线 选择闭合平面多段线: .wipeout-rectangular-specify-first-corner = WIPEOUT 矩形 指定第一个角点: .wipeout-rectangular-specify-opposite-corner = WIPEOUT 矩形 指定对角点: + .wipeout-frames-enter-frame-mode = WIPEOUT 边框 输入边框模式 [关/开/显示但不打印] <__ocs_arg_mode__>: + .wipeout-polyline-erase-source = WIPEOUT 多段线 是否删除源多段线?[是/否] <否>: + .wipeout-polyline-invalid = WIPEOUT 多段线:请选择一条由至少 3 个不相交顶点组成的直线、闭合、平面 2D 多段线。 + .setvar-frame-range = SETVAR:__ocs_fmt_0__ 必须为 0、1 或 2。 + .frames = 边框 + .display-but-not-plot = 显示但不打印 + .frames-on-not-plotted = 边框开,不打印 + .image-adjust = 图像调整 .x-ray = X 射线 .xline-specify-a-point = XLINE 指定点: .xline-specify-through-point = XLINE 指定通过点: diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index f0b3c306..9317f23d 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -1403,11 +1403,19 @@ draw = .usage-attdisp-on-off-normal = 用法: ATTDISP ON | OFF | NORMAL .usage-polysolid-width-height-select-a-polyline = 用法: POLYSOLID (select a polyline first) .usage-pyramid-radius-height-sides-default-4-si = 用法: PYRAMID [sides] (default 4 sides) - .wipeout-specify-first-point-or-polyline = WIPEOUT 指定第一點或 [多段線]: + .wipeout-specify-first-point-or-polyline = WIPEOUT 指定第一點或 [邊框/多段線] <多段線>: .wipeout-specify-next-point-or-undo-close-n-poi = WIPEOUT 指定下一點或 [放棄/閉合](__ocs_arg_n__ 個點): .wipeout-polyline-select-a-closed-planar-polyli = WIPEOUT 多段線 選擇閉合平面多段線: .wipeout-rectangular-specify-first-corner = WIPEOUT 矩形 指定第一個角點: .wipeout-rectangular-specify-opposite-corner = WIPEOUT 矩形 指定對角點: + .wipeout-frames-enter-frame-mode = WIPEOUT 邊框 輸入邊框模式 [關/開/顯示但不列印] <__ocs_arg_mode__>: + .wipeout-polyline-erase-source = WIPEOUT 多段線 是否刪除來源多段線?[是/否] <否>: + .wipeout-polyline-invalid = WIPEOUT 多段線:請選擇一條由至少 3 個不相交頂點組成的直線、閉合、平面 2D 多段線。 + .setvar-frame-range = SETVAR:__ocs_fmt_0__ 必須為 0、1 或 2。 + .frames = 邊框 + .display-but-not-plot = 顯示但不列印 + .frames-on-not-plotted = 邊框開,不列印 + .image-adjust = 圖像調整 .x-ray = X 射線 .xline-specify-a-point = XLINE 指定點: .xline-specify-through-point = XLINE 指定通過點: diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index a3411a7d..669d8c19 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -1014,7 +1014,10 @@ impl OpenCADStudio { self.commit_undo_delta(i, pending); } } - CmdResult::WipeoutFromPolyline(handle) => { + CmdResult::WipeoutFromPolyline { + handle, + erase_source, + } => { let wipeout = { let scene = &self.tabs[i].scene; scene @@ -1026,16 +1029,23 @@ impl OpenCADStudio { ) }; if let Some(wipeout) = wipeout { + if erase_source { + return self.apply_cmd_result(CmdResult::ReplaceMany( + vec![(handle, Vec::new())], + vec![wipeout], + )); + } return self.apply_cmd_result(CmdResult::CommitAndExit(wipeout)); } self.command_line.push_error( - "WIPEOUT Polyline: select a closed planar polyline with at least 3 vertices.", + crate::t!("WIPEOUT Polyline: select a straight, closed, planar 2D polyline with at least 3 non-intersecting vertices.").as_ref(), ); - if let Some(prompt) = - self.tabs[i].active_cmd.as_ref().map(|command| command.prompt()) - { - self.command_line.push_info(&prompt); - } + let command = + crate::modules::draw::draw::wipeout::WipeoutCommand::new_polyline(); + self.command_line.push_info( + &crate::command::CadCommand::prompt(&command), + ); + self.tabs[i].active_cmd = Some(Box::new(command)); } CmdResult::MviewSwitchLayout(layout) => { let task = self.on_layout_switch_preserving_command(layout); diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index b05809d9..b2e9ca5f 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -48,7 +48,11 @@ impl OpenCADStudio { let wo_cmd = match args.as_str() { "P" | "POLYLINE" => WipeoutCommand::new_polyline(), "R" | "RECTANGULAR" => WipeoutCommand::new_rectangular(), - _ => WipeoutCommand::new_polygonal(), + _ => WipeoutCommand::new_polygonal( + crate::modules::draw::draw::wipeout::wipeout_frame_mode( + &self.tabs[i].scene.document, + ), + ), }; self.command_line.push_info(&wo_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(wo_cmd)); diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index 174653f8..7745eef1 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -514,7 +514,15 @@ inventory::submit!(crate::command::CommandRegistration { "CMLJUST", "TEXTQLTY", "SORTENTS", + "FRAME", + "IMAGEFRAME", + "PDFFRAME", + "POINTCLOUDCLIPFRAME", "XCLIPFRAME", + "WIPEOUTFRAME", + "FRAMES0", + "FRAMES1", + "FRAMES2", "HALOGAP", "TRACEWID", "SKETCHINC", diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index 5eba1ee6..b04aa5fa 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -3,6 +3,9 @@ use super::*; impl OpenCADStudio { pub(super) fn dispatch_styleprops(&mut self, cmd: &str, i: usize) -> Option> { match cmd { + "FRAMES0" => return self.dispatch_styleprops("SETVAR FRAME 0", i), + "FRAMES1" => return self.dispatch_styleprops("SETVAR FRAME 1", i), + "FRAMES2" => return self.dispatch_styleprops("SETVAR FRAME 2", i), // COLOR — the colour applied to new // objects (CECOLOR). Bare COLOR reports the current value. "COLOR" | "COLOUR" | "CECOLOR" | "DDCOLOR" => { @@ -919,7 +922,12 @@ impl OpenCADStudio { | "CMLJUST" | "TEXTQLTY" | "SORTENTS" + | "FRAME" + | "IMAGEFRAME" + | "PDFFRAME" + | "POINTCLOUDCLIPFRAME" | "XCLIPFRAME" + | "WIPEOUTFRAME" | "HALOGAP" | "TRACEWID" | "SKETCHINC" @@ -945,9 +953,83 @@ impl OpenCADStudio { let value = it.next().map(|s| s.trim().to_string()); if name.is_empty() || name == "?" { self.command_line.push_info( - "SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR | CLAYER CELTYPE TEXTSTYLE (read-only)", + "SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR | CLAYER CELTYPE TEXTSTYLE (read-only)", ); } else { + let frame_kind = crate::scene::frame::kind_for_name(&name); + if name == "FRAME" || frame_kind.is_some() { + let current = frame_kind.map_or_else( + || crate::scene::frame::master_mode(&self.tabs[i].scene.document), + |kind| crate::scene::frame::mode(&self.tabs[i].scene.document, kind), + ); + match &value { + Some(value) => match value.parse::() { + Ok(mode @ 0..=2) => { + if current != mode { + let changed_kinds: Vec<_> = frame_kind.map_or_else( + || { + crate::scene::frame::ALL_KINDS + .into_iter() + .filter(|kind| { + crate::scene::frame::mode( + &self.tabs[i].scene.document, + *kind, + ) != mode + }) + .collect() + }, + |kind| vec![kind], + ); + self.push_undo_snapshot(i, &name); + if let Some(kind) = frame_kind { + crate::scene::frame::set_mode( + &mut self.tabs[i].scene.document, + kind, + mode, + ); + } else { + crate::scene::frame::set_master_mode( + &mut self.tabs[i].scene.document, + mode, + ); + } + let changes: Vec<_> = self.tabs[i] + .scene + .document + .entities() + .filter_map(|entity| { + changed_kinds + .iter() + .any(|kind| { + crate::scene::frame::affected(entity, *kind) + }) + .then_some(( + entity.common().handle, + crate::scene::ChangeKind::Modified, + )) + }) + .collect(); + if !changes.is_empty() { + self.tabs[i].scene.bump_entities(&changes); + } + self.tabs[i].dirty = true; + } + self.command_line + .push_output(&format!("{name} = {mode}")); + } + _ => self.command_line.push_error( + crate::tf!("SETVAR: {name} requires 0, 1, or 2.").as_ref(), + ), + }, + None => { + self.command_line.push_output(crate::tf!( + "Enter new value for {name} <{current}>:" + ).as_ref()); + self.pending_setvar = Some(name.clone()); + } + } + return Some(self.finish_dispatch(cmd)); + } // Parse a boolean given as 0/1 or ON/OFF. let parse_bool = |s: &str| match s.to_uppercase().as_str() { "1" | "ON" | "TRUE" => Some(true), @@ -1597,16 +1679,6 @@ impl OpenCADStudio { .map_err(|_| "SETVAR: integer value required.".into()), None => Ok((format!("SORTENTS = {}", h.sort_entities), false)), }, - "XCLIPFRAME" => match &value { - Some(v) => v - .parse::() - .map(|x| { - h.xclip_frame = x; - (format!("XCLIPFRAME = {x}"), true) - }) - .map_err(|_| "SETVAR: integer value required.".into()), - None => Ok((format!("XCLIPFRAME = {}", h.xclip_frame), false)), - }, "HALOGAP" => match &value { Some(v) => v .parse::() diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 7283db73..a3a3f4bb 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -168,7 +168,9 @@ fn plot_scene_content( Vec, crate::io::pdf_export::PlotGroupSplits, ) { - let (paper_wires, model_wires) = scene.plot_wire_groups(render_mode_override); + let (mut paper_wires, mut model_wires) = scene.plot_wire_groups(render_mode_override); + paper_wires.retain(|wire| wire.plot_visible); + model_wires.retain(|wire| wire.plot_visible); let paper_hatches = scene.paper_canvas_hatches().as_ref().clone(); let paper_wipeouts = scene.paper_canvas_wipeouts().as_ref().clone(); if scene.current_layout == "Model" { @@ -184,8 +186,9 @@ fn plot_scene_content( splits, ); } - let (model_pattern_wires, model_hatches, model_wipeouts) = + let (mut model_pattern_wires, model_hatches, model_wipeouts) = scene.viewport_plot_fills(); + model_pattern_wires.retain(|wire| wire.plot_visible); let (wires, hatches, wipeouts, splits) = if paper_space_last { let splits = crate::io::pdf_export::PlotGroupSplits { diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 089ddaac..69fa4c84 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -2141,6 +2141,8 @@ impl OpenCADStudio { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/command.rs b/src/command.rs index 2faf989e..35f1b904 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1297,7 +1297,11 @@ pub enum CmdResult { boundary_handle: Handle, }, /// Create a wipeout from an existing closed polyline in the active space. - WipeoutFromPolyline(Handle), + /// `erase_source` controls whether the source boundary is consumed. + WipeoutFromPolyline { + handle: Handle, + erase_source: bool, + }, /// Temporarily switch between paper and Model while MVIEW defines a new /// model-space window, keeping the command active. MviewSwitchLayout(String), diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index 109d3fad..8ae8f044 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -2069,6 +2069,8 @@ fn tessellate_dimension_inner( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -2100,6 +2102,8 @@ fn tessellate_dimension_inner( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -2131,6 +2135,8 @@ fn tessellate_dimension_inner( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -2163,6 +2169,8 @@ fn tessellate_dimension_inner( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -2211,6 +2219,8 @@ fn tessellate_dimension_inner( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/entities/extended.rs b/src/entities/extended.rs index c3a324fe..d5740ec2 100644 --- a/src/entities/extended.rs +++ b/src/entities/extended.rs @@ -301,6 +301,11 @@ fn geo_marker_lines(data: &GeoPositionMarkerData) -> Vec<[f64; 3]> { fn point_cloud_lines(data: &PointCloudData) -> Vec<[f64; 3]> { let mut points = Vec::new(); push_box(&mut points, data.extents_min, data.extents_max); + points +} + +fn point_cloud_clip_lines(data: &PointCloudData) -> Vec<[f64; 3]> { + let mut points = Vec::new(); if data.show_clipping { for clip in &data.clippings { if clip.vertices.len() < 2 { @@ -323,6 +328,11 @@ fn point_cloud_lines(data: &PointCloudData) -> Vec<[f64; 3]> { fn point_cloud_ex_lines(data: &PointCloudExData) -> Vec<[f64; 3]> { let mut points = Vec::new(); push_box(&mut points, data.extents_min, data.extents_max); + points +} + +fn point_cloud_ex_clip_lines(data: &PointCloudExData) -> Vec<[f64; 3]> { + let mut points = Vec::new(); if data.show_cropping { for crop in &data.croppings { if crop.points.len() < 2 { @@ -339,6 +349,14 @@ fn point_cloud_ex_lines(data: &PointCloudExData) -> Vec<[f64; 3]> { points } +pub(crate) fn point_cloud_frame_lines(entity: &ExtendedEntity) -> Option> { + match &entity.data { + ExtendedEntityData::PointCloud(data) => Some(point_cloud_clip_lines(data)), + ExtendedEntityData::PointCloudEx(data) => Some(point_cloud_ex_clip_lines(data)), + _ => None, + } +} + fn camera_lines(document: &acadrust::CadDocument, view_handle: Handle) -> Vec<[f64; 3]> { let Some(view) = document.views.iter().find(|view| view.handle == view_handle) else { return Vec::new(); diff --git a/src/entities/leader.rs b/src/entities/leader.rs index aec16ce7..bd14abfa 100644 --- a/src/entities/leader.rs +++ b/src/entities/leader.rs @@ -677,6 +677,8 @@ impl LeaderTess for Leader { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -853,6 +855,8 @@ impl LeaderTess for Leader { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index c2ade8ed..6cdfcf09 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -1506,6 +1506,8 @@ impl MultiLeaderTess for MultiLeader { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -1843,6 +1845,8 @@ impl MultiLeaderTess for MultiLeader { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -1913,6 +1917,8 @@ impl MultiLeaderTess for MultiLeader { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -1998,6 +2004,8 @@ impl MultiLeaderTess for MultiLeader { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -2038,6 +2046,8 @@ impl MultiLeaderTess for MultiLeader { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/entities/raster_image.rs b/src/entities/raster_image.rs index be4437fb..604d48d3 100644 --- a/src/entities/raster_image.rs +++ b/src/entities/raster_image.rs @@ -7,6 +7,7 @@ use crate::entities::text_support::{resolve_text_style, text_local_bounds}; use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; use crate::scene::convert::acad_to_render::{GlyphRun, TextStroke, RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; +use crate::scene::model::wire_model::SnapHint; use crate::scene::text::lff; // ── Shared geometry helpers ─────────────────────────────────────────────────── @@ -416,62 +417,112 @@ impl Transformable for RasterImage { // ── Wipeout ─────────────────────────────────────────────────────────────────── +fn wipeout_is_polygonal(wipeout: &Wipeout) -> bool { + wipeout.clipping_enabled + && wipeout.clip_boundary_vertices.len() >= 3 + && matches!( + wipeout.clip_type, + acadrust::entities::WipeoutClipType::Polygonal + ) +} + +fn wipeout_clip_to_world(wipeout: &Wipeout, point: &acadrust::types::Vector2) -> [f64; 3] { + let x = point.x + wipeout.size.x * 0.5; + let y = wipeout.size.y * 0.5 - point.y; + wipeout_plane(wipeout).point_at([x, y]) +} + +fn wipeout_plane(wipeout: &Wipeout) -> cadkernel::space::Plane { + cadkernel::space::Plane::from_axes( + [ + wipeout.insertion_point.x, + wipeout.insertion_point.y, + wipeout.insertion_point.z, + ], + [ + wipeout.u_vector.x, + wipeout.u_vector.y, + wipeout.u_vector.z, + ], + [ + wipeout.v_vector.x, + wipeout.v_vector.y, + wipeout.v_vector.z, + ], + ) +} + +fn wipeout_boundary(wipeout: &Wipeout) -> Vec<[f64; 3]> { + if wipeout_is_polygonal(wipeout) { + wipeout + .clip_boundary_vertices + .iter() + .map(|point| wipeout_clip_to_world(wipeout, point)) + .collect() + } else { + image_corners( + &wipeout.insertion_point, + &wipeout.u_vector, + &wipeout.v_vector, + wipeout.size.x, + wipeout.size.y, + ) + .to_vec() + } +} + +fn wipeout_pick_rings(wipeout: &Wipeout) -> Vec> { + let boundary = wipeout_boundary(wipeout); + if wipeout_is_polygonal(wipeout) + && matches!( + wipeout.clip_mode, + acadrust::entities::WipeoutClipMode::Inside + ) + { + vec![ + image_corners( + &wipeout.insertion_point, + &wipeout.u_vector, + &wipeout.v_vector, + wipeout.size.x, + wipeout.size.y, + ) + .to_vec(), + boundary, + ] + } else { + vec![boundary] + } +} + +fn wipeout_world_to_clip(wipeout: &Wipeout, world: [f64; 3]) -> Option { + let [x, y] = wipeout_plane(wipeout).project(world)?; + Some(acadrust::types::Vector2::new( + x - wipeout.size.x * 0.5, + wipeout.size.y * 0.5 - y, + )) +} + impl RenderConvertible for Wipeout { fn to_render(&self, _document: &acadrust::CadDocument) -> Option { - let corners = image_corners( - &self.insertion_point, - &self.u_vector, - &self.v_vector, - self.size.x, - self.size.y, - ); - - // If clipping is enabled and there's a polygon boundary, show that. - let pts = if self.clipping_enabled - && self.clip_boundary_vertices.len() >= 3 - && matches!( - self.clip_type, - acadrust::entities::WipeoutClipType::Polygonal - ) { - // Clip vertices are stored in image-pixel space, centred on the - // image (range ±size/2). The image's bottom-left corner sits at - // `insertion_point`, the image-Y axis points DOWN (per DXF - // "v_vector points down the image"), so map: - // x_off = (clip.x + size.x/2) × u_vector - // y_off = (size.y/2 − clip.y) × v_vector ← y flipped - let ox = self.insertion_point.x; - let oy = self.insertion_point.y; - let oz = self.insertion_point.z; - let mut poly: Vec<[f64; 3]> = self - .clip_boundary_vertices - .iter() - .map(|v| { - let cx = v.x + self.size.x * 0.5; - let cy = self.size.y * 0.5 - v.y; - let wx = self.u_vector.x * cx + self.v_vector.x * cy; - let wy = self.u_vector.y * cx + self.v_vector.y * cy; - let wz = self.u_vector.z * cx + self.v_vector.z * cy; - [ox + wx, oy + wy, oz + wz] - }) - .collect(); - // Close the polygon. - if let Some(&first) = poly.first() { - poly.push(first); - } - poly - } else { - // Rectangular boundary — just the border, no diagonals (mask area). - image_wire(corners, false) - }; + let boundary = wipeout_boundary(self); + let mut pts = boundary.clone(); + if let Some(&first) = pts.first() { + pts.push(first); + } Some(RenderEntity { - // Interior pick surface — a wipeout reads as a solid patch, so a - // click anywhere on it should select it. - pick_tris: crate::entities::common::quad_pick_tris(&corners), + pick_tris: cadkernel::space::polygon::triangulate_rings( + &wipeout_pick_rings(self), + cadkernel::geom2d::Tolerance::default(), + ), object: RenderObject::Lines(pts), - snap_pts: vec![], + snap_pts: boundary + .iter() + .map(|point| (glam::DVec3::from(*point), SnapHint::Endpoint)) + .collect(), tangent_geoms: vec![], - key_vertices: corners.to_vec(), + key_vertices: boundary, fill_tris: vec![], }) } @@ -479,33 +530,15 @@ impl RenderConvertible for Wipeout { impl Grippable for Wipeout { fn grips(&self) -> Vec { - // If polygonal clipping is active, expose individual polygon vertices as grips. - let is_polygon = self.clipping_enabled - && self.clip_boundary_vertices.len() >= 3 - && matches!( - self.clip_type, - acadrust::entities::WipeoutClipType::Polygonal - ); - - if is_polygon { - let ox = self.insertion_point.x; - let oy = self.insertion_point.y; - let oz = self.insertion_point.z; - // Same image-pixel-space → WCS mapping as `to_render` so grips - // sit exactly on the rendered polygon vertices. - self.clip_boundary_vertices - .iter() + if wipeout_is_polygonal(self) { + wipeout_boundary(self) + .into_iter() .enumerate() - .map(|(i, v)| { - let cx = v.x + self.size.x * 0.5; - let cy = self.size.y * 0.5 - v.y; - let wx = self.u_vector.x * cx + self.v_vector.x * cy; - let wy = self.u_vector.y * cx + self.v_vector.y * cy; - let wz = self.u_vector.z * cx + self.v_vector.z * cy; + .map(|(i, point)| { if i == 0 { - square_grip(i, glam::DVec3::new(ox + wx, oy + wy, oz + wz)) + square_grip(i, glam::DVec3::from(point)) } else { - center_grip(i, glam::DVec3::new(ox + wx, oy + wy, oz + wz)) + center_grip(i, glam::DVec3::from(point)) } }) .collect() @@ -527,61 +560,103 @@ impl Grippable for Wipeout { } fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { - let is_polygon = self.clipping_enabled - && self.clip_boundary_vertices.len() >= 3 - && matches!( - self.clip_type, - acadrust::entities::WipeoutClipType::Polygonal - ); - - if is_polygon { - // Move the clicked polygon vertex in world space → back-project to pixel space. - if let Some(v) = self.clip_boundary_vertices.get_mut(grip_id) { - // Compute current world position of this vertex. - let ox = self.insertion_point.x; - let oy = self.insertion_point.y; - let oz = self.insertion_point.z; - let cur_wx = - ox + self.u_vector.x * v.x * self.size.x + self.v_vector.x * v.y * self.size.y; - let cur_wy = - oy + self.u_vector.y * v.x * self.size.x + self.v_vector.y * v.y * self.size.y; - let cur_wz = - oz + self.u_vector.z * v.x * self.size.x + self.v_vector.z * v.y * self.size.y; + if wipeout_is_polygonal(self) { + if let Some(current) = self.clip_boundary_vertices.get(grip_id).cloned() { + let current_world = wipeout_clip_to_world(self, ¤t); let new_w = match apply { GripApply::Translate(d) => [ - cur_wx + d.x as f64, - cur_wy + d.y as f64, - cur_wz + d.z as f64, + current_world[0] + d.x as f64, + current_world[1] + d.y as f64, + current_world[2] + d.z as f64, ], GripApply::Absolute(p) => [p.x as f64, p.y as f64, p.z as f64], }; - // Back-project: solve for pixel coords using u_vector and v_vector. - // In 2D (XY plane): new_w - insertion_point = u_vec * vx * sx + v_vec * vy * sy - let dx = new_w[0] - self.insertion_point.x; - let dy = new_w[1] - self.insertion_point.y; - let ux = self.u_vector.x * self.size.x; - let uy = self.u_vector.y * self.size.x; - let vx = self.v_vector.x * self.size.y; - let vy = self.v_vector.y * self.size.y; - let det = ux * vy - uy * vx; - if det.abs() > 1e-12 { - v.x = (dx * vy - dy * vx) / det; - v.y = (ux * dy - uy * dx) / det; + if let Some(clip) = wipeout_world_to_clip(self, new_w) { + self.clip_boundary_vertices[grip_id] = clip; } } - } else if grip_id == 0 { - match apply { - GripApply::Translate(d) => { - self.insertion_point.x += d.x as f64; - self.insertion_point.y += d.y as f64; - self.insertion_point.z += d.z as f64; - } - GripApply::Absolute(p) => { - self.insertion_point.x = p.x as f64; - self.insertion_point.y = p.y as f64; - self.insertion_point.z = p.z as f64; - } + return; + } + + let corners = wipeout_boundary(self); + let Some(current) = corners.get(grip_id).copied() else { + return; + }; + let target = match apply { + GripApply::Translate(delta) => glam::DVec3::from(current) + delta, + GripApply::Absolute(point) => point, + }; + let u_hat = glam::DVec3::new(self.u_vector.x, self.u_vector.y, self.u_vector.z) + .normalize_or_zero(); + let v_hat = glam::DVec3::new(self.v_vector.x, self.v_vector.y, self.v_vector.z) + .normalize_or_zero(); + if u_hat == glam::DVec3::ZERO || v_hat == glam::DVec3::ZERO { + return; + } + let resize_plane = cadkernel::space::Plane::from_axes( + [0.0; 3], + u_hat.to_array(), + v_hat.to_array(), + ); + let (width, height, insertion) = match grip_id { + 0 => { + let fixed = glam::DVec3::from(corners[2]); + let Some([width, height]) = + resize_plane.project_vector((fixed - target).to_array()) + else { + return; + }; + (width, height, target) } + 1 => { + let fixed = glam::DVec3::from(corners[3]); + let Some([width, neg_height]) = + resize_plane.project_vector((target - fixed).to_array()) + else { + return; + }; + let height = -neg_height; + (width, height, fixed - v_hat * height) + } + 2 => { + let fixed = glam::DVec3::from(corners[0]); + let Some([width, height]) = + resize_plane.project_vector((target - fixed).to_array()) + else { + return; + }; + (width, height, fixed) + } + 3 => { + let fixed = glam::DVec3::from(corners[1]); + let Some([neg_width, height]) = + resize_plane.project_vector((target - fixed).to_array()) + else { + return; + }; + let width = -neg_width; + (width, height, fixed - u_hat * width) + } + _ => return, + }; + if width.abs() > 1e-9 && height.abs() > 1e-9 { + self.insertion_point = acadrust::types::Vector3::new( + insertion.x, + insertion.y, + insertion.z, + ); + let sx = self.size.x.abs().max(1e-9); + let sy = self.size.y.abs().max(1e-9); + self.u_vector = acadrust::types::Vector3::new( + u_hat.x * width / sx, + u_hat.y * width / sx, + u_hat.z * width / sx, + ); + self.v_vector = acadrust::types::Vector3::new( + v_hat.x * height / sy, + v_hat.y * height / sy, + v_hat.z * height / sy, + ); } } } @@ -607,76 +682,23 @@ impl PropertyEditable for Wipeout { PropSection { title: t!("Misc").into_owned(), props: vec![ - Property { - label: t!("Show image").into_owned(), - field: "wo_show_image", - value: PropValue::BoolToggle { - field: "wo_show_image", - value: show_image, - }, - }, - Property { - label: t!("Show clipped").into_owned(), - field: "wo_show_clipped", - value: PropValue::BoolToggle { - field: "wo_show_clipped", - value: show_clipped, - }, - }, - Property { - label: t!("Background transparency").into_owned(), - field: "wo_bg_transparency", - value: PropValue::BoolToggle { - field: "wo_bg_transparency", - value: bg_transparency, - }, - }, + ro(t!("Show image").as_ref(), "wo_show_image", if show_image { t!("Yes") } else { t!("No") }), + ro(t!("Show clipped").as_ref(), "wo_show_clipped", if show_clipped { t!("Yes") } else { t!("No") }), + ro(t!("Background transparency").as_ref(), "wo_bg_transparency", if bg_transparency { t!("Yes") } else { t!("No") }), + ], + }, + PropSection { + title: t!("Image Adjust").into_owned(), + props: vec![ + ro(t!("Brightness").as_ref(), "wo_brightness", self.brightness.to_string()), + ro(t!("Contrast").as_ref(), "wo_contrast", self.contrast.to_string()), + ro(t!("Fade").as_ref(), "wo_fade", self.fade.to_string()), ], }, ] } fn apply_geom_prop(&mut self, field: &str, value: &str) { - match field { - "wo_show_image" => { - let on = if value == "toggle" { - !self.flags.contains(acadrust::entities::WipeoutDisplayFlags::SHOW_IMAGE) - } else { - value == "true" - }; - self.set_frame_visible(on); - return; - } - "wo_show_clipped" => { - let on = if value == "toggle" { - !self - .flags - .contains(acadrust::entities::WipeoutDisplayFlags::USE_CLIPPING_BOUNDARY) - } else { - value == "true" - }; - if on { - self.flags |= acadrust::entities::WipeoutDisplayFlags::USE_CLIPPING_BOUNDARY; - } else { - self.flags -= acadrust::entities::WipeoutDisplayFlags::USE_CLIPPING_BOUNDARY; - } - return; - } - "wo_bg_transparency" => { - let on = if value == "toggle" { - !self.flags.contains(acadrust::entities::WipeoutDisplayFlags::TRANSPARENCY_ON) - } else { - value == "true" - }; - if on { - self.flags |= acadrust::entities::WipeoutDisplayFlags::TRANSPARENCY_ON; - } else { - self.flags -= acadrust::entities::WipeoutDisplayFlags::TRANSPARENCY_ON; - } - return; - } - _ => {} - } let Ok(v) = value.trim().parse::() else { return; }; diff --git a/src/entities/table.rs b/src/entities/table.rs index de3df94a..aa6a6ff6 100644 --- a/src/entities/table.rs +++ b/src/entities/table.rs @@ -1670,6 +1670,8 @@ pub fn tessellate_table( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/io/mod.rs b/src/io/mod.rs index 7edef281..1dda8e2b 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -1814,6 +1814,10 @@ fn vardict_value(doc: &CadDocument, name: &str) -> Option { } } +pub(crate) fn drawing_variable(doc: &CadDocument, name: &str) -> Option { + vardict_value(doc, name) +} + /// Write a drawing variable, creating the variable dictionary and record when /// needed so new drawings preserve the value too. pub(crate) fn set_drawing_variable(doc: &mut CadDocument, name: &str, value: &str) { diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 940da5bc..c4ec293b 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -875,6 +875,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Discussions" => Some(("common", "discussions")), "Discussions load from GitHub." => Some(("common", "discussions-load-from-github")), "Display" => Some(("common", "display")), + "Display but not plot" => Some(("draw", "display-but-not-plot")), "Distance" => Some(("common", "distance")), "Distance = %{dist}, Angle in XY Plane = %{angle_xy}°\n Delta X = %{dx}, Delta Y = %{dy}, Delta Z = %{dz}" => Some(("inquiry", "distance-dist-angle-in-xy-plane-angle-xy-delta")), "Distance = %{dist}, Angle in XY Plane = %{angle_xy}°, Angle from XY Plane = %{angle_z}°\n Delta X = %{dx}, Delta Y = %{dy}, Delta Z = %{dz}" => Some(("inquiry", "distance-dist-angle-in-xy-plane-angle-xy-angle")), @@ -1104,9 +1105,11 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Fraction format (DIMFRAC)" => Some(("common", "fraction-format-dimfrac")), "Fractional" => Some(("common", "fractional")), "Frame Text" => Some(("properties", "frame-text")), + "Frames" => Some(("draw", "frames")), "Frames & Print" => Some(("insert", "frames-print")), "Frames Off" => Some(("insert", "frames-off")), "Frames On" => Some(("insert", "frames-on")), + "Frames On, Not Plotted" => Some(("draw", "frames-on-not-plotted")), "Freeze" => Some(("layers", "freeze")), "From DXF" => Some(("properties", "from-dxf")), "Front" => Some(("common", "front")), @@ -1205,6 +1208,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Identifier" => Some(("properties", "identifier")), "Ignore" => Some(("properties", "ignore")), "Illuminance Distance" => Some(("properties", "illuminance-distance")), + "Image Adjust" => Some(("draw", "image-adjust")), "Import" => Some(("common", "import")), "In" => Some(("annotate", "in")), "Inactive" => Some(("layout", "inactive")), @@ -2223,6 +2227,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "SETBYLAYER: select entities first." => Some(("styles", "setbylayer-select-entities-first")), "SETVAR new value (blank to read):" => Some(("styles", "setvar-new-value-blank-to-read")), "SETVAR variable name:" => Some(("styles", "setvar-variable-name")), + "SETVAR: {name} requires 0, 1, or 2." => Some(("draw", "setvar-frame-range")), "SHEETSET: Sheet Set Manager not yet implemented." => Some(("view", "sheetset-sheet-set-manager-not-yet-implemented")), "SHX" => Some(("properties", "shx")), "SKETCH Pen down — move to sketch, click to lift, Enter to record:" => Some(("draw", "sketch-pen-down-move-to-sketch-click-to-lift-e")), @@ -2916,9 +2921,12 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Volume" => Some(("properties", "volume")), "WBLOCK Saved \"{block_name}\" → \"{}\"" => Some(("common", "wblock-saved-block-name")), "WBLOCK save failed: {error}" => Some(("common", "wblock-save-failed-error")), - "WIPEOUT Specify first point or [Polyline]:" => Some(("draw", "wipeout-specify-first-point-or-polyline")), + "WIPEOUT Specify first point or [Frames/Polyline] :" => Some(("draw", "wipeout-specify-first-point-or-polyline")), "WIPEOUT Specify next point or [Undo/Close] (%{n} points):" => Some(("draw", "wipeout-specify-next-point-or-undo-close-n-poi")), + "WIPEOUT Frames Enter frame mode [Off/On/DisplayButNotPlot] <%{mode}>:" => Some(("draw", "wipeout-frames-enter-frame-mode")), + "WIPEOUT Polyline Erase source polyline? [Yes/No] :" => Some(("draw", "wipeout-polyline-erase-source")), "WIPEOUT Polyline Select a closed planar polyline:" => Some(("draw", "wipeout-polyline-select-a-closed-planar-polyli")), + "WIPEOUT Polyline: select a straight, closed, planar 2D polyline with at least 3 non-intersecting vertices." => Some(("draw", "wipeout-polyline-invalid")), "WIPEOUT Rectangular Specify first corner:" => Some(("draw", "wipeout-rectangular-specify-first-corner")), "WIPEOUT Rectangular Specify opposite corner:" => Some(("draw", "wipeout-rectangular-specify-opposite-corner")), "Warning" => Some(("errors", "warning")), diff --git a/src/modules/annotate/aligned_dim.rs b/src/modules/annotate/aligned_dim.rs index 4481bba9..68ff5061 100644 --- a/src/modules/annotate/aligned_dim.rs +++ b/src/modules/annotate/aligned_dim.rs @@ -223,6 +223,8 @@ impl CadCommand for AlignedDimensionCommand { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -279,6 +281,8 @@ fn preview_aligned(p1: DVec3, p2: DVec3, dim_pt: DVec3) -> WireModel { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/annotate/angular_dim.rs b/src/modules/annotate/angular_dim.rs index 7d30edd6..1b93dd70 100644 --- a/src/modules/annotate/angular_dim.rs +++ b/src/modules/annotate/angular_dim.rs @@ -164,6 +164,8 @@ fn preview_wire(points: Vec) -> WireModel { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/annotate/diameter_dim.rs b/src/modules/annotate/diameter_dim.rs index ec331ea3..e319c171 100644 --- a/src/modules/annotate/diameter_dim.rs +++ b/src/modules/annotate/diameter_dim.rs @@ -195,6 +195,8 @@ fn preview_line(a: Vec3, b: Vec3) -> WireModel { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/annotate/dim_baseline.rs b/src/modules/annotate/dim_baseline.rs index 6918dda4..28d35e7b 100644 --- a/src/modules/annotate/dim_baseline.rs +++ b/src/modules/annotate/dim_baseline.rs @@ -183,6 +183,8 @@ impl CadCommand for DimBaselineCommand { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/annotate/dim_continue.rs b/src/modules/annotate/dim_continue.rs index 2bf1d97b..695c8b15 100644 --- a/src/modules/annotate/dim_continue.rs +++ b/src/modules/annotate/dim_continue.rs @@ -163,6 +163,8 @@ impl CadCommand for DimContinueCommand { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/annotate/dimjogline.rs b/src/modules/annotate/dimjogline.rs index 6000b212..ab314a01 100644 --- a/src/modules/annotate/dimjogline.rs +++ b/src/modules/annotate/dimjogline.rs @@ -89,6 +89,8 @@ impl CadCommand for DimJogLineCommand { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/annotate/dimtedit.rs b/src/modules/annotate/dimtedit.rs index 59c04b1a..891ff95f 100644 --- a/src/modules/annotate/dimtedit.rs +++ b/src/modules/annotate/dimtedit.rs @@ -106,6 +106,8 @@ impl CadCommand for DimTeditCommand { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/annotate/leader_cmd.rs b/src/modules/annotate/leader_cmd.rs index 48b6d4ad..9ef884b6 100644 --- a/src/modules/annotate/leader_cmd.rs +++ b/src/modules/annotate/leader_cmd.rs @@ -303,6 +303,8 @@ fn preview_wire(pts: &[Vec3], arrow_size: f32) -> WireModel { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/annotate/linear_dim.rs b/src/modules/annotate/linear_dim.rs index ca106e1b..fbc6c3d3 100644 --- a/src/modules/annotate/linear_dim.rs +++ b/src/modules/annotate/linear_dim.rs @@ -240,6 +240,8 @@ fn preview_wire(points: Vec) -> WireModel { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/annotate/mleader_cmd.rs b/src/modules/annotate/mleader_cmd.rs index eb1963f5..896e2fd4 100644 --- a/src/modules/annotate/mleader_cmd.rs +++ b/src/modules/annotate/mleader_cmd.rs @@ -230,6 +230,8 @@ fn preview_wire(pts: &[Vec3], arrow_size: f32) -> WireModel { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/annotate/mleader_edit.rs b/src/modules/annotate/mleader_edit.rs index b24f436b..b91e9d2e 100644 --- a/src/modules/annotate/mleader_edit.rs +++ b/src/modules/annotate/mleader_edit.rs @@ -465,6 +465,8 @@ fn preview_wire(pts: &[Vec3]) -> WireModel { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/annotate/ordinate_dim.rs b/src/modules/annotate/ordinate_dim.rs index 9827d490..df3564fc 100644 --- a/src/modules/annotate/ordinate_dim.rs +++ b/src/modules/annotate/ordinate_dim.rs @@ -140,6 +140,8 @@ fn preview_wire(points: Vec) -> WireModel { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/annotate/qdim.rs b/src/modules/annotate/qdim.rs index 486caf2d..e9320510 100644 --- a/src/modules/annotate/qdim.rs +++ b/src/modules/annotate/qdim.rs @@ -96,6 +96,8 @@ impl CadCommand for QdimCommand { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/annotate/radius_dim.rs b/src/modules/annotate/radius_dim.rs index 3c8ff8e4..6aa43017 100644 --- a/src/modules/annotate/radius_dim.rs +++ b/src/modules/annotate/radius_dim.rs @@ -200,6 +200,8 @@ fn preview_wire(points: Vec) -> WireModel { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/annotate/table_cmd.rs b/src/modules/annotate/table_cmd.rs index e5d59050..4173d763 100644 --- a/src/modules/annotate/table_cmd.rs +++ b/src/modules/annotate/table_cmd.rs @@ -202,6 +202,8 @@ impl CadCommand for TableCommand { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/annotate/tolerance_cmd.rs b/src/modules/annotate/tolerance_cmd.rs index d78deb17..18c11839 100644 --- a/src/modules/annotate/tolerance_cmd.rs +++ b/src/modules/annotate/tolerance_cmd.rs @@ -106,6 +106,8 @@ impl CadCommand for ToleranceCommand { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/draw/draw/attdef.rs b/src/modules/draw/draw/attdef.rs index d1990d1e..fa2c38dd 100644 --- a/src/modules/draw/draw/attdef.rs +++ b/src/modules/draw/draw/attdef.rs @@ -204,6 +204,8 @@ impl CadCommand for AttdefCommand { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/draw/draw/hatch.rs b/src/modules/draw/draw/hatch.rs index 3f97f4a7..e8637358 100644 --- a/src/modules/draw/draw/hatch.rs +++ b/src/modules/draw/draw/hatch.rs @@ -348,6 +348,8 @@ impl HatchCommand { scale: source.scale, world_origin: origin, boundary_wcs: Some(std::sync::Arc::new(wcs)), + fill_plane: None, + fill_plane_boundary: None, boundary_exterior: Some(std::sync::Arc::new(exterior)), boundary_sources: Some(std::sync::Arc::new(boundary_sources)), draw_depth: source.draw_depth, @@ -387,6 +389,8 @@ impl HatchCommand { scale: 1.0, world_origin: origin, boundary_wcs: Some(std::sync::Arc::new(wcs)), + fill_plane: None, + fill_plane_boundary: None, boundary_exterior: Some(std::sync::Arc::new(exterior)), boundary_sources: Some(std::sync::Arc::new(boundary_sources)), draw_depth: 0.0, @@ -699,6 +703,8 @@ impl GradientCommand { scale: 1.0, world_origin: origin, boundary_wcs: Some(std::sync::Arc::new(wcs)), + fill_plane: None, + fill_plane_boundary: None, boundary_exterior: Some(std::sync::Arc::new(exterior)), boundary_sources: Some(std::sync::Arc::new(boundary_sources)), draw_depth: 0.0, diff --git a/src/modules/draw/draw/mline.rs b/src/modules/draw/draw/mline.rs index 4bef4224..120840e5 100644 --- a/src/modules/draw/draw/mline.rs +++ b/src/modules/draw/draw/mline.rs @@ -184,6 +184,8 @@ impl CadCommand for MlineCommand { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/draw/draw/raster_image.rs b/src/modules/draw/draw/raster_image.rs index 5b337394..7f97be78 100644 --- a/src/modules/draw/draw/raster_image.rs +++ b/src/modules/draw/draw/raster_image.rs @@ -126,6 +126,8 @@ impl CadCommand for ImageCommand { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/draw/draw/ray.rs b/src/modules/draw/draw/ray.rs index a3e9c132..14696aec 100644 --- a/src/modules/draw/draw/ray.rs +++ b/src/modules/draw/draw/ray.rs @@ -88,6 +88,8 @@ impl CadCommand for RayCommand { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -188,6 +190,8 @@ impl CadCommand for XLineCommand { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/draw/draw/wipeout.rs b/src/modules/draw/draw/wipeout.rs index 7a0a3976..5d4a93f8 100644 --- a/src/modules/draw/draw/wipeout.rs +++ b/src/modules/draw/draw/wipeout.rs @@ -2,7 +2,9 @@ use acadrust::entities::{Wipeout, WipeoutClipType}; use acadrust::types::{Vector2, Vector3}; -use acadrust::{EntityType, Handle}; +use acadrust::{CadDocument, EntityType, Handle}; +use cadkernel::geom2d::{polygon_frame, Tolerance}; +use cadkernel::space::Plane; use glam::DVec3; use crate::t; @@ -26,6 +28,8 @@ pub struct WipeoutCommand { first: Option, points: Vec, plane: WorkingPlane, + selected_polyline: Option, + frame_mode: i16, } #[derive(Clone, Copy, PartialEq)] @@ -33,15 +37,19 @@ enum WipeoutMode { Draw, Polyline, Rectangular, + Frames, + ErasePolyline, } impl WipeoutCommand { - pub fn new_polygonal() -> Self { + pub fn new_polygonal(frame_mode: i16) -> Self { Self { mode: WipeoutMode::Draw, first: None, points: Vec::new(), plane: WorkingPlane::default(), + selected_polyline: None, + frame_mode: frame_mode.clamp(0, 2), } } @@ -51,6 +59,8 @@ impl WipeoutCommand { first: None, points: Vec::new(), plane: WorkingPlane::default(), + selected_polyline: None, + frame_mode: 1, } } @@ -61,17 +71,27 @@ impl WipeoutCommand { first: None, points: Vec::new(), plane: WorkingPlane::default(), + selected_polyline: None, + frame_mode: 1, } } fn finish_draw(&self) -> CmdResult { - let local: Vec = self + let local: Vec<[f64; 2]> = self .points .iter() - .map(|point| self.plane.to_local(*point)) + .map(|point| { + let point = self.plane.to_local(*point); + [point.x, point.y] + }) .collect(); - match make_poly_wipeout(&local) { - Some(entity) => CmdResult::CommitAndExit(self.plane.place_entity(entity)), + let plane = Plane::from_axes( + self.plane.origin.to_array(), + self.plane.x.to_array(), + self.plane.y.to_array(), + ); + match make_poly_wipeout(&local, plane) { + Some(entity) => CmdResult::CommitAndExit(entity), None => CmdResult::NeedPoint, } } @@ -94,7 +114,7 @@ impl CadCommand for WipeoutCommand { fn prompt(&self) -> String { match self.mode { WipeoutMode::Draw if self.points.is_empty() => { - t!("WIPEOUT Specify first point or [Polyline]:").into_owned() + t!("WIPEOUT Specify first point or [Frames/Polyline] :").into_owned() } WipeoutMode::Draw => { let n = self.points.len(); @@ -113,18 +133,38 @@ impl CadCommand for WipeoutCommand { WipeoutMode::Rectangular => { t!("WIPEOUT Rectangular Specify opposite corner:").into_owned() } + WipeoutMode::Frames => t!( + "WIPEOUT Frames Enter frame mode [Off/On/DisplayButNotPlot] <%{mode}>:", + mode = self.frame_mode + ) + .into_owned(), + WipeoutMode::ErasePolyline => { + t!("WIPEOUT Polyline Erase source polyline? [Yes/No] :").into_owned() + } } } fn options(&self) -> Vec { match self.mode { WipeoutMode::Draw if self.points.is_empty() => { - vec![CmdOption::new(t!("Polyline").as_ref(), "P")] + vec![ + CmdOption::new(t!("Frames").as_ref(), "F"), + CmdOption::new(t!("Polyline").as_ref(), "P"), + ] } WipeoutMode::Draw => vec![ CmdOption::new(t!("Undo").as_ref(), "U"), CmdOption::new(t!("Close").as_ref(), "C"), ], + WipeoutMode::Frames => vec![ + CmdOption::new(t!("Off").as_ref(), "OFF"), + CmdOption::new(t!("On").as_ref(), "ON"), + CmdOption::new(t!("Display but not plot").as_ref(), "D"), + ], + WipeoutMode::ErasePolyline => vec![ + CmdOption::new(t!("Yes").as_ref(), "Y"), + CmdOption::new(t!("No").as_ref(), "N"), + ], _ => Vec::new(), } } @@ -133,9 +173,13 @@ impl CadCommand for WipeoutCommand { match self.mode { WipeoutMode::Draw => { if let Some(first) = self.points.first() { - let distance_squared = - (point.x - first.x).powi(2) + (point.y - first.y).powi(2); - if self.points.len() >= 3 && distance_squared < 1e-12 { + let first = self.plane.to_local(*first); + let point = self.plane.to_local(point); + let distance = cadkernel::geom2d::Vec2::new(point.x, point.y) + .distance(cadkernel::geom2d::Vec2::new(first.x, first.y)); + if self.points.len() >= 3 + && distance <= Tolerance::default().linear() + { return self.finish_draw(); } } @@ -146,21 +190,35 @@ impl CadCommand for WipeoutCommand { if let Some(first) = self.first { let first = self.plane.to_local(first); let point = self.plane.to_local(point); - CmdResult::CommitAndExit( - self.plane.place_entity(make_rect_wipeout(first, point)), - ) + let plane = Plane::from_axes( + self.plane.origin.to_array(), + self.plane.x.to_array(), + self.plane.y.to_array(), + ); + make_rect_wipeout(first, point, plane) + .map_or(CmdResult::NeedPoint, CmdResult::CommitAndExit) } else { self.first = Some(point); CmdResult::NeedPoint } } - WipeoutMode::Polyline => CmdResult::NeedPoint, + WipeoutMode::Polyline | WipeoutMode::Frames | WipeoutMode::ErasePolyline => { + CmdResult::NeedPoint + } } } fn on_enter(&mut self) -> CmdResult { match self.mode { + WipeoutMode::Draw if self.points.is_empty() => { + self.mode = WipeoutMode::Polyline; + CmdResult::NeedPoint + } WipeoutMode::Draw if self.points.len() >= 3 => self.finish_draw(), + WipeoutMode::Frames => { + CmdResult::Dispatch(format!("WIPEOUTFRAME {}", self.frame_mode)) + } + WipeoutMode::ErasePolyline => self.finish_polyline(false), _ => CmdResult::Cancel, } } @@ -177,33 +235,55 @@ impl CadCommand for WipeoutCommand { if handle.is_null() { CmdResult::NeedPoint } else { - CmdResult::WipeoutFromPolyline(handle) + self.selected_polyline = Some(handle); + self.mode = WipeoutMode::ErasePolyline; + CmdResult::NeedPoint } } fn wants_text_input(&self) -> bool { - self.mode == WipeoutMode::Draw + matches!( + self.mode, + WipeoutMode::Draw | WipeoutMode::Frames | WipeoutMode::ErasePolyline + ) } fn point_step_accepts_keywords(&self) -> bool { - self.mode == WipeoutMode::Draw + matches!( + self.mode, + WipeoutMode::Draw | WipeoutMode::Frames | WipeoutMode::ErasePolyline + ) } fn on_text_input(&mut self, text: &str) -> Option { - if self.mode != WipeoutMode::Draw { - return None; - } - match text.trim().to_ascii_uppercase().as_str() { - "P" | "POLYLINE" if self.points.is_empty() => { - self.mode = WipeoutMode::Polyline; - Some(CmdResult::NeedPoint) - } - "U" | "UNDO" if !self.points.is_empty() => { - Some(self.undo_point()) - } - "C" | "CLOSE" if self.points.len() >= 3 => { - Some(self.finish_draw()) - } + let text = text.trim().to_ascii_uppercase(); + match self.mode { + WipeoutMode::Draw => match text.as_str() { + "F" | "FRAMES" if self.points.is_empty() => { + self.mode = WipeoutMode::Frames; + Some(CmdResult::NeedPoint) + } + "P" | "POLYLINE" if self.points.is_empty() => { + self.mode = WipeoutMode::Polyline; + Some(CmdResult::NeedPoint) + } + "U" | "UNDO" if !self.points.is_empty() => Some(self.undo_point()), + "C" | "CLOSE" if self.points.len() >= 3 => Some(self.finish_draw()), + _ => None, + }, + WipeoutMode::Frames => match text.as_str() { + "0" | "OFF" => Some(CmdResult::Dispatch("WIPEOUTFRAME 0".into())), + "1" | "ON" => Some(CmdResult::Dispatch("WIPEOUTFRAME 1".into())), + "2" | "D" | "DISPLAYBUTNOTPLOT" => { + Some(CmdResult::Dispatch("WIPEOUTFRAME 2".into())) + } + _ => None, + }, + WipeoutMode::ErasePolyline => match text.as_str() { + "Y" | "YES" => Some(self.finish_polyline(true)), + "N" | "NO" => Some(self.finish_polyline(false)), + _ => None, + }, _ => None, } } @@ -264,250 +344,115 @@ impl CadCommand for WipeoutCommand { false, )) } - WipeoutMode::Polyline => None, + WipeoutMode::Polyline | WipeoutMode::Frames | WipeoutMode::ErasePolyline => None, } } } -fn make_rect_wipeout(first: DVec3, second: DVec3) -> EntityType { - let corner1 = Vector3::new( - first.x.min(second.x), - first.y.min(second.y), - first.z, - ); - let corner2 = Vector3::new( - first.x.max(second.x), - first.y.max(second.y), - first.z, - ); - EntityType::Wipeout(Wipeout::from_corners(corner1, corner2)) -} - -fn same_point(first: DVec3, second: DVec3) -> bool { - (first - second).length_squared() < 1e-18 -} - -fn clean_boundary(points: &[DVec3]) -> Vec { - let mut clean = Vec::with_capacity(points.len()); - for point in points.iter().copied().filter(|point| point.is_finite()) { - if clean.last().is_none_or(|last| !same_point(*last, point)) { - clean.push(point); - } - } - if clean.len() > 1 && same_point(clean[0], *clean.last().unwrap()) { - clean.pop(); - } - clean -} - -fn make_poly_wipeout(points: &[DVec3]) -> Option { - let points = clean_boundary(points); - if points.len() < 3 { - return None; - } - let z = points[0].z; - if points.iter().any(|point| (point.z - z).abs() > 1e-7) { - return None; - } - - let mut min_x = points[0].x; - let mut min_y = points[0].y; - let mut max_x = points[0].x; - let mut max_y = points[0].y; - for point in points.iter().skip(1) { - min_x = min_x.min(point.x); - min_y = min_y.min(point.y); - max_x = max_x.max(point.x); - max_y = max_y.max(point.y); - } - let width = max_x - min_x; - let height = max_y - min_y; - if width < 1e-9 || height < 1e-9 { - return None; - } - - // Stable signed area around a local origin rejects collinear boundaries - // without losing precision on large world coordinates. - let origin = points[0]; - let mut twice_area = 0.0; - for index in 0..points.len() { - let a = points[index] - origin; - let b = points[(index + 1) % points.len()] - origin; - twice_area += a.x * b.y - b.x * a.y; - } - if twice_area.abs() <= width * height * 1e-10 { - return None; - } - - // Wipeout clip vertices use image-pixel coordinates centred on the image: - // X grows right, Y grows up while the stored V vector points down. - let clip_boundary_vertices = points - .iter() - .map(|point| { - let normalized_x = (point.x - min_x) / width; - let normalized_y = (point.y - min_y) / height; - Vector2::new(normalized_x - 0.5, 0.5 - normalized_y) +impl WipeoutCommand { + fn finish_polyline(&self, erase_source: bool) -> CmdResult { + self.selected_polyline.map_or(CmdResult::Cancel, |handle| { + CmdResult::WipeoutFromPolyline { + handle, + erase_source, + } }) - .collect(); + } +} + +pub(crate) fn wipeout_frame_mode(document: &CadDocument) -> i16 { + crate::scene::frame::mode(document, crate::scene::frame::FrameKind::Wipeout) +} + +fn vector(value: [f64; 3]) -> Vector3 { + Vector3::new(value[0], value[1], value[2]) +} + +fn make_rect_wipeout(first: DVec3, second: DVec3, plane: Plane) -> Option { + plane.normal()?; + let points = [ + [first.x, first.y], + [second.x, first.y], + [second.x, second.y], + [first.x, second.y], + ]; + let frame = polygon_frame(&points, Tolerance::default())?; let mut wipeout = Wipeout::new(); - wipeout.insertion_point = Vector3::new(min_x, min_y, z); - wipeout.u_vector = Vector3::new(width, 0.0, 0.0); - wipeout.v_vector = Vector3::new(0.0, height, 0.0); + wipeout.insertion_point = vector(plane.point_at(frame.origin)); + wipeout.u_vector = vector(plane.x_axis) * frame.size[0]; + wipeout.v_vector = vector(plane.y_axis) * frame.size[1]; + Some(EntityType::Wipeout(wipeout)) +} + +fn make_poly_wipeout(points: &[[f64; 2]], plane: Plane) -> Option { + plane.normal()?; + let frame = polygon_frame(points, Tolerance::default())?; + let [width, height] = frame.size; + let mut wipeout = Wipeout::new(); + wipeout.insertion_point = vector(plane.point_at(frame.origin)); + wipeout.u_vector = vector(plane.x_axis) * width; + wipeout.v_vector = vector(plane.y_axis) * height; wipeout.size = Vector2::new(1.0, 1.0); wipeout.clip_type = WipeoutClipType::Polygonal; - wipeout.clip_boundary_vertices = clip_boundary_vertices; + wipeout.clip_boundary_vertices = frame + .points + .iter() + .map(|point| { + Vector2::new( + (point[0] - frame.origin[0]) / width - 0.5, + 0.5 - (point[1] - frame.origin[1]) / height, + ) + }) + .collect(); wipeout.clipping_enabled = true; Some(EntityType::Wipeout(wipeout)) } -fn explicitly_closed(points: &[DVec3]) -> bool { - points.len() >= 4 && same_point(points[0], *points.last().unwrap()) -} - -fn sample_2d_polyline( - vertices: &[([f64; 2], f64)], - elevation: f64, - normal: (f64, f64, f64), -) -> Vec { - if vertices.len() < 3 { - return Vec::new(); - } - let to_wcs = |point: [f64; 2]| { - let point = crate::scene::view::transform::ocs_point_to_wcs( - (point[0], point[1], elevation), - normal, - ); - DVec3::new(point.0, point.1, point.2) - }; - let mut points = vec![to_wcs(vertices[0].0)]; - for index in 0..vertices.len() { - let (start, bulge) = vertices[index]; - let end_index = (index + 1) % vertices.len(); - let end = vertices[end_index].0; - if let Some(arc) = - crate::entities::common::BulgeArc::from_bulge(start, end, bulge) - { - let steps = ((arc.sweep.abs() / std::f64::consts::TAU * 64.0).ceil() - as usize) - .clamp(4, 64); - for step in 1..=steps { - if end_index == 0 && step == steps { - break; - } - points.push(to_wcs(arc.sample(step as f64 / steps as f64))); - } - } else if end_index != 0 { - points.push(to_wcs(end)); - } - } - points +fn explicitly_closed(points: &[[f64; 2]]) -> bool { + points.len() >= 4 + && cadkernel::geom2d::Vec2::from(points[0]) + .distance(cadkernel::geom2d::Vec2::from(*points.last().unwrap())) + <= Tolerance::default().linear() } /// Build a wipeout boundary from a picked closed polyline without consuming -/// the source entity. Bulged 2D segments are sampled into the polygonal mask. +/// the source entity. Only straight, closed 2D polygonal boundaries qualify. pub(crate) fn wipeout_from_polyline(entity: &EntityType) -> Option { - let points = match entity { + fn from_ocs(points: &[[f64; 2]], normal: Vector3, elevation: f64) -> Option { + make_poly_wipeout(points, crate::entities::curve::ocs_plane(normal, elevation)) + } + + match entity { EntityType::LwPolyline(polyline) => { - let raw: Vec = polyline + if polyline.vertices.iter().any(|vertex| vertex.bulge != 0.0) { + return None; + } + let raw: Vec<[f64; 2]> = polyline .vertices .iter() - .map(|vertex| DVec3::new(vertex.location.x, vertex.location.y, polyline.elevation)) + .map(|vertex| [vertex.location.x, vertex.location.y]) .collect(); if !polyline.is_closed && !explicitly_closed(&raw) { return None; } - let vertices: Vec<([f64; 2], f64)> = polyline - .vertices - .iter() - .map(|vertex| { - ( - [vertex.location.x, vertex.location.y], - vertex.bulge, - ) - }) - .collect(); - sample_2d_polyline( - &vertices, - polyline.elevation, - ( - polyline.normal.x, - polyline.normal.y, - polyline.normal.z, - ), - ) + from_ocs(&raw, polyline.normal, polyline.elevation) } EntityType::Polyline2D(polyline) => { - let raw: Vec = polyline + if polyline.vertices.iter().any(|vertex| vertex.bulge != 0.0) { + return None; + } + let raw: Vec<[f64; 2]> = polyline .vertices .iter() - .map(|vertex| { - DVec3::new( - vertex.location.x, - vertex.location.y, - polyline.elevation, - ) - }) + .map(|vertex| [vertex.location.x, vertex.location.y]) .collect(); if !polyline.is_closed() && !explicitly_closed(&raw) { return None; } - let vertices: Vec<([f64; 2], f64)> = polyline - .vertices - .iter() - .map(|vertex| { - ( - [vertex.location.x, vertex.location.y], - vertex.bulge, - ) - }) - .collect(); - sample_2d_polyline( - &vertices, - polyline.elevation, - ( - polyline.normal.x, - polyline.normal.y, - polyline.normal.z, - ), - ) - } - EntityType::Polyline(polyline) => { - let points: Vec = polyline - .vertices - .iter() - .map(|vertex| { - DVec3::new( - vertex.location.x, - vertex.location.y, - vertex.location.z, - ) - }) - .collect(); - if !polyline.is_closed() && !explicitly_closed(&points) { - return None; - } - points - } - EntityType::Polyline3D(polyline) => { - let points: Vec = polyline - .vertices - .iter() - .map(|vertex| { - DVec3::new( - vertex.position.x, - vertex.position.y, - vertex.position.z, - ) - }) - .collect(); - if !polyline.flags.closed && !explicitly_closed(&points) { - return None; - } - points + from_ocs(&raw, polyline.normal, polyline.elevation) } _ => return None, - }; - make_poly_wipeout(&points) + } } // ── Autocomplete registry ───────────────────────────────── diff --git a/src/modules/draw/inquiry/area.rs b/src/modules/draw/inquiry/area.rs index 97d5f32e..f9f34aca 100644 --- a/src/modules/draw/inquiry/area.rs +++ b/src/modules/draw/inquiry/area.rs @@ -365,6 +365,8 @@ impl CadCommand for AreaCommand { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/draw/inquiry/dist.rs b/src/modules/draw/inquiry/dist.rs index d493ec6a..157318ca 100644 --- a/src/modules/draw/inquiry/dist.rs +++ b/src/modules/draw/inquiry/dist.rs @@ -88,6 +88,8 @@ impl CadCommand for DistCommand { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/draw/inquiry/measuregeom.rs b/src/modules/draw/inquiry/measuregeom.rs index 1c96c33c..d9eaa56a 100644 --- a/src/modules/draw/inquiry/measuregeom.rs +++ b/src/modules/draw/inquiry/measuregeom.rs @@ -73,6 +73,8 @@ impl MeasureGeomCommand { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/draw/modify/trim.rs b/src/modules/draw/modify/trim.rs index b170d8a7..bfd54b3c 100644 --- a/src/modules/draw/modify/trim.rs +++ b/src/modules/draw/modify/trim.rs @@ -3724,6 +3724,8 @@ fn preview_wire(points: Vec<[f32; 3]>, color: [f32; 4], name: &str) -> WireModel taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/insert/mod.rs b/src/modules/insert/mod.rs index d758a476..81d6c68b 100644 --- a/src/modules/insert/mod.rs +++ b/src/modules/insert/mod.rs @@ -69,7 +69,7 @@ impl CadModule for InsertModule { ), ( "FRAMES2", - "Frames & Print", + "Frames On, Not Plotted", IconKind::Svg(include_bytes!( "../../../assets/icons/underlay_frames.svg" )), diff --git a/src/modules/model/primitive_cmd.rs b/src/modules/model/primitive_cmd.rs index a8c420b7..5eb27f24 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -514,6 +514,8 @@ fn wire(name: &str, points: Vec<[f32; 3]>) -> WireModel { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/view/plot_window.rs b/src/modules/view/plot_window.rs index 3aeac951..31a888de 100644 --- a/src/modules/view/plot_window.rs +++ b/src/modules/view/plot_window.rs @@ -57,6 +57,8 @@ impl CadCommand for PlotWindowCommand { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/modules/view/zoom_window.rs b/src/modules/view/zoom_window.rs index 76a7a09b..dd91f094 100644 --- a/src/modules/view/zoom_window.rs +++ b/src/modules/view/zoom_window.rs @@ -139,6 +139,8 @@ impl CadCommand for ZoomWindowCommand { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index d5c1d5f1..d8f79c6c 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -73,6 +73,8 @@ pub struct LocalWire { /// shader band grows with a scaled insert. `0.0` = a normal wire. pub world_width: f32, pub plinegen: bool, + pub plot_visible: bool, + pub hide_unselected: bool, /// Set at construction; used to discriminate fill-only GPU batches from /// stroke batches in [`StyleKey`]. Derived from /// `points.is_empty() && !fill_tris.is_empty()`. @@ -729,6 +731,8 @@ fn tessellate_sub_local( return vec![]; } + let frame_mode = crate::scene::frame::entity_kind(sub) + .map(|kind| crate::scene::frame::mode(doc, kind)); let mut result = Vec::with_capacity(wires_out.len()); for wire in wires_out { // Per-wire point-count cap: a single wire that exceeds this is skipped @@ -804,6 +808,8 @@ fn tessellate_sub_local( line_weight_px: lw_px, world_width: wire.world_width, plinegen: wire.plinegen, + plot_visible: frame_mode.is_none_or(|mode| mode == 1), + hide_unselected: frame_mode == Some(0), is_fill_only, color_is_byblock: color_is_byblock && wire_on_base_color, lt_is_byblock, @@ -1362,6 +1368,8 @@ struct StyleKey { /// switchable after block geometry is merged by style. fill_is_2d_solid: bool, fill_is_3d: bool, + plot_visible: bool, + hide_unselected: bool, /// Bit-cast composed block-local depth for band wires (`0` = no override). /// Keeps bands of different in-block draw ranks in separate batches so /// each finalized WireModel carries one correct `depth_override`. @@ -1396,6 +1404,8 @@ struct BatchEntry { fill_tris_low: Vec<[f32; 3]>, fill_is_3d: bool, fill_is_2d_solid: bool, + plot_visible: bool, + hide_unselected: bool, /// Accumulated thickness-wall pick geometry, paired high/low like /// `fill_tris`. Pick-only — no GPU batch reads this. pick_tris: Vec<[f32; 3]>, @@ -1460,6 +1470,8 @@ impl BatchEntry { _is_fill_only: bool, fill_is_2d_solid: bool, fill_is_3d: bool, + plot_visible: bool, + hide_unselected: bool, ) -> Self { // `is_fill_only` is part of the StyleKey hash so greek fills never // share a batch with regular wires (otherwise the finalized @@ -1479,6 +1491,8 @@ impl BatchEntry { plinegen, fill_is_2d_solid, fill_is_3d, + plot_visible, + hide_unselected, min_x: f32::INFINITY, min_y: f32::INFINITY, max_x: f32::NEG_INFINITY, @@ -1496,6 +1510,10 @@ impl Batches { .into_iter() .chain(self.by_style.into_values()) .map(|mut b| { + if b.hide_unselected && !selected { + b.points.clear(); + b.points_low.clear(); + } let aabb = if b.min_x.is_infinite() { WireModel::UNBOUNDED_AABB } else { @@ -1521,6 +1539,8 @@ impl Batches { taper_widths: Vec::new(), world_width: b.world_width, depth_override: b.local_depth, + display_visible: !b.hide_unselected || selected, + plot_visible: b.plot_visible, fill_is_3d: b.fill_is_3d, fill_is_2d_solid: b.fill_is_2d_solid, render_instance: None, @@ -1573,6 +1593,8 @@ fn style_key( is_fill_only: bool, fill_is_2d_solid: bool, fill_is_3d: bool, + plot_visible: bool, + hide_unselected: bool, local_depth: Option, ) -> StyleKey { StyleKey { @@ -1603,6 +1625,8 @@ fn style_key( is_fill_only, fill_is_2d_solid, fill_is_3d, + plot_visible, + hide_unselected, depth_bits: local_depth.map_or(0, f32::to_bits), } } @@ -2014,6 +2038,8 @@ fn emit_wire( lw.is_fill_only, lw.fill_is_2d_solid, lw.fill_is_3d, + lw.plot_visible, + lw.hide_unselected, local_depth, ); @@ -2041,6 +2067,8 @@ fn emit_wire( lw.is_fill_only, lw.fill_is_2d_solid, lw.fill_is_3d, + lw.plot_visible, + lw.hide_unselected, ) }); entry.local_depth = local_depth; diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index 2d9e42c6..71a60a6a 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -711,6 +711,8 @@ pub(crate) fn tessellate_entity( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -1063,6 +1065,8 @@ pub(crate) fn tessellate_entity( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -1148,14 +1152,21 @@ pub(crate) fn tessellate_entity( wire.render_instance = Some(instance); } } - if document.header.xclip_frame != 0 && polygon.len() >= 3 { - wires.push(pick::xclip::frame_wire( + let frame_mode = crate::scene::frame::mode( + document, + crate::scene::frame::FrameKind::Xclip, + ); + if polygon.len() >= 3 { + let mut frame = pick::xclip::frame_wire( &polygon, - format!("{}_xclipframe", h.value()), + h.value().to_string(), ins_color, sel, ins_lw_px, - )); + ); + frame.display_visible = frame_mode != 0; + frame.plot_visible = frame_mode == 1; + wires.push(frame); } } @@ -1200,6 +1211,8 @@ pub(crate) fn tessellate_entity( bg_color, false, ); + let frame_mode = crate::scene::frame::entity_kind(e) + .map(|kind| crate::scene::frame::mode(document, kind)); for b in &mut bases { b.aci = aci; // SDF text wires carry a glyph-bounds AABB (the true text extent) set @@ -1208,8 +1221,18 @@ pub(crate) fn tessellate_entity( if b.text_verts.is_empty() { set_wire_aabb(b, aabb); } + if let Some(mode) = frame_mode { + b.display_visible = mode != 0; + b.plot_visible = mode == 1; + } + if matches!(e, EntityType::Wipeout(_)) { + b.depth_override = Some(0.5); + } } + // A hidden mask frame remains selectable and appears while selected, but + // contributes no visible line work during normal display. The interior + // pick triangles remain intact. // Complex linetypes (with embedded shapes / text) expand the *base* // polyline along its tangent. Text-type entities never have a complex // linetype assigned, so we only consult the first wire here — multi-wire @@ -1437,6 +1460,8 @@ fn lod_stub_wire( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -1528,6 +1553,8 @@ fn lod_stub_wire_3d( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 64f8dd8a..619dd248 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -127,6 +127,75 @@ pub(crate) fn points_to_ds( (high, low) } +fn point_cloud_wires( + document: &CadDocument, + handle: Handle, + entity: &EntityType, + selected: bool, + color: [f32; 4], + line_weight_px: f32, +) -> Option> { + let EntityType::Extended(extended) = entity else { + return None; + }; + let frame_points = crate::entities::extended::point_cloud_frame_lines(extended)?; + let rendered = convert(entity, document)?; + let RenderObject::Lines(body_points) = rendered.object else { + return None; + }; + let (points, points_low) = points_to_ds(body_points); + let mut wires = vec![WireModel { + taper_widths: Vec::new(), + world_width: 0.0, + depth_override: None, + display_visible: true, + plot_visible: true, + fill_is_3d: false, + fill_is_2d_solid: false, + render_instance: None, + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), + dash_from_start: false, + dash_align_end: None, + text_verts: Vec::new(), + name: handle.value().to_string(), + points, + points_low, + color, + selected, + pattern_length: 0.0, + pattern: [0.0; 8], + line_weight_px, + snap_pts: rendered.snap_pts, + tangent_geoms: rendered.tangent_geoms, + aci: 0, + key_vertices: rendered.key_vertices, + aabb: WireModel::UNBOUNDED_AABB, + plinegen: true, + fill_tris: Vec::new(), + fill_tris_low: Vec::new(), + }]; + let mode = crate::scene::frame::mode( + document, + crate::scene::frame::FrameKind::PointCloudClip, + ); + if !frame_points.is_empty() { + let (points, points_low) = points_to_ds(frame_points); + let mut frame = WireModel::solid( + handle.value().to_string(), + points, + color, + selected, + ); + frame.points_low = points_low; + frame.line_weight_px = line_weight_px; + frame.display_visible = mode != 0; + frame.plot_visible = mode == 1; + wires.push(frame); + } + Some(wires) +} + /// Lift a WireModel built in a local frame (a fixed f64 origin subtracted) back /// to absolute world coordinates, re-splitting every position — polyline points /// and SDF glyph vertices — into double-single so it stays precise at UTM scale. @@ -434,6 +503,8 @@ pub fn tessellate( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -482,6 +553,17 @@ pub fn tessellate( return out; } + if let Some(wires) = point_cloud_wires( + document, + handle, + entity, + selected, + color, + line_weight_px, + ) { + return wires; + } + // ── Try the kernel path first ─────────────────────────────────────────── // Relative-PDSIZE points size their glyph from the current zoom so they // stay a roughly constant on-screen size; otherwise the header-driven path. @@ -728,6 +810,8 @@ pub fn tessellate( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -775,6 +859,8 @@ pub fn tessellate( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -824,6 +910,8 @@ pub fn tessellate( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -854,6 +942,8 @@ pub fn tessellate( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -914,6 +1004,8 @@ pub fn tessellate( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -956,6 +1048,8 @@ pub fn tessellate( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -995,6 +1089,8 @@ pub fn tessellate( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -1027,6 +1123,8 @@ pub fn tessellate( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -1093,6 +1191,8 @@ pub fn tessellate( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -1214,6 +1314,8 @@ pub fn tessellate( taper_widths: Vec::new(), world_width: polyline_band_width(entity), depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d, fill_is_2d_solid: false, render_instance: None, @@ -1279,6 +1381,8 @@ pub fn tessellate( fill_is_2d_solid: matches!(entity, EntityType::Solid(_)), render_instance: None, depth_override: None, + display_visible: true, + plot_visible: true, }); } @@ -1287,6 +1391,8 @@ pub fn tessellate( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -1332,6 +1438,8 @@ pub fn tessellate( taper_widths: Vec::new(), world_width: polyline_band_width(entity), depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -1378,6 +1486,8 @@ pub fn tessellate( taper_widths: widths, world_width, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -1497,6 +1607,8 @@ pub fn tessellate( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 62f7f2c9..fd82c8cc 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -1393,6 +1393,9 @@ impl Scene { wipeout.v_vector = context.transform.apply_rotation(source.v_vector); } + let Some(mut fill_plane) = Self::wipeout_fill_plane(&wipeout) else { + return; + }; let (world_origin, mut boundary) = Self::wipeout_boundary_2d(&wipeout); for clip in &context.clips { @@ -1409,6 +1412,16 @@ impl Scene { if boundary.len() < 3 { return; } + if !context.clips.is_empty() { + let Some(local) = Self::wipeout_boundary_at_xy( + fill_plane.0, + world_origin, + &boundary, + ) else { + return; + }; + fill_plane.1 = local; + } let selection_handle = if tint_insert_selection && context.is_instanced() { context.root_handle @@ -1446,10 +1459,14 @@ impl Scene { } else { None }; + let fill_plane_boundary = Some(Arc::new(fill_plane.1)); + let fill_plane = Some(fill_plane.0); models.push(HatchModel { render_instance, boundary: Arc::new(boundary), boundary_wcs: None, + fill_plane, + fill_plane_boundary, boundary_exterior: None, boundary_sources: None, pattern: model::hatch_model::HatchPattern::Solid, @@ -1474,66 +1491,113 @@ impl Scene { pub(super) fn wipeout_boundary_2d( wo: &acadrust::entities::Wipeout, ) -> ([f64; 2], Vec<[f32; 2]>) { - use acadrust::entities::WipeoutClipType; - let origin = [wo.insertion_point.x, wo.insertion_point.y]; - - let is_polygon = wo.clipping_enabled - && wo.clip_boundary_vertices.len() >= 3 - && matches!(wo.clip_type, WipeoutClipType::Polygonal); - - if is_polygon { - // DXF clip vertices live in image-pixel space, centred on the - // image (range −size/2 … +size/2). Image-bottom-left → insertion, - // image-y-axis points DOWN (per the DXF "v_vector points down the - // image" convention), so map: - // x_off = (clip.x + size.x/2) × u_vec - // y_off = (size.y/2 − clip.y) × v_vec ← y flipped - // Offsets are relative to `origin` (the insertion point). - let cx_of = |v: &acadrust::types::Vector2| v.x + wo.size.x * 0.5; - let cy_of = |v: &acadrust::types::Vector2| wo.size.y * 0.5 - v.y; - let mut poly: Vec<[f32; 2]> = wo - .clip_boundary_vertices - .iter() - .map(|v| { - let cx = cx_of(v); - let cy = cy_of(v); - let wx = (wo.u_vector.x * cx + wo.v_vector.x * cy) as f32; - let wy = (wo.u_vector.y * cx + wo.v_vector.y * cy) as f32; - [wx, wy] - }) - .collect(); - // Close the loop: the GPU `in_polygon` ray-cast walks - // sequential pairs and doesn't wrap, so without an explicit - // closing vertex the last edge (vN-1 → v0) is never tested and - // the fill bleeds far past the boundary. - if let Some(&first) = poly.first() { - if poly.last() != Some(&first) { - poly.push(first); + let plane = cadkernel::space::Plane::from_axes( + [wo.insertion_point.x, wo.insertion_point.y, wo.insertion_point.z], + [wo.u_vector.x, wo.u_vector.y, wo.u_vector.z], + [wo.v_vector.x, wo.v_vector.y, wo.v_vector.z], + ); + let boundary = Self::wipeout_local_boundary(wo) + .into_iter() + .map(|point| { + if point[0].is_finite() && point[1].is_finite() { + let offset = plane.vector_at(point); + [offset[0] as f32, offset[1] as f32] + } else { + [f32::NAN; 2] } - } - (origin, poly) - } else { - // Rectangular boundary from 4 corners, as offsets from `origin`. - let ux = (wo.u_vector.x * wo.size.x) as f32; - let uy = (wo.u_vector.y * wo.size.x) as f32; - let vx = (wo.v_vector.x * wo.size.y) as f32; - let vy = (wo.v_vector.y * wo.size.y) as f32; - // Close the loop (repeat corner 0): the GPU `in_polygon` ray-cast - // walks sequential vertex pairs and never wraps last→first, so an - // unclosed quad leaves the v3→v0 edge untested and the solid mask - // bleeds past the boundary — same reason the polygon branch closes. - ( - origin, - vec![ - [0.0, 0.0], - [ux, uy], - [ux + vx, uy + vy], - [vx, vy], - [0.0, 0.0], - ], - ) + }) + .collect(); + (origin, boundary) + } + + fn wipeout_local_boundary(wo: &acadrust::entities::Wipeout) -> Vec<[f64; 2]> { + use acadrust::entities::{WipeoutClipMode, WipeoutClipType}; + let mut outer = vec![ + [0.0, 0.0], + [wo.size.x, 0.0], + [wo.size.x, wo.size.y], + [0.0, wo.size.y], + [0.0, 0.0], + ]; + if !wo.clipping_enabled + || wo.clip_boundary_vertices.len() < 3 + || !matches!(wo.clip_type, WipeoutClipType::Polygonal) + { + return outer; } + let mut clip: Vec<[f64; 2]> = wo + .clip_boundary_vertices + .iter() + .map(|point| { + [ + point.x + wo.size.x * 0.5, + wo.size.y * 0.5 - point.y, + ] + }) + .collect(); + if clip.last() != clip.first() { + clip.push(clip[0]); + } + if matches!(wo.clip_mode, WipeoutClipMode::Inside) { + outer.push([f64::NAN; 2]); + outer.extend(clip); + outer + } else { + clip + } + } + + fn wipeout_fill_plane( + wipeout: &acadrust::entities::Wipeout, + ) -> Option<(model::hatch_model::FillPlane, Vec<[f32; 2]>)> { + let origin = [ + wipeout.insertion_point.x, + wipeout.insertion_point.y, + wipeout.insertion_point.z, + ]; + let x_axis = [wipeout.u_vector.x, wipeout.u_vector.y, wipeout.u_vector.z]; + let y_axis = [wipeout.v_vector.x, wipeout.v_vector.y, wipeout.v_vector.z]; + cadkernel::space::Plane::from_axes(origin, x_axis, y_axis).normal()?; + let boundary = Self::wipeout_local_boundary(wipeout) + .into_iter() + .map(|point| [point[0] as f32, point[1] as f32]) + .collect(); + Some(( + model::hatch_model::FillPlane { + origin, + x_axis, + y_axis, + }, + boundary, + )) + } + + fn wipeout_boundary_at_xy( + plane: model::hatch_model::FillPlane, + world_origin: [f64; 2], + boundary: &[[f32; 2]], + ) -> Option> { + let plane = cadkernel::space::Plane::from_axes( + plane.origin, + plane.x_axis, + plane.y_axis, + ); + boundary + .iter() + .map(|point| { + if point[0].is_finite() && point[1].is_finite() { + plane + .coordinates_at_xy([ + world_origin[0] + point[0] as f64, + world_origin[1] + point[1] as f64, + ]) + .map(|point| [point[0] as f32, point[1] as f32]) + } else { + Some([f32::NAN; 2]) + } + }) + .collect() } pub(crate) fn hatch_model_from_dxf( @@ -1817,6 +1881,8 @@ impl Scene { render_instance: None, boundary: std::sync::Arc::new(boundary_f32), boundary_wcs: None, + fill_plane: None, + fill_plane_boundary: None, boundary_exterior: Some(std::sync::Arc::new(boundary_exterior)), boundary_sources: Some(std::sync::Arc::new(boundary_sources)), pattern, @@ -2133,6 +2199,8 @@ impl Scene { render_instance: None, boundary: std::sync::Arc::new(boundary), boundary_wcs: None, + fill_plane: None, + fill_plane_boundary: None, boundary_exterior: None, boundary_sources: None, pattern: model::hatch_model::HatchPattern::Solid, diff --git a/src/scene/frame.rs b/src/scene/frame.rs new file mode 100644 index 00000000..1b14000b --- /dev/null +++ b/src/scene/frame.rs @@ -0,0 +1,211 @@ +use acadrust::objects::{ObjectType, RasterVariables, WipeoutVariables}; +use acadrust::{CadDocument, EntityType, Handle}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum FrameKind { + Image, + Pdf, + Wipeout, + Xclip, + PointCloudClip, +} + +pub(crate) const ALL_KINDS: [FrameKind; 5] = [ + FrameKind::Image, + FrameKind::Pdf, + FrameKind::Wipeout, + FrameKind::Xclip, + FrameKind::PointCloudClip, +]; + +fn root_entry(document: &CadDocument, name: &str) -> Option { + let from_dictionary = |handle| match document.objects.get(&handle) { + Some(ObjectType::Dictionary(dictionary)) => dictionary + .entries + .iter() + .find(|(key, _)| key.eq_ignore_ascii_case(name)) + .map(|(_, handle)| *handle), + _ => None, + }; + from_dictionary(document.header.named_objects_dict_handle).or_else(|| { + document.objects.values().find_map(|object| match object { + ObjectType::Dictionary(dictionary) if dictionary.owner.is_null() => { + from_dictionary(dictionary.handle) + } + _ => None, + }) + }) +} + +fn raster_variables_handle(document: &CadDocument) -> Option { + root_entry(document, "ACAD_IMAGE_VARS") + .filter(|handle| matches!(document.objects.get(handle), Some(ObjectType::RasterVariables(_)))) +} + +fn wipeout_variables_handle(document: &CadDocument) -> Option { + root_entry(document, "ACAD_WIPEOUT_VARS") + .filter(|handle| matches!(document.objects.get(handle), Some(ObjectType::WipeoutVariables(_)))) +} + +fn drawing_mode(document: &CadDocument, name: &str, default: i16) -> i16 { + crate::io::drawing_variable(document, name) + .and_then(|value| value.parse::().ok()) + .unwrap_or(default) + .clamp(0, 2) +} + +pub(crate) fn mode(document: &CadDocument, kind: FrameKind) -> i16 { + match kind { + FrameKind::Image => raster_variables_handle(document) + .and_then(|handle| match document.objects.get(&handle) { + Some(ObjectType::RasterVariables(value)) => Some(value.display_image_frame), + _ => None, + }) + .unwrap_or(1) + .clamp(0, 2), + FrameKind::Pdf => drawing_mode(document, "PDFFRAME", 1), + FrameKind::Wipeout => crate::io::drawing_variable(document, "WIPEOUTFRAME") + .and_then(|value| value.parse::().ok()) + .or_else(|| { + wipeout_variables_handle(document).and_then(|handle| { + match document.objects.get(&handle) { + Some(ObjectType::WipeoutVariables(value)) => Some(value.display_frame), + _ => None, + } + }) + }) + .unwrap_or(1) + .clamp(0, 2), + FrameKind::Xclip => document.header.xclip_frame.clamp(0, 2), + FrameKind::PointCloudClip => drawing_mode(document, "POINTCLOUDCLIPFRAME", 2), + } +} + +pub(crate) fn master_mode(document: &CadDocument) -> i16 { + let first = mode(document, ALL_KINDS[0]); + ALL_KINDS + .iter() + .skip(1) + .all(|kind| mode(document, *kind) == first) + .then_some(first) + .unwrap_or(3) +} + +fn attach_root_entry(document: &mut CadDocument, name: &str, handle: Handle) { + let root = crate::scene::annotative::root_named_dict_handle(document); + if let Some(ObjectType::Dictionary(dictionary)) = document.objects.get_mut(&root) { + dictionary + .entries + .retain(|(key, _)| !key.eq_ignore_ascii_case(name)); + dictionary.add_entry(name, handle); + } +} + +fn set_image_mode(document: &mut CadDocument, value: i16) { + let owner = crate::scene::annotative::root_named_dict_handle(document); + let handle = raster_variables_handle(document).unwrap_or_else(|| { + let handle = document.allocate_handle(); + let mut variables = RasterVariables::new(); + variables.handle = handle; + variables.owner = owner; + document + .objects + .insert(handle, ObjectType::RasterVariables(variables)); + handle + }); + if let Some(ObjectType::RasterVariables(variables)) = document.objects.get_mut(&handle) { + variables.owner = owner; + variables.display_image_frame = value; + } + attach_root_entry(document, "ACAD_IMAGE_VARS", handle); +} + +fn set_wipeout_mode(document: &mut CadDocument, value: i16) { + crate::io::set_drawing_variable(document, "WIPEOUTFRAME", &value.to_string()); + let owner = crate::scene::annotative::root_named_dict_handle(document); + let handle = wipeout_variables_handle(document).unwrap_or_else(|| { + let handle = document.allocate_handle(); + let mut variables = WipeoutVariables::new(); + variables.handle = handle; + variables.owner = owner; + document + .objects + .insert(handle, ObjectType::WipeoutVariables(variables)); + handle + }); + if let Some(ObjectType::WipeoutVariables(variables)) = document.objects.get_mut(&handle) { + variables.owner = owner; + variables.display_frame = value; + } + attach_root_entry(document, "ACAD_WIPEOUT_VARS", handle); +} + +pub(crate) fn set_mode(document: &mut CadDocument, kind: FrameKind, value: i16) { + let value = value.clamp(0, 2); + match kind { + FrameKind::Image => set_image_mode(document, value), + FrameKind::Pdf => crate::io::set_drawing_variable(document, "PDFFRAME", &value.to_string()), + FrameKind::Wipeout => set_wipeout_mode(document, value), + FrameKind::Xclip => document.header.xclip_frame = value, + FrameKind::PointCloudClip => crate::io::set_drawing_variable( + document, + "POINTCLOUDCLIPFRAME", + &value.to_string(), + ), + } +} + +pub(crate) fn set_master_mode(document: &mut CadDocument, value: i16) { + let value = value.clamp(0, 2); + for kind in ALL_KINDS { + if mode(document, kind) != value { + set_mode(document, kind, value); + } + } +} + +pub(crate) fn kind_for_name(name: &str) -> Option { + match name { + "IMAGEFRAME" => Some(FrameKind::Image), + "PDFFRAME" => Some(FrameKind::Pdf), + "WIPEOUTFRAME" => Some(FrameKind::Wipeout), + "XCLIPFRAME" => Some(FrameKind::Xclip), + "POINTCLOUDCLIPFRAME" => Some(FrameKind::PointCloudClip), + _ => None, + } +} + +pub(crate) fn entity_kind(entity: &EntityType) -> Option { + match entity { + EntityType::RasterImage(_) => Some(FrameKind::Image), + EntityType::Underlay(underlay) + if matches!(underlay.underlay_type, acadrust::entities::UnderlayType::Pdf) => + { + Some(FrameKind::Pdf) + } + EntityType::Wipeout(_) => Some(FrameKind::Wipeout), + _ => None, + } +} + +pub(crate) fn affected(entity: &EntityType, kind: FrameKind) -> bool { + match kind { + FrameKind::Image => matches!(entity, EntityType::RasterImage(_)), + FrameKind::Pdf => matches!( + entity, + EntityType::Underlay(underlay) + if matches!(underlay.underlay_type, acadrust::entities::UnderlayType::Pdf) + ), + FrameKind::Wipeout => matches!(entity, EntityType::Wipeout(_)), + FrameKind::Xclip => matches!(entity, EntityType::Insert(_)), + FrameKind::PointCloudClip => matches!( + entity, + EntityType::Extended(extended) + if matches!( + &extended.data, + acadrust::entities::ExtendedEntityData::PointCloud(_) + | acadrust::entities::ExtendedEntityData::PointCloudEx(_) + ) + ), + } +} diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 4ec529ff..04548003 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -9,6 +9,7 @@ pub mod annotative; pub mod cache; pub mod convert; pub mod creation_style; +pub(crate) mod frame; pub mod model; pub mod pick; pub mod pipeline; @@ -3210,6 +3211,8 @@ impl Scene { world_origin: [0.0, 0.0], boundary: Arc::new(vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]), boundary_wcs: None, + fill_plane: None, + fill_plane_boundary: None, boundary_exterior: None, boundary_sources: None, pattern: crate::scene::model::hatch_model::HatchPattern::Solid, diff --git a/src/scene/model/hatch_model.rs b/src/scene/model/hatch_model.rs index 2a0792f4..a36f16af 100644 --- a/src/scene/model/hatch_model.rs +++ b/src/scene/model/hatch_model.rs @@ -165,6 +165,13 @@ pub enum HatchPattern { /// (anchor-relative), so real coordinates never approach it. pub const GPU_BOUNDARY_SEP: f32 = 1.0e30; +#[derive(Clone, Copy, Debug)] +pub struct FillPlane { + pub origin: [f64; 3], + pub x_axis: [f64; 3], + pub y_axis: [f64; 3], +} + /// A hatched region defined by a closed polygon boundary. #[derive(Clone, Debug)] pub struct HatchModel { @@ -196,6 +203,9 @@ pub struct HatchModel { /// rebuilt from a DXF entity — `add_hatch` then reconstructs the persisted /// vertices from `boundary` + `world_origin` instead. pub boundary_wcs: Option>>, + /// Optional 3-D placement used by wipeout fills. + pub fill_plane: Option, + pub fill_plane_boundary: Option>>, /// Per-ring DXF role, aligned with the NaN-separated boundary paths. pub boundary_exterior: Option>>, /// Source entity handles for each boundary ring. diff --git a/src/scene/model/wire_model.rs b/src/scene/model/wire_model.rs index af7d1bbe..0133640a 100644 --- a/src/scene/model/wire_model.rs +++ b/src/scene/model/wire_model.rs @@ -146,6 +146,10 @@ pub struct WireModel { /// mirroring how exploded block fills seed their depth, so bands and fills /// from the same block interleave by the block's internal draw order. pub depth_override: Option, + /// Whether this wire is drawn in the normal viewport pass. + pub display_visible: bool, + /// Whether this wire is included in plotted output. + pub plot_visible: bool, /// `true` when [`fill_tris`] is a real 3-D surface (PolyfaceMesh / /// PolygonMesh face) that must render with hidden-surface depth and only in /// shaded modes. `false` for a flat 2-D overlay fill (SOLID arrowhead, @@ -208,6 +212,8 @@ impl WireModel { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -502,6 +508,8 @@ impl Default for WireModel { fill_tris: Vec::new(), fill_tris_low: Vec::new(), depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/scene/pick/xclip.rs b/src/scene/pick/xclip.rs index 7ff56d10..da47cf08 100644 --- a/src/scene/pick/xclip.rs +++ b/src/scene/pick/xclip.rs @@ -260,6 +260,8 @@ pub fn frame_wire( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index 1a9cd2f6..a5954b35 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -948,10 +948,8 @@ impl Pipeline { ); // ── Hatch pipeline ───────────────────────────────────────────────── - // binding 0 (HatchUniforms) is read by the vertex shader too — it - // pulls `origin` to undo the CPU-side hatch-local pre-shift when - // computing clip position. bindings 1 (Boundary) and 2 - // (FamilyBatch) stay fragment-only. + // Binding 0 carries the fill plane; boundary and family data are + // fragment-only. let hatch_entry = |binding: u32, vis: wgpu::ShaderStages| wgpu::BindGroupLayoutEntry { binding, visibility: vis, @@ -2364,6 +2362,10 @@ impl Pipeline { let mut block_wires: Vec<&WireModel> = Vec::new(); let mut i = 0; while i < wires.len() { + if !wires[i].display_visible { + i += 1; + continue; + } if wires[i].render_instance.is_some() { block_wires.push(&wires[i]); i += 1; @@ -2372,6 +2374,7 @@ impl Pipeline { let mesh_edge = is_mesh_edge(&wires[i]); let mut j = i + 1; while j < wires.len() + && wires[j].display_visible && wires[j].render_instance.is_none() && is_mesh_edge(&wires[j]) == mesh_edge { diff --git a/src/scene/pipeline/text_gpu.rs b/src/scene/pipeline/text_gpu.rs index 0eb08073..8bfc3bee 100644 --- a/src/scene/pipeline/text_gpu.rs +++ b/src/scene/pipeline/text_gpu.rs @@ -433,6 +433,9 @@ pub fn upload_block_vertex_refs( let mut slots = rustc_hash::FxHashMap::default(); let mut groups: Vec> = Vec::new(); for &wire in wires { + if tint.is_none() && !wire.display_visible { + continue; + } let Some(instance) = wire.render_instance else { continue; }; diff --git a/src/scene/pipeline/wipeout_gpu.rs b/src/scene/pipeline/wipeout_gpu.rs index 720d7e62..02b15f22 100644 --- a/src/scene/pipeline/wipeout_gpu.rs +++ b/src/scene/pipeline/wipeout_gpu.rs @@ -9,11 +9,13 @@ // a limit on visible hatch fills. // // Group 1 bindings per fill: -// binding 0 — HatchUniformData (64 bytes) mode, color, angle_offset, scale, gradient params +// binding 0 — HatchUniformData fill and plane parameters // binding 1 — BoundaryData (16384 bytes) boundary polygon vertices // binding 2 — FamilyBatchData (1296 bytes) up to 16 line families + 128 dash values (unused for wipeouts) -use crate::scene::model::hatch_model::{HatchModel, HatchPattern, PatFamily, MAX_HATCH_BOUNDARY_VERTS}; +use crate::scene::model::hatch_model::{ + FillPlane, HatchModel, HatchPattern, PatFamily, MAX_HATCH_BOUNDARY_VERTS, +}; use iced::wgpu; use iced::wgpu::util::DeviceExt; @@ -48,10 +50,10 @@ impl HatchVertex { #[repr(C)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] pub struct WipeoutPlacement { - pub translation: [f32; 2], - pub translation_low: [f32; 2], + pub translation: [f32; 3], + pub translation_low: [f32; 3], pub draw_depth: f32, - pub _pad: [f32; 3], + pub _pad: f32, } impl WipeoutPlacement { @@ -60,9 +62,9 @@ impl WipeoutPlacement { array_stride: std::mem::size_of::() as u64, step_mode: wgpu::VertexStepMode::Instance, attributes: &[ - wgpu::VertexAttribute { offset: 0, shader_location: 1, format: wgpu::VertexFormat::Float32x2 }, - wgpu::VertexAttribute { offset: 8, shader_location: 2, format: wgpu::VertexFormat::Float32x2 }, - wgpu::VertexAttribute { offset: 16, shader_location: 3, format: wgpu::VertexFormat::Float32 }, + wgpu::VertexAttribute { offset: 0, shader_location: 1, format: wgpu::VertexFormat::Float32x3 }, + wgpu::VertexAttribute { offset: 12, shader_location: 2, format: wgpu::VertexFormat::Float32x3 }, + wgpu::VertexAttribute { offset: 24, shader_location: 3, format: wgpu::VertexFormat::Float32 }, ], } } @@ -70,7 +72,7 @@ impl WipeoutPlacement { // ── Uniform structs ─────────────────────────────────────────────────────── -/// Per-hatch parameters (binding 0). Must be 96 bytes (16-byte aligned). +/// Per-fill parameters (binding 0). /// /// `mode` encoding: /// 0 → Pattern (families in FamilyBatchData) @@ -89,18 +91,15 @@ pub struct HatchUniformData { pub grad_sin: f32, // 52: gradient direction sin pub grad_min: f32, // 56: gradient proj_min pub grad_range: f32, // 60: gradient proj_range - pub origin: [f32; 2], // 64: hatch-local origin (boundary AABB centre), - // // high half of the double-single anchor. - // // The pattern fragment shader subtracts this - // // from xz before the perp/perp_step quotient - // // so f32 doesn't catastrophically cancel when - // // world coords are large and pattern spacing - // // is small (e.g. UTM drawing + sub-mm hatch). - pub origin_low: [f32; 2], // 72: anchor low residual (double-single) so the - // // vertex clip position stays precise at UTM scale. + pub origin: [f32; 2], // 64: reserved pattern origin + pub origin_low: [f32; 2], // 72: reserved low residual pub draw_depth: f32, // 80: signed (-1,1) draw-order bias pub _pad: [f32; 3], // 84..96 -} // total 96 bytes + pub plane_origin_high: [f32; 4], + pub plane_origin_low: [f32; 4], + pub plane_x: [f32; 4], + pub plane_y: [f32; 4], +} /// Boundary polygon (binding 1). Matches WGSL `Boundary`. #[repr(C)] @@ -194,13 +193,22 @@ impl WipeoutGpu { }; // ── Bounding box ───────────────────────────────────────────────── + let plane = model.fill_plane.unwrap_or(FillPlane { + origin: [model.world_origin[0], model.world_origin[1], 0.0], + x_axis: [1.0, 0.0, 0.0], + y_axis: [0.0, 1.0, 0.0], + }); + let gpu_boundary = model + .fill_plane_boundary + .as_deref() + .unwrap_or(model.boundary.as_ref()); let (mut min_x, mut max_x, mut min_y, mut max_y) = ( f32::INFINITY, f32::NEG_INFINITY, f32::INFINITY, f32::NEG_INFINITY, ); - for &[x, y] in model.boundary.iter() { + for &[x, y] in gpu_boundary { if !x.is_finite() || !y.is_finite() { continue; } @@ -218,68 +226,16 @@ impl WipeoutGpu { _ => 5.0, }; let diag = ((max_x - min_x).powi(2) + (max_y - min_y).powi(2)).sqrt(); - let pad = (diag * 0.8 + max_spacing * 2.0 * model.scale).max(1.0); - // ── Pattern phase anchor. - // - // Boundary verts are already stored as small f32 offsets from - // `model.world_origin` (an f64 anchor near the AABB centre), so - // the in_polygon ray-cast and the pattern math both run at full - // f32 precision in the fragment shader. The remaining question - // is *which* offset-rel point we hand to the vertex shader as - // `h.origin` (so the quad lands at the correct WCS position - // after `view_proj * (v.pos + h.origin)`). - // - // For pattern alignment across adjacent hatches with the same - // family, snap `h.origin` to the family's perp/along grid in - // f64. Without the snap, each hatch's pattern phase is per-hatch - // and stripes don't continue across hatch boundaries (you see - // pattern misalignment along a road etc). - let mut origin = model.world_origin; - if let HatchPattern::Pattern(families) = &model.pattern { - if let Some(fam) = families.first() { - // Must mirror the shader's pattern math exactly. QCAD PAT - // convention (matching build_family_batch): `dy` is the - // perpendicular spacing and `dx` is the along-line phase - // shift, both in family-local space. Multiplied by - // `model.scale` in the fragment shader. Combined angle - // (family + pattern_angle) rotates the perp/along axes. - let angle = (fam.angle_deg.to_radians() + model.angle_offset) as f64; - let cos_a = angle.cos(); - let sin_a = angle.sin(); - let scale = model.scale as f64; - let perp_step = fam.dy as f64 * scale; - let along_step = fam.dx as f64 * scale; - let o_perp = -origin[0] * sin_a + origin[1] * cos_a; - let o_along = origin[0] * cos_a + origin[1] * sin_a; - let snapped_perp = if perp_step.abs() > 1e-12 { - (o_perp / perp_step).round() * perp_step - } else { - o_perp - }; - let snapped_along = if along_step.abs() > 1e-12 { - (o_along / along_step).round() * along_step - } else { - o_along - }; - origin = [ - -snapped_perp * sin_a + snapped_along * cos_a, - snapped_perp * cos_a + snapped_along * sin_a, - ]; - } - } - // Boundary buffer + quad now live in "snap-shifted" hatch-local - // coords: subtract any drift between model.world_origin and the - // snapped origin so the in_polygon ray-cast in fragment shader - // (which reads the boundary verts straight) lines up with v.xz. - let drift = [ - (model.world_origin[0] - origin[0]) as f32, - (model.world_origin[1] - origin[1]) as f32, - ]; + let pad = if mode == 1 { + 0.0 + } else { + (diag * 0.8 + max_spacing * 2.0 * model.scale).max(1.0) + }; let (x0, x1, y0, y1) = ( - min_x + drift[0] - pad, - max_x + drift[0] + pad, - min_y + drift[1] - pad, - max_y + drift[1] + pad, + min_x - pad, + max_x + pad, + min_y - pad, + max_y + pad, ); let quad = [ @@ -316,11 +272,9 @@ impl WipeoutGpu { // ── Gradient: projection range (in snapped-local space) ────────── let (grad_min, grad_range) = if mode == 2 { - let projs: Vec = model - .boundary - .iter() + let projs: Vec = gpu_boundary.iter() .filter(|v| v[0].is_finite() && v[1].is_finite()) - .map(|&[x, y]| (x + drift[0]) * grad_cos + (y + drift[1]) * grad_sin) + .map(|&[x, y]| x * grad_cos + y * grad_sin) .collect(); if projs.is_empty() { (0.0, 1.0) @@ -334,7 +288,11 @@ impl WipeoutGpu { }; // ── HatchUniformData ───────────────────────────────────────────── - let n = model.boundary.len().min(MAX_HATCH_BOUNDARY_VERTS); + let n = gpu_boundary.len().min(MAX_HATCH_BOUNDARY_VERTS); + let plane_origin_high = plane.origin.map(|value| value as f32); + let plane_origin_low: [f32; 3] = std::array::from_fn(|index| { + (plane.origin[index] - plane_origin_high[index] as f64) as f32 + }); let uniform_data = HatchUniformData { color: model.color, color2, @@ -346,30 +304,46 @@ impl WipeoutGpu { grad_sin, grad_min, grad_range, - origin: [origin[0] as f32, origin[1] as f32], - origin_low: [ - (origin[0] - origin[0] as f32 as f64) as f32, - (origin[1] - origin[1] as f32 as f64) as f32, - ], + origin: [0.0; 2], + origin_low: [0.0; 2], draw_depth: 0.0, _pad: [0.0; 3], + plane_origin_high: [ + plane_origin_high[0], + plane_origin_high[1], + plane_origin_high[2], + 0.0, + ], + plane_origin_low: [ + plane_origin_low[0], + plane_origin_low[1], + plane_origin_low[2], + 0.0, + ], + plane_x: [ + plane.x_axis[0] as f32, + plane.x_axis[1] as f32, + plane.x_axis[2] as f32, + 0.0, + ], + plane_y: [ + plane.y_axis[0] as f32, + plane.y_axis[1] as f32, + plane.y_axis[2] as f32, + 0.0, + ], }; // ── BoundaryData (in snapped-local space) ──────────────────────── let mut boundary_data = BoundaryData { verts: [[0.0; 4]; MAX_HATCH_BOUNDARY_VERTS], }; - for (i, &[x, y]) in model - .boundary - .iter() + for (i, &[x, y]) in gpu_boundary.iter() .take(MAX_HATCH_BOUNDARY_VERTS) .enumerate() { - // Path-break separators go up as the finite GPU sentinel, not - // NaN — the shader NaN self-compare is folded to `true` on some - // drivers (#386, #416) — and must not be shifted by `drift`. if x.is_finite() && y.is_finite() { - boundary_data.verts[i] = [x + drift[0], y + drift[1], 0.0, 0.0]; + boundary_data.verts[i] = [x, y, 0.0, 0.0]; } else { let s = crate::scene::model::hatch_model::GPU_BOUNDARY_SEP; boundary_data.verts[i] = [s, s, 0.0, 0.0]; @@ -433,16 +407,15 @@ impl WipeoutGpu { let translation = model .render_instance .map_or([0.0; 3], |instance| instance.translation); - let delta = [translation[0] - base[0], translation[1] - base[1]]; - let high = [delta[0] as f32, delta[1] as f32]; + let delta = std::array::from_fn(|index| translation[index] - base[index]); + let high = delta.map(|value| value as f32); WipeoutPlacement { translation: high, - translation_low: [ - (delta[0] - high[0] as f64) as f32, - (delta[1] - high[1] as f64) as f32, - ], + translation_low: std::array::from_fn(|index| { + (delta[index] - high[index] as f64) as f32 + }), draw_depth: model.draw_depth, - _pad: [0.0; 3], + _pad: 0.0, } }) .collect(); @@ -451,19 +424,38 @@ impl WipeoutGpu { contents: bytemuck::cast_slice(&placements), usage: wgpu::BufferUsages::VERTEX, }); - let ox = model.world_origin[0] as f32; - let oy = model.world_origin[1] as f32; - let world_aabb = if min_x.is_finite() && min_y.is_finite() { + let mut world_bounds = [ + f32::INFINITY, + f32::INFINITY, + f32::NEG_INFINITY, + f32::NEG_INFINITY, + ]; + for &[x, y] in model.boundary.iter() { + if x.is_finite() && y.is_finite() { + world_bounds[0] = world_bounds[0].min(x); + world_bounds[1] = world_bounds[1].min(y); + world_bounds[2] = world_bounds[2].max(x); + world_bounds[3] = world_bounds[3].max(y); + } + } + let tilted = plane.origin[2].abs() > f64::EPSILON + || plane.x_axis[2].abs() > f64::EPSILON + || plane.y_axis[2].abs() > f64::EPSILON; + let world_aabb = if tilted { + [f32::NAN; 4] + } else if world_bounds[0].is_finite() && world_bounds[1].is_finite() { + let ox = model.world_origin[0] as f32; + let oy = model.world_origin[1] as f32; let mut aabb = [f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY]; for placement in &placements { - aabb[0] = aabb[0].min(min_x + ox + placement.translation[0]); - aabb[1] = aabb[1].min(min_y + oy + placement.translation[1]); - aabb[2] = aabb[2].max(max_x + ox + placement.translation[0]); - aabb[3] = aabb[3].max(max_y + oy + placement.translation[1]); + aabb[0] = aabb[0].min(world_bounds[0] + ox + placement.translation[0]); + aabb[1] = aabb[1].min(world_bounds[1] + oy + placement.translation[1]); + aabb[2] = aabb[2].max(world_bounds[2] + ox + placement.translation[0]); + aabb[3] = aabb[3].max(world_bounds[3] + oy + placement.translation[1]); } aabb } else { - [min_x, min_y, max_x, max_y] + world_bounds }; Self { @@ -482,7 +474,7 @@ impl WipeoutGpu { // ── Helpers ─────────────────────────────────────────────────────────────── /// Perpendicular spacing between adjacent parallel lines. -/// QCAD PAT format stores (dx, dy) in line-local coordinates: dy = perpendicular, dx = along-line. +/// Pattern spacing perpendicular to adjacent parallel lines. fn perp_spacing(f: &PatFamily) -> f32 { f.dy } @@ -519,7 +511,7 @@ fn build_family_batch(pattern: &HatchPattern) -> FamilyBatchData { let angle_r = family.angle_deg.to_radians(); let cos_a = angle_r.cos(); let sin_a = angle_r.sin(); - // QCAD PAT local-frame convention: dy = perpendicular spacing, dx = along-line phase shift. + // Pattern offsets are stored in the line-local frame. let perp_step = family.dy; let along_step = family.dx; let line_width = 0.0_f32; // unused: shader uses screen-space derivative for 1px lines diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index f880ee7d..2de66884 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -655,6 +655,9 @@ impl BlockWireGpu { rustc_hash::FxHashMap::default(); let mut groups: Vec<(bool, Vec<&WireModel>)> = Vec::new(); for &wire in wires { + if color_override.is_none() && !wire.display_visible { + continue; + } let Some(instance) = wire.render_instance else { continue; }; diff --git a/src/scene/preview.rs b/src/scene/preview.rs index 805363b0..9faeda98 100644 --- a/src/scene/preview.rs +++ b/src/scene/preview.rs @@ -158,6 +158,8 @@ impl Scene { world_origin: origin, boundary: std::sync::Arc::new(boundary), boundary_wcs: None, + fill_plane: None, + fill_plane_boundary: None, boundary_exterior: None, boundary_sources: None, pattern: HatchPattern::Solid, diff --git a/src/scene/project.rs b/src/scene/project.rs index 8e03aa64..c975ff2c 100644 --- a/src/scene/project.rs +++ b/src/scene/project.rs @@ -409,8 +409,8 @@ impl Scene { } else { 0.0 }; - let project = |x: f64, y: f64| -> Option<[f32; 2]> { - let delta = glam::DVec3::new(x, y, 0.0) - camera.target; + let project_3d = |point: [f64; 3]| -> Option<[f32; 2]> { + let delta = glam::DVec3::from(point) - camera.target; let u = delta.dot(view_right.as_dvec3()); let v = delta.dot(view_up.as_dvec3()); let factor = if perspective { @@ -427,6 +427,7 @@ impl Scene { (center_y + v * factor * viewport_scale) as f32, ]) }; + let project = |x: f64, y: f64| project_3d([x, y, 0.0]); let frozen: rustc_hash::FxHashSet = viewport.frozen_layers.iter().copied().collect(); @@ -481,7 +482,7 @@ impl Scene { continue; } if let Some(hatch) = - project_plot_fill(hatch, &project, xmin, ymin, xmax, ymax) + project_plot_fill(hatch, &project_3d, xmin, ymin, xmax, ymax) { projected_hatches.push(hatch); } @@ -495,7 +496,7 @@ impl Scene { false, ) { if let Some(wipeout) = - project_plot_fill(wipeout, &project, xmin, ymin, xmax, ymax) + project_plot_fill(wipeout, &project_3d, xmin, ymin, xmax, ymax) { projected_wipeouts.push(wipeout); } @@ -753,7 +754,7 @@ fn project_plot_fill( ymax: f32, ) -> Option where - F: Fn(f64, f64) -> Option<[f32; 2]>, + F: Fn([f64; 3]) -> Option<[f32; 2]>, { let mut output = Vec::new(); let mut ring = Vec::new(); @@ -778,15 +779,35 @@ where } output.extend(clipped); }; - for &[x, y] in fill.boundary.iter() { - if x.is_nan() || y.is_nan() { - flush_ring(&mut ring, &mut output); - continue; + if let (Some(plane), Some(boundary)) = (fill.fill_plane, fill.fill_plane_boundary.as_deref()) { + let plane = cadkernel::space::Plane::from_axes( + plane.origin, + plane.x_axis, + plane.y_axis, + ); + for &[x, y] in boundary { + if x.is_nan() || y.is_nan() { + flush_ring(&mut ring, &mut output); + continue; + } + let point = plane.point_at([x as f64, y as f64]); + if let Some(point) = project(point) { + ring.push(point); + } } - let absolute_x = fill.world_origin[0] + x as f64; - let absolute_y = fill.world_origin[1] + y as f64; - if let Some(point) = project(absolute_x, absolute_y) { - ring.push(point); + } else { + for &[x, y] in fill.boundary.iter() { + if x.is_nan() || y.is_nan() { + flush_ring(&mut ring, &mut output); + continue; + } + if let Some(point) = project([ + fill.world_origin[0] + x as f64, + fill.world_origin[1] + y as f64, + 0.0, + ]) { + ring.push(point); + } } } flush_ring(&mut ring, &mut output); @@ -796,6 +817,8 @@ where fill.world_origin = [0.0, 0.0]; fill.boundary = std::sync::Arc::new(output); fill.boundary_wcs = None; + fill.fill_plane = None; + fill.fill_plane_boundary = None; Some(fill) } diff --git a/src/scene/text/complex_lt.rs b/src/scene/text/complex_lt.rs index 1c4812e6..5d39e48a 100644 --- a/src/scene/text/complex_lt.rs +++ b/src/scene/text/complex_lt.rs @@ -312,6 +312,8 @@ pub fn apply_along( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, @@ -356,6 +358,8 @@ pub fn apply_along( taper_widths: Vec::new(), world_width: 0.0, depth_override: None, + display_visible: true, + plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 1259872d..55424b5f 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -2925,6 +2925,9 @@ impl Scene { } let mut out: Vec = Vec::new(); for w in wires { + if !w.display_visible { + continue; + } if w.render_instance.is_some() { continue; } diff --git a/src/shaders/wipeout.wgsl b/src/shaders/wipeout.wgsl index 3b7d97ab..2558441b 100644 --- a/src/shaders/wipeout.wgsl +++ b/src/shaders/wipeout.wgsl @@ -46,15 +46,15 @@ struct HatchUniforms { grad_sin: f32, // 52: gradient direction sin grad_min: f32, // 56: gradient proj_min grad_range: f32, // 60: gradient proj_range - origin: vec2, // 64: hatch-local pattern origin (boundary - // // centre). Pattern math runs against - // // `xz - origin` so the f32 modulo doesn't - // // catastrophically cancel when world coords - // // are large and pattern spacing is small. - origin_low: vec2, // 72: anchor low residual (double-single) + origin: vec2, // 64: reserved pattern origin + origin_low: vec2, // 72: reserved low residual draw_depth: f32, // 80: signed (-1,1) draw-order bias; 0 = neutral _pad0: f32, // 84 _pad1: vec2, // 88 + plane_origin_high: vec4, + plane_origin_low: vec4, + plane_x: vec4, + plane_y: vec4, } @group(1) @binding(0) var h: HatchUniforms; @@ -91,8 +91,8 @@ struct FamilyBatch { struct VIn { @location(0) pos: vec3, - @location(1) translation: vec2, - @location(2) translation_low: vec2, + @location(1) translation: vec3, + @location(2) translation_low: vec3, @location(3) draw_depth: f32, } struct VOut { @@ -102,22 +102,9 @@ struct VOut { @vertex fn vs_main(v: VIn) -> VOut { var o: VOut; - // v.pos comes in pre-shifted to hatch-local space (CPU subtracted - // h.origin from the quad). Add origin back inside the view_proj - // multiply to land at the correct world-space clip position. The - // f32+f32 addition can lose sub-mm precision at very far hatches - // but the result feeds view_proj for screen NDC where sub-pixel - // jitter is invisible. The local-space `xz` we export to the - // fragment shader stays at full f32 precision so the in_polygon / - // pattern math doesn't catastrophically cancel. - // Double-single relative-to-eye: anchor high cancels eye_high (Sterbenz); - // boundary-local v.pos + anchor low + (−eye_low) carry the residual. - let hi = vec3(h.origin.x + v.translation.x - u.eye_high.x, - h.origin.y + v.translation.y - u.eye_high.y, - -u.eye_high.z); - let lo = vec3(v.pos.x + h.origin_low.x + v.translation_low.x - u.eye_low.x, - v.pos.y + h.origin_low.y + v.translation_low.y - u.eye_low.y, - v.pos.z - u.eye_low.z); + let local_world = h.plane_x.xyz * v.pos.x + h.plane_y.xyz * v.pos.y; + let hi = h.plane_origin_high.xyz + v.translation - u.eye_high; + let lo = local_world + h.plane_origin_low.xyz + v.translation_low - u.eye_low; o.clip = u.view_rot * vec4(hi + lo, 1.0); // Draw-order bias (see wire.wgsl): the mask erases what draws BELOW its // entity and loses to what draws above — including its own frame wire, @@ -303,12 +290,7 @@ fn check_family( } } - // 4. Pattern: v.xz is already in hatch-local space (CPU pre-shifted - // quad + BoundaryData by h.origin). The PAT family origin - // (typically (0,0)) is treated as relative to h.origin, so - // pattern phase is per-hatch rather than world-aligned — that - // trade-off is the price of f32 precision at large drawing - // extents. + // 4. Pattern coordinates are fill-local. let cos_off = cos(h.angle_offset); let sin_off = sin(h.angle_offset); for (var i = 0u; i < f.n_families; i++) { diff --git a/src/ui/properties.rs b/src/ui/properties.rs index 118afec8..4aeab73b 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -130,6 +130,8 @@ impl canvas::Program for HatchPatternPreview { [pad, bounds.height - pad], ]), boundary_wcs: None, + fill_plane: None, + fill_plane_boundary: None, boundary_exterior: None, boundary_sources: None, pattern: self.pattern.clone(),