feat(frame): complete frame controls

Preserve frame geometry for selection while separating viewport and plot visibility across supported reference entities.

Refs #642
This commit is contained in:
Hakan Seven committed 2026-08-20 23:37:02 +03:00
commit 44efb8fd53
88 files changed
+1538 -702

No files matched your search

Generated
+2 -2
View File
@@ -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",
+2 -2
View File
@@ -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"] }
+2 -2
View File
@@ -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"
+1 -1
View File
@@ -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"
+9 -1
View File
@@ -1416,11 +1416,19 @@ draw =
.usage-attdisp-on-off-normal = الاستخدام: ATTDISP ON | OFF | NORMAL
.usage-polysolid-width-height-select-a-polyline = الاستخدام: POLYSOLID <width> <height> (حدد خطًا متعددًا أولًا)
.usage-pyramid-radius-height-sides-default-4-si = الاستخدام: PYRAMID <radius> <height> [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 حدد نقطة العبور:
+9 -1
View File
@@ -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 Посочи точка на преминаване:
+9 -1
View File
@@ -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 <width> <height> (nejprve vyberte křivku)
.usage-pyramid-radius-height-sides-default-4-si = Použití: PYRAMID <radius> <height> [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] <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] <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:
+9 -1
View File
@@ -1404,11 +1404,19 @@ draw =
.usage-attdisp-on-off-normal = Verwendung: ATTDISP ON | OFF | NORMAL
.usage-polysolid-width-height-select-a-polyline = Verwendung: POLYSOLID <width> <height> (select a polyline first)
.usage-pyramid-radius-height-sides-default-4-si = Verwendung: PYRAMID <radius> <height> [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 <Polylinie>:
.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] <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:
+9 -1
View File
@@ -1405,11 +1405,19 @@ draw =
.usage-attdisp-on-off-normal = Usage: ATTDISP ON | OFF | NORMAL
.usage-polysolid-width-height-select-a-polyline = Usage: POLYSOLID <width> <height> (select a polyline first)
.usage-pyramid-radius-height-sides-default-4-si = Usage: PYRAMID <radius> <height> [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] <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] <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:
+9 -1
View File
@@ -1404,11 +1404,19 @@ draw =
.usage-attdisp-on-off-normal = Uso: ATTDISP ON | OFF | NORMAL
.usage-polysolid-width-height-select-a-polyline = Uso: POLYSOLID <anchura> <altura> (seleccione primero una polilínea)
.usage-pyramid-radius-height-sides-default-4-si = Uso: PYRAMID <radio> <altura> [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] <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] <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:
+9 -1
View File
@@ -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 <width> <height> (valitse ensin polyline)
.usage-pyramid-radius-height-sides-default-4-si = Käyttö: PYRAMID <radius> <height> [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] <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] <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:
+9 -1
View File
@@ -1404,11 +1404,19 @@ draw =
.usage-attdisp-on-off-normal = Utilisation : ATTDISP ON | OFF | NORMAL
.usage-polysolid-width-height-select-a-polyline = Utilisation : POLYSOLID <width> <height> (select a polyline first)
.usage-pyramid-radius-height-sides-default-4-si = Utilisation : PYRAMID <radius> <height> [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] <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] <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:
+9 -1
View File
@@ -1392,11 +1392,19 @@ draw =
.usage-attdisp-on-off-normal = उपयोग: ATTDISP ON | OFF | NORMAL
.usage-polysolid-width-height-select-a-polyline = उपयोग: POLYSOLID <width> <height> (select a polyline first)
.usage-pyramid-radius-height-sides-default-4-si = उपयोग: PYRAMID <radius> <height> [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 बिंदु के माध्यम से निर्दिष्ट करें:
+9 -1
View File
@@ -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 <width> <height> (először válasszon ki egy vonalláncot)
.usage-pyramid-radius-height-sides-default-4-si = Használat: PYRAMID <radius> <height> [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] <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] <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:
+9 -1
View File
@@ -1403,11 +1403,19 @@ draw =
.usage-attdisp-on-off-normal = Utilizzo: ATTDISP ON | OFF | NORMAL
.usage-polysolid-width-height-select-a-polyline = Utilizzo: POLYSOLID <width> <height> (selezionare prima una polilinea)
.usage-pyramid-radius-height-sides-default-4-si = Utilizzo: PYRAMID <radius> <height> [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] <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] <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:
+9 -1
View File
@@ -1394,11 +1394,19 @@ draw =
.usage-attdisp-on-off-normal = 使用法: ATTDISP ON | OFF | NORMAL
.usage-polysolid-width-height-select-a-polyline = 使用法: POLYSOLID <width> <height> (先にポリラインを選択)
.usage-pyramid-radius-height-sides-default-4-si = 使用法: PYRAMID <radius> <height> [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 通過点を指定:
+9 -1
View File
@@ -1403,11 +1403,19 @@ draw =
.usage-attdisp-on-off-normal = 사용법: ATTDISP ON | OFF | NORMAL
.usage-polysolid-width-height-select-a-polyline = 사용법: POLYSOLID <width> <height>(폴리라인을 먼저 선택하세요)
.usage-pyramid-radius-height-sides-default-4-si = 사용법: PYRAMID <radius> <height> [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 통과점 지정:
+9 -1
View File
@@ -1404,11 +1404,19 @@ draw =
.usage-attdisp-on-off-normal = Gebruik: ATTDISP ON | OFF | NORMAL
.usage-polysolid-width-height-select-a-polyline = Gebruik: POLYSOLID <width> <height> (select a polyline first)
.usage-pyramid-radius-height-sides-default-4-si = Gebruik: PYRAMID <radius> <height> [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] <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] <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:
+9 -1
View File
@@ -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 <width> <height> (najpierw wybierz polilinię)
.usage-pyramid-radius-height-sides-default-4-si = Zastosowanie: PYRAMID <radius> <height> [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] <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] <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:
+9 -1
View File
@@ -1404,11 +1404,19 @@ draw =
.usage-attdisp-on-off-normal = Uso: ATTDISP ON | OFF | NORMAL
.usage-polysolid-width-height-select-a-polyline = Uso: POLYSOLID <largura> <altura> (selecione primeiro uma polilinha)
.usage-pyramid-radius-height-sides-default-4-si = Uso: PYRAMID <raio> <altura> [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] <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] <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:
+9 -1
View File
@@ -1407,11 +1407,19 @@ draw =
.usage-attdisp-on-off-normal = Использование: ATTDISP ON | OFF | NORMAL
.usage-polysolid-width-height-select-a-polyline = Использование: POLYSOLID <width> <height> (select a polyline first)
.usage-pyramid-radius-height-sides-default-4-si = Использование: PYRAMID <radius> <height> [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 Укажите через точку:
+9 -1
View File
@@ -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 <width> <height> (önce bir çoklu çizgi seçin)
.usage-pyramid-radius-height-sides-default-4-si = Kullanım: PYRAMID <radius> <height> [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] <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:
+9 -1
View File
@@ -1391,11 +1391,19 @@ draw =
.usage-attdisp-on-off-normal = 用法: ATTDISP ON | OFF | NORMAL
.usage-polysolid-width-height-select-a-polyline = 用法: POLYSOLID <width> <height> (select a polyline first)
.usage-pyramid-radius-height-sides-default-4-si = 用法: PYRAMID <radius> <height> [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 指定通过点:
+9 -1
View File
@@ -1403,11 +1403,19 @@ draw =
.usage-attdisp-on-off-normal = 用法: ATTDISP ON | OFF | NORMAL
.usage-polysolid-width-height-select-a-polyline = 用法: POLYSOLID <width> <height> (select a polyline first)
.usage-pyramid-radius-height-sides-default-4-si = 用法: PYRAMID <radius> <height> [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 指定通過點:
+17 -7
View File
@@ -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);
+5 -1
View File
@@ -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));
+8
View File
@@ -514,7 +514,15 @@ inventory::submit!(crate::command::CommandRegistration {
"CMLJUST",
"TEXTQLTY",
"SORTENTS",
"FRAME",
"IMAGEFRAME",
"PDFFRAME",
"POINTCLOUDCLIPFRAME",
"XCLIPFRAME",
"WIPEOUTFRAME",
"FRAMES0",
"FRAMES1",
"FRAMES2",
"HALOGAP",
"TRACEWID",
"SKETCHINC",
+83 -11
View File
@@ -3,6 +3,9 @@ use super::*;
impl OpenCADStudio {
pub(super) fn dispatch_styleprops(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
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 <ByLayer|ByBlock|1-255|name> — 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::<i16>() {
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::<i16>()
.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::<i16>()
+5 -2
View File
@@ -168,7 +168,9 @@ fn plot_scene_content(
Vec<crate::scene::model::hatch_model::HatchModel>,
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 {
+2
View File
@@ -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,
+5 -1
View File
@@ -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),
+10
View File
@@ -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,
+18
View File
@@ -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<Vec<[f64; 3]>> {
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();
+4
View File
@@ -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,
+10
View File
@@ -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,
+207 -185
View File
@@ -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<Vec<[f64; 3]>> {
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<acadrust::types::Vector2> {
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<RenderEntity> {
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<GripDef> {
// 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, &current);
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::<f64>() else {
return;
};
+2
View File
@@ -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,
+4
View File
@@ -1814,6 +1814,10 @@ fn vardict_value(doc: &CadDocument, name: &str) -> Option<String> {
}
}
pub(crate) fn drawing_variable(doc: &CadDocument, name: &str) -> Option<String> {
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) {
+9 -1
View File
@@ -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] <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] <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")),
+4
View File
@@ -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,
+2
View File
@@ -164,6 +164,8 @@ fn preview_wire(points: Vec<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,
+2
View File
@@ -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,
+2
View File
@@ -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,
+2
View File
@@ -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,
+2
View File
@@ -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,
+2
View File
@@ -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,
+2
View File
@@ -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,
+2
View File
@@ -240,6 +240,8 @@ fn preview_wire(points: Vec<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,
+2
View File
@@ -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,
+2
View File
@@ -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,
+2
View File
@@ -140,6 +140,8 @@ fn preview_wire(points: Vec<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,
+2
View File
@@ -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,
+2
View File
@@ -200,6 +200,8 @@ fn preview_wire(points: Vec<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,
+2
View File
@@ -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,
+2
View File
@@ -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,
+2
View File
@@ -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,
+6
View File
@@ -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,
+2
View File
@@ -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,
+2
View File
@@ -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,
+4
View File
@@ -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,
+188 -243
View File
@@ -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<DVec3>,
points: Vec<DVec3>,
plane: WorkingPlane,
selected_polyline: Option<Handle>,
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<DVec3> = 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] <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] <No>:").into_owned()
}
}
}
fn options(&self) -> Vec<CmdOption> {
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<CmdResult> {
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<DVec3> {
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<EntityType> {
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<EntityType> {
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<EntityType> {
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<DVec3> {
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<EntityType> {
let points = match entity {
fn from_ocs(points: &[[f64; 2]], normal: Vector3, elevation: f64) -> Option<EntityType> {
make_poly_wipeout(points, crate::entities::curve::ocs_plane(normal, elevation))
}
match entity {
EntityType::LwPolyline(polyline) => {
let raw: Vec<DVec3> = 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<DVec3> = 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<DVec3> = 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<DVec3> = 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 ─────────────────────────────────
+2
View File
@@ -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,
+2
View File
@@ -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,
+2
View File
@@ -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,
+2
View File
@@ -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,
+1 -1
View File
@@ -69,7 +69,7 @@ impl CadModule for InsertModule {
),
(
"FRAMES2",
"Frames & Print",
"Frames On, Not Plotted",
IconKind::Svg(include_bytes!(
"../../../assets/icons/underlay_frames.svg"
)),
+2
View File
@@ -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,
+2
View File
@@ -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,
+2
View File
@@ -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,
+28
View File
@@ -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<f32>,
) -> 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;
+31 -4
View File
@@ -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,
+112
View File
@@ -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<Vec<WireModel>> {
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,
+125 -57
View File
@@ -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<Vec<[f32; 2]>> {
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,
+211
View File
@@ -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<Handle> {
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<Handle> {
root_entry(document, "ACAD_IMAGE_VARS")
.filter(|handle| matches!(document.objects.get(handle), Some(ObjectType::RasterVariables(_))))
}
fn wipeout_variables_handle(document: &CadDocument) -> Option<Handle> {
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::<i16>().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::<i16>().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<FrameKind> {
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<FrameKind> {
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(_)
)
),
}
}
+3
View File
@@ -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,
+10
View File
@@ -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<Arc<Vec<[f64; 2]>>>,
/// Optional 3-D placement used by wipeout fills.
pub fill_plane: Option<FillPlane>,
pub fill_plane_boundary: Option<Arc<Vec<[f32; 2]>>>,
/// Per-ring DXF role, aligned with the NaN-separated boundary paths.
pub boundary_exterior: Option<Arc<Vec<bool>>>,
/// Source entity handles for each boundary ring.
+8
View File
@@ -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<f32>,
/// 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,
+2
View File
@@ -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,
+7 -4
View File
@@ -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
{
+3
View File
@@ -433,6 +433,9 @@ pub fn upload_block_vertex_refs(
let mut slots = rustc_hash::FxHashMap::default();
let mut groups: Vec<Vec<&crate::scene::model::wire_model::WireModel>> = Vec::new();
for &wire in wires {
if tint.is_none() && !wire.display_visible {
continue;
}
let Some(instance) = wire.render_instance else {
continue;
};
+107 -115
View File
@@ -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::<Self>() 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<f32> = model
.boundary
.iter()
let projs: Vec<f32> = 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
+3
View File
@@ -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;
};
+2
View File
@@ -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,
+36 -13
View File
@@ -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<Handle> =
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<F>(
ymax: f32,
) -> Option<HatchModel>
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)
}
+4
View File
@@ -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,
+3
View File
@@ -2925,6 +2925,9 @@ impl Scene {
}
let mut out: Vec<crate::scene::pipeline::text_gpu::TextVertex> = Vec::new();
for w in wires {
if !w.display_visible {
continue;
}
if w.render_instance.is_some() {
continue;
}
+12 -30
View File
@@ -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<f32>, // 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<f32>, // 72: anchor low residual (double-single)
origin: vec2<f32>, // 64: reserved pattern origin
origin_low: vec2<f32>, // 72: reserved low residual
draw_depth: f32, // 80: signed (-1,1) draw-order bias; 0 = neutral
_pad0: f32, // 84
_pad1: vec2<f32>, // 88
plane_origin_high: vec4<f32>,
plane_origin_low: vec4<f32>,
plane_x: vec4<f32>,
plane_y: vec4<f32>,
}
@group(1) @binding(0) var<uniform> h: HatchUniforms;
@@ -91,8 +91,8 @@ struct FamilyBatch {
struct VIn {
@location(0) pos: vec3<f32>,
@location(1) translation: vec2<f32>,
@location(2) translation_low: vec2<f32>,
@location(1) translation: vec3<f32>,
@location(2) translation_low: vec3<f32>,
@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<f32>(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<f32>(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<f32>(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++) {
+2
View File
@@ -130,6 +130,8 @@ impl canvas::Program<Message> 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(),