feat(model): add exact solid modelling tools
No files matched your search
@@ -72,7 +72,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "acadrust"
|
name = "acadrust"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=db73dab#db73dab540dbfb690c93f4416014262e6cf7bf42"
|
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cadkernel"
|
name = "cadkernel"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=467c3fd#467c3fd02f419ab5af4030691699b34b25d2f51a"
|
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=76de3b3#76de3b35e55a45f3ffb813c499cf3affe176351d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acadrust",
|
"acadrust",
|
||||||
"cavalier_contours",
|
"cavalier_contours",
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ glam = { version = "0.33", features = ["bytemuck"] }
|
|||||||
rfd = "0.17"
|
rfd = "0.17"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "db73dab", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "94df2c3", features = ["serde"] }
|
||||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "467c3fd", features = ["acis", "offset"] }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "76de3b3", features = ["acis", "offset"] }
|
||||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
|
||||||
|
<path d="M4 16 10 13 16 16 10 19Z" fill="none" stroke="#e0e0e0" stroke-width="1.7" stroke-linejoin="round"/>
|
||||||
|
<path d="M10 13V5M7 8l3-3 3 3" fill="none" stroke="#4cc9f0" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 332 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
|
||||||
|
<path d="M5 18 12 21 19 18 16 6 12 3 8 6Z" fill="none" stroke="#e0e0e0" stroke-width="1.7" stroke-linejoin="round"/>
|
||||||
|
<path d="M8 6h8M5 18h14" stroke="#4cc9f0" stroke-width="1.4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 275 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
|
||||||
|
<path d="M3 17 8 12 13 16 21 8V14L13 22 8 18 3 23Z" fill="none" stroke="#e0e0e0" stroke-width="1.7" stroke-linejoin="round"/>
|
||||||
|
<path d="M3 17V23M8 12V18M13 16V22M21 8V14" stroke="#4cc9f0" stroke-width="1.4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 303 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
|
||||||
|
<path d="M4 17 10 14 16 17 10 20Z" fill="none" stroke="#e0e0e0" stroke-width="1.7" stroke-linejoin="round"/>
|
||||||
|
<path d="M10 14V5M7 8l3-3 3 3M18 5v9m-2-3 2 3 2-3" fill="none" stroke="#4cc9f0" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 352 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
|
||||||
|
<path d="M12 3 3 18 12 22 21 18Z" fill="none" stroke="#e0e0e0" stroke-width="1.7" stroke-linejoin="round"/>
|
||||||
|
<path d="M12 3V22M3 18H21" fill="none" stroke="#4cc9f0" stroke-width="1.4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 280 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
|
||||||
|
<path d="M8 18V6M8 7c7-2 11 1 11 5s-4 7-11 5" fill="none" stroke="#e0e0e0" stroke-width="1.7" stroke-linecap="round"/>
|
||||||
|
<path d="m16 8 3 4-4 2" fill="none" stroke="#4cc9f0" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 335 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
|
||||||
|
<circle cx="6" cy="17" r="3" fill="none" stroke="#e0e0e0" stroke-width="1.7"/>
|
||||||
|
<path d="M8 15c3-6 6-8 11-8M16 5l3 2-2 3" fill="none" stroke="#4cc9f0" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 313 B |
@@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
|
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
|
||||||
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
||||||
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
|
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "db73dab", optional = true, features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "94df2c3", optional = true, features = ["serde"] }
|
||||||
|
|
||||||
# Runtime IPC and serialization (host feature only).
|
# Runtime IPC and serialization (host feature only).
|
||||||
interprocess = { version = "2", optional = true }
|
interprocess = { version = "2", optional = true }
|
||||||
@@ -37,7 +37,7 @@ serde_json = "1"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
cargo-lock = "11"
|
cargo-lock = "11"
|
||||||
# acadrust is scanned at build time to generate the embedded type registry.
|
# acadrust is scanned at build time to generate the embedded type registry.
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "db73dab", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "94df2c3", features = ["serde"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
@@ -8,7 +8,7 @@ publish = false
|
|||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "db73dab", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "94df2c3", features = ["serde"] }
|
||||||
bincode = "1.3"
|
bincode = "1.3"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ribbon-tab =
|
|||||||
.draw = رسم
|
.draw = رسم
|
||||||
.annotate = تعليقات توضيحية
|
.annotate = تعليقات توضيحية
|
||||||
.insert = إدراج
|
.insert = إدراج
|
||||||
.model = نموذج
|
.model = النمذجة
|
||||||
.layout = تخطيط
|
.layout = تخطيط
|
||||||
.manage = إدارة
|
.manage = إدارة
|
||||||
.view = عرض
|
.view = عرض
|
||||||
@@ -2047,6 +2047,15 @@ insert =
|
|||||||
.object-s-cut-to-clipboard = قُص __ocs_fmt_0__ كائن إلى الحافظة.
|
.object-s-cut-to-clipboard = قُص __ocs_fmt_0__ كائن إلى الحافظة.
|
||||||
|
|
||||||
model =
|
model =
|
||||||
|
.create = إنشاء
|
||||||
|
.boolean = عمليات منطقية
|
||||||
|
.pyramid = هرم
|
||||||
|
.polysolid = مجسّم متعدد
|
||||||
|
.extrude = بثق
|
||||||
|
.revolve = تدوير
|
||||||
|
.loft = تشكيل انتقالي
|
||||||
|
.sweep = سحب على مسار
|
||||||
|
.presspull = دفع/سحب
|
||||||
.n-specify-center-point = __ocs_arg_n__ حدد نقطة المركز:
|
.n-specify-center-point = __ocs_arg_n__ حدد نقطة المركز:
|
||||||
.n-specify-first-corner = __ocs_arg_n__ حدد الزاوية الأولى:
|
.n-specify-first-corner = __ocs_arg_n__ حدد الزاوية الأولى:
|
||||||
.n-specify-height-enter-for-default = __ocs_arg_n__ حدد الارتفاع <Enter للافتراضي>:
|
.n-specify-height-enter-for-default = __ocs_arg_n__ حدد الارتفاع <Enter للافتراضي>:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ribbon-tab =
|
|||||||
.draw = Чертане
|
.draw = Чертане
|
||||||
.annotate = Анотиране
|
.annotate = Анотиране
|
||||||
.insert = Вмъкване
|
.insert = Вмъкване
|
||||||
.model = Модел
|
.model = Моделиране
|
||||||
.layout = Оформление
|
.layout = Оформление
|
||||||
.manage = Управление
|
.manage = Управление
|
||||||
.view = Изглед
|
.view = Изглед
|
||||||
@@ -2034,6 +2034,15 @@ insert =
|
|||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ обект(а) изрязани в клипборда.
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ обект(а) изрязани в клипборда.
|
||||||
|
|
||||||
model =
|
model =
|
||||||
|
.create = Създаване
|
||||||
|
.boolean = Булеви операции
|
||||||
|
.pyramid = Пирамида
|
||||||
|
.polysolid = Полисолид
|
||||||
|
.extrude = Екструдиране
|
||||||
|
.revolve = Завъртане
|
||||||
|
.loft = Лофт
|
||||||
|
.sweep = Изтегляне по траектория
|
||||||
|
.presspull = Натискане/издърпване
|
||||||
.n-specify-center-point = __ocs_arg_n__ Посочи централна точка:
|
.n-specify-center-point = __ocs_arg_n__ Посочи централна точка:
|
||||||
.n-specify-first-corner = __ocs_arg_n__ Посочи първи ъгъл:
|
.n-specify-first-corner = __ocs_arg_n__ Посочи първи ъгъл:
|
||||||
.n-specify-height-enter-for-default = __ocs_arg_n__ Посочи височина <Enter за подразбиране>:
|
.n-specify-height-enter-for-default = __ocs_arg_n__ Посочи височина <Enter за подразбиране>:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ribbon-tab =
|
|||||||
.draw = Kreslení
|
.draw = Kreslení
|
||||||
.annotate = Poznámky
|
.annotate = Poznámky
|
||||||
.insert = Vložit
|
.insert = Vložit
|
||||||
.model = Model
|
.model = Modelování
|
||||||
.layout = Rozvržení
|
.layout = Rozvržení
|
||||||
.manage = Správa
|
.manage = Správa
|
||||||
.view = Zobrazení
|
.view = Zobrazení
|
||||||
@@ -2034,6 +2034,15 @@ insert =
|
|||||||
.object-s-cut-to-clipboard = Objekt(y) __ocs_fmt_0__ vyříznutý do schránky.
|
.object-s-cut-to-clipboard = Objekt(y) __ocs_fmt_0__ vyříznutý do schránky.
|
||||||
|
|
||||||
model =
|
model =
|
||||||
|
.create = Vytvořit
|
||||||
|
.boolean = Booleovské operace
|
||||||
|
.pyramid = Jehlan
|
||||||
|
.polysolid = Polysolid
|
||||||
|
.extrude = Vysunutí
|
||||||
|
.revolve = Rotace
|
||||||
|
.loft = Přechod
|
||||||
|
.sweep = Tažení
|
||||||
|
.presspull = Tlačit/táhnout
|
||||||
.n-specify-center-point = __ocs_arg_n__ Zadejte středový bod:
|
.n-specify-center-point = __ocs_arg_n__ Zadejte středový bod:
|
||||||
.n-specify-first-corner = __ocs_arg_n__ Určete první roh:
|
.n-specify-first-corner = __ocs_arg_n__ Určete první roh:
|
||||||
.n-specify-height-enter-for-default = __ocs_arg_n__ Zadejte výšku <Enter for default>:
|
.n-specify-height-enter-for-default = __ocs_arg_n__ Zadejte výšku <Enter for default>:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ribbon-tab =
|
|||||||
.draw = Zeichnen
|
.draw = Zeichnen
|
||||||
.annotate = Beschriften
|
.annotate = Beschriften
|
||||||
.insert = Einsatz
|
.insert = Einsatz
|
||||||
.model = Modell
|
.model = Modellierung
|
||||||
.layout = Layout
|
.layout = Layout
|
||||||
.manage = Verwalten
|
.manage = Verwalten
|
||||||
.view = Ansicht
|
.view = Ansicht
|
||||||
@@ -2033,6 +2033,15 @@ insert =
|
|||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ Objekt(e) in die Zwischenablage geschnitten.
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ Objekt(e) in die Zwischenablage geschnitten.
|
||||||
|
|
||||||
model =
|
model =
|
||||||
|
.create = Erstellen
|
||||||
|
.boolean = Boolesche Operationen
|
||||||
|
.pyramid = Pyramide
|
||||||
|
.polysolid = Polykörper
|
||||||
|
.extrude = Extrudieren
|
||||||
|
.revolve = Drehen
|
||||||
|
.loft = Loft
|
||||||
|
.sweep = Austragen
|
||||||
|
.presspull = Drücken/Ziehen
|
||||||
.n-specify-center-point = __ocs_arg_n__ Mittelpunkt angeben:
|
.n-specify-center-point = __ocs_arg_n__ Mittelpunkt angeben:
|
||||||
.n-specify-first-corner = __ocs_arg_n__ Erste Ecke angeben:
|
.n-specify-first-corner = __ocs_arg_n__ Erste Ecke angeben:
|
||||||
.n-specify-height-enter-for-default = __ocs_arg_n__ Höhe angeben <Eingabe für Standardwert>:
|
.n-specify-height-enter-for-default = __ocs_arg_n__ Höhe angeben <Eingabe für Standardwert>:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ribbon-tab =
|
|||||||
.draw = Draw
|
.draw = Draw
|
||||||
.annotate = Annotate
|
.annotate = Annotate
|
||||||
.insert = Insert
|
.insert = Insert
|
||||||
.model = Model
|
.model = Modelling
|
||||||
.layout = Layout
|
.layout = Layout
|
||||||
.manage = Manage
|
.manage = Manage
|
||||||
.view = View
|
.view = View
|
||||||
@@ -2063,6 +2063,15 @@ insert =
|
|||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ object(s) cut to clipboard.
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ object(s) cut to clipboard.
|
||||||
|
|
||||||
model =
|
model =
|
||||||
|
.create = Create
|
||||||
|
.boolean = Boolean
|
||||||
|
.pyramid = Pyramid
|
||||||
|
.polysolid = Polysolid
|
||||||
|
.extrude = Extrude
|
||||||
|
.revolve = Revolve
|
||||||
|
.loft = Loft
|
||||||
|
.sweep = Sweep
|
||||||
|
.presspull = Presspull
|
||||||
.n-specify-center-point = __ocs_arg_n__ Specify center point:
|
.n-specify-center-point = __ocs_arg_n__ Specify center point:
|
||||||
.n-specify-first-corner = __ocs_arg_n__ Specify first corner:
|
.n-specify-first-corner = __ocs_arg_n__ Specify first corner:
|
||||||
.n-specify-height-enter-for-default = __ocs_arg_n__ Specify height <Enter for default>:
|
.n-specify-height-enter-for-default = __ocs_arg_n__ Specify height <Enter for default>:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ribbon-tab =
|
|||||||
.draw = Dibujo
|
.draw = Dibujo
|
||||||
.annotate = Anotar
|
.annotate = Anotar
|
||||||
.insert = Insertar
|
.insert = Insertar
|
||||||
.model = Modelo
|
.model = Modelado
|
||||||
.layout = Presentación
|
.layout = Presentación
|
||||||
.manage = Administrar
|
.manage = Administrar
|
||||||
.view = Vista
|
.view = Vista
|
||||||
@@ -2035,6 +2035,15 @@ insert =
|
|||||||
.object-s-cut-to-clipboard = Se cortaron __ocs_fmt_0__ objetos al portapapeles.
|
.object-s-cut-to-clipboard = Se cortaron __ocs_fmt_0__ objetos al portapapeles.
|
||||||
|
|
||||||
model =
|
model =
|
||||||
|
.create = Crear
|
||||||
|
.boolean = Booleanas
|
||||||
|
.pyramid = Pirámide
|
||||||
|
.polysolid = Polisólido
|
||||||
|
.extrude = Extruir
|
||||||
|
.revolve = Revolucionar
|
||||||
|
.loft = Transición
|
||||||
|
.sweep = Barrido
|
||||||
|
.presspull = Presionar/tirar
|
||||||
.n-specify-center-point = __ocs_arg_n__ Especifique el centro:
|
.n-specify-center-point = __ocs_arg_n__ Especifique el centro:
|
||||||
.n-specify-first-corner = __ocs_arg_n__ Especifique la primera esquina:
|
.n-specify-first-corner = __ocs_arg_n__ Especifique la primera esquina:
|
||||||
.n-specify-height-enter-for-default = __ocs_arg_n__ Especifique la altura <Enter para predeterminada>:
|
.n-specify-height-enter-for-default = __ocs_arg_n__ Especifique la altura <Enter para predeterminada>:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ribbon-tab =
|
|||||||
.draw = Piirrä
|
.draw = Piirrä
|
||||||
.annotate = Merkinnät
|
.annotate = Merkinnät
|
||||||
.insert = Lisää
|
.insert = Lisää
|
||||||
.model = Malli
|
.model = Mallinnus
|
||||||
.layout = Asettelu
|
.layout = Asettelu
|
||||||
.manage = Hallinta
|
.manage = Hallinta
|
||||||
.view = Näkymä
|
.view = Näkymä
|
||||||
@@ -2034,6 +2034,15 @@ insert =
|
|||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ objekti(t) leikattu leikepöydälle.
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ objekti(t) leikattu leikepöydälle.
|
||||||
|
|
||||||
model =
|
model =
|
||||||
|
.create = Luo
|
||||||
|
.boolean = Boolen operaatiot
|
||||||
|
.pyramid = Pyramidi
|
||||||
|
.polysolid = Monikappale
|
||||||
|
.extrude = Pursota
|
||||||
|
.revolve = Pyöräytä
|
||||||
|
.loft = Loftaus
|
||||||
|
.sweep = Pyyhkäisy
|
||||||
|
.presspull = Työnnä/vedä
|
||||||
.n-specify-center-point = __ocs_arg_n__ Määritä keskipiste:
|
.n-specify-center-point = __ocs_arg_n__ Määritä keskipiste:
|
||||||
.n-specify-first-corner = __ocs_arg_n__ Määritä ensimmäinen kulma:
|
.n-specify-first-corner = __ocs_arg_n__ Määritä ensimmäinen kulma:
|
||||||
.n-specify-height-enter-for-default = __ocs_arg_n__ Määritä korkeus <Enter for default>:
|
.n-specify-height-enter-for-default = __ocs_arg_n__ Määritä korkeus <Enter for default>:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ribbon-tab =
|
|||||||
.draw = Dessin
|
.draw = Dessin
|
||||||
.annotate = Annoter
|
.annotate = Annoter
|
||||||
.insert = Insérer
|
.insert = Insérer
|
||||||
.model = Modèle
|
.model = Modélisation
|
||||||
.layout = Mise en page
|
.layout = Mise en page
|
||||||
.manage = Gérer
|
.manage = Gérer
|
||||||
.view = Affichage
|
.view = Affichage
|
||||||
@@ -2035,6 +2035,15 @@ insert =
|
|||||||
.object-s-cut-to-clipboard = Objets coupés vers le presse-papiers : __ocs_fmt_0__.
|
.object-s-cut-to-clipboard = Objets coupés vers le presse-papiers : __ocs_fmt_0__.
|
||||||
|
|
||||||
model =
|
model =
|
||||||
|
.create = Créer
|
||||||
|
.boolean = Opérations booléennes
|
||||||
|
.pyramid = Pyramide
|
||||||
|
.polysolid = Polysolide
|
||||||
|
.extrude = Extruder
|
||||||
|
.revolve = Révolution
|
||||||
|
.loft = Lissage
|
||||||
|
.sweep = Balayage
|
||||||
|
.presspull = Tirer-pousser
|
||||||
.n-specify-center-point = __ocs_arg_n__ Spécifier le point central:
|
.n-specify-center-point = __ocs_arg_n__ Spécifier le point central:
|
||||||
.n-specify-first-corner = __ocs_arg_n__ Spécifiez le premier coin :
|
.n-specify-first-corner = __ocs_arg_n__ Spécifiez le premier coin :
|
||||||
.n-specify-height-enter-for-default = __ocs_arg_n__ Spécifiez la hauteur <Entrer pour la valeur par défaut> :
|
.n-specify-height-enter-for-default = __ocs_arg_n__ Spécifiez la hauteur <Entrer pour la valeur par défaut> :
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ribbon-tab =
|
|||||||
.draw = ड्रॉ
|
.draw = ड्रॉ
|
||||||
.annotate = एनोटेट
|
.annotate = एनोटेट
|
||||||
.insert = सम्मिलित करें
|
.insert = सम्मिलित करें
|
||||||
.model = मॉडल
|
.model = मॉडलिंग
|
||||||
.layout = लेआउट
|
.layout = लेआउट
|
||||||
.manage = प्रबंधित करें
|
.manage = प्रबंधित करें
|
||||||
.view = देखें
|
.view = देखें
|
||||||
@@ -2023,6 +2023,15 @@ insert =
|
|||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ ऑब्जेक्ट क्लिपबोर्ड पर काटे गए।
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ ऑब्जेक्ट क्लिपबोर्ड पर काटे गए।
|
||||||
|
|
||||||
model =
|
model =
|
||||||
|
.create = बनाएँ
|
||||||
|
.boolean = बूलियन संक्रियाएँ
|
||||||
|
.pyramid = पिरामिड
|
||||||
|
.polysolid = बहुखंड ठोस
|
||||||
|
.extrude = एक्सट्रूड
|
||||||
|
.revolve = घुमाएँ
|
||||||
|
.loft = लॉफ्ट
|
||||||
|
.sweep = स्वीप
|
||||||
|
.presspull = दबाएँ/खींचें
|
||||||
.n-specify-center-point = __ocs_arg_n__ केंद्र बिंदु निर्दिष्ट करें:
|
.n-specify-center-point = __ocs_arg_n__ केंद्र बिंदु निर्दिष्ट करें:
|
||||||
.n-specify-first-corner = __ocs_arg_n__ पहले कोने को निर्दिष्ट करें:
|
.n-specify-first-corner = __ocs_arg_n__ पहले कोने को निर्दिष्ट करें:
|
||||||
.n-specify-height-enter-for-default = __ocs_arg_n__ ऊंचाई निर्दिष्ट करें <Diefal> के लिए दर्ज करें>:
|
.n-specify-height-enter-for-default = __ocs_arg_n__ ऊंचाई निर्दिष्ट करें <Diefal> के लिए दर्ज करें>:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ribbon-tab =
|
|||||||
.draw = Rajz
|
.draw = Rajz
|
||||||
.annotate = Feliratozás
|
.annotate = Feliratozás
|
||||||
.insert = Beszúrás
|
.insert = Beszúrás
|
||||||
.model = Modell
|
.model = Modellezés
|
||||||
.layout = Elrendezés
|
.layout = Elrendezés
|
||||||
.manage = Kezelés
|
.manage = Kezelés
|
||||||
.view = Nézet
|
.view = Nézet
|
||||||
@@ -2034,6 +2034,15 @@ insert =
|
|||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ objektum(ok) vágólapra vágva.
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ objektum(ok) vágólapra vágva.
|
||||||
|
|
||||||
model =
|
model =
|
||||||
|
.create = Létrehozás
|
||||||
|
.boolean = Logikai műveletek
|
||||||
|
.pyramid = Gúla
|
||||||
|
.polysolid = Poliszilárdtest
|
||||||
|
.extrude = Kihúzás
|
||||||
|
.revolve = Forgatás
|
||||||
|
.loft = Átmenet
|
||||||
|
.sweep = Seprés
|
||||||
|
.presspull = Nyomás/húzás
|
||||||
.n-specify-center-point = __ocs_arg_n__ Középpont megadása:
|
.n-specify-center-point = __ocs_arg_n__ Középpont megadása:
|
||||||
.n-specify-first-corner = __ocs_arg_n__ Adja meg az első sarkot:
|
.n-specify-first-corner = __ocs_arg_n__ Adja meg az első sarkot:
|
||||||
.n-specify-height-enter-for-default = __ocs_arg_n__ Adja meg a <Enter for default> magasságot:
|
.n-specify-height-enter-for-default = __ocs_arg_n__ Adja meg a <Enter for default> magasságot:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ribbon-tab =
|
|||||||
.draw = Disegno
|
.draw = Disegno
|
||||||
.annotate = Annotazione
|
.annotate = Annotazione
|
||||||
.insert = Inserisci
|
.insert = Inserisci
|
||||||
.model = Modello
|
.model = Modellazione
|
||||||
.layout = Disposizione
|
.layout = Disposizione
|
||||||
.manage = Gestisci
|
.manage = Gestisci
|
||||||
.view = Vista
|
.view = Vista
|
||||||
@@ -2034,6 +2034,15 @@ insert =
|
|||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ oggetto/i tagliato negli appunti.
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ oggetto/i tagliato negli appunti.
|
||||||
|
|
||||||
model =
|
model =
|
||||||
|
.create = Crea
|
||||||
|
.boolean = Operazioni booleane
|
||||||
|
.pyramid = Piramide
|
||||||
|
.polysolid = Polisolido
|
||||||
|
.extrude = Estrudi
|
||||||
|
.revolve = Rivoluzione
|
||||||
|
.loft = Loft
|
||||||
|
.sweep = Scorrimento
|
||||||
|
.presspull = Premi/tira
|
||||||
.n-specify-center-point = __ocs_arg_n__ Specificare il punto centrale:
|
.n-specify-center-point = __ocs_arg_n__ Specificare il punto centrale:
|
||||||
.n-specify-first-corner = __ocs_arg_n__ Specificare il primo angolo:
|
.n-specify-first-corner = __ocs_arg_n__ Specificare il primo angolo:
|
||||||
.n-specify-height-enter-for-default = __ocs_arg_n__ Specificare l'altezza <Enter for default>:
|
.n-specify-height-enter-for-default = __ocs_arg_n__ Specificare l'altezza <Enter for default>:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ribbon-tab =
|
|||||||
.draw = 作図
|
.draw = 作図
|
||||||
.annotate = 注釈
|
.annotate = 注釈
|
||||||
.insert = 挿入
|
.insert = 挿入
|
||||||
.model = モデル
|
.model = モデリング
|
||||||
.layout = レイアウト
|
.layout = レイアウト
|
||||||
.manage = 管理
|
.manage = 管理
|
||||||
.view = 表示
|
.view = 表示
|
||||||
@@ -2023,6 +2023,15 @@ insert =
|
|||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ 個のオブジェクトを切り取ってクリップボードに保存しました。
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ 個のオブジェクトを切り取ってクリップボードに保存しました。
|
||||||
|
|
||||||
model =
|
model =
|
||||||
|
.create = 作成
|
||||||
|
.boolean = ブール演算
|
||||||
|
.pyramid = ピラミッド
|
||||||
|
.polysolid = ポリソリッド
|
||||||
|
.extrude = 押し出し
|
||||||
|
.revolve = 回転
|
||||||
|
.loft = ロフト
|
||||||
|
.sweep = スイープ
|
||||||
|
.presspull = プレスプル
|
||||||
.n-specify-center-point = __ocs_arg_n__ 中心点を指定:
|
.n-specify-center-point = __ocs_arg_n__ 中心点を指定:
|
||||||
.n-specify-first-corner = __ocs_arg_n__ 最初のコーナーを指定:
|
.n-specify-first-corner = __ocs_arg_n__ 最初のコーナーを指定:
|
||||||
.n-specify-height-enter-for-default = __ocs_arg_n__ 高さを指定 <Enter で既定値>:
|
.n-specify-height-enter-for-default = __ocs_arg_n__ 高さを指定 <Enter で既定値>:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ribbon-tab =
|
|||||||
.draw = 그리기
|
.draw = 그리기
|
||||||
.annotate = 주석
|
.annotate = 주석
|
||||||
.insert = 삽입
|
.insert = 삽입
|
||||||
.model = 모델
|
.model = 모델링
|
||||||
.layout = 배치
|
.layout = 배치
|
||||||
.manage = 관리
|
.manage = 관리
|
||||||
.view = 보기
|
.view = 보기
|
||||||
@@ -2034,6 +2034,15 @@ insert =
|
|||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ 개체를 클립보드로 잘라냈습니다.
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ 개체를 클립보드로 잘라냈습니다.
|
||||||
|
|
||||||
model =
|
model =
|
||||||
|
.create = 생성
|
||||||
|
.boolean = 불리언 연산
|
||||||
|
.pyramid = 피라미드
|
||||||
|
.polysolid = 폴리솔리드
|
||||||
|
.extrude = 돌출
|
||||||
|
.revolve = 회전
|
||||||
|
.loft = 로프트
|
||||||
|
.sweep = 스윕
|
||||||
|
.presspull = 누르기/당기기
|
||||||
.n-specify-center-point = __ocs_arg_n__ 중심점 지정:
|
.n-specify-center-point = __ocs_arg_n__ 중심점 지정:
|
||||||
.n-specify-first-corner = __ocs_arg_n__ 첫 번째 구석 지정:
|
.n-specify-first-corner = __ocs_arg_n__ 첫 번째 구석 지정:
|
||||||
.n-specify-height-enter-for-default = __ocs_arg_n__ 높이 지정 <Enter for default>:
|
.n-specify-height-enter-for-default = __ocs_arg_n__ 높이 지정 <Enter for default>:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ribbon-tab =
|
|||||||
.draw = Tekenen
|
.draw = Tekenen
|
||||||
.annotate = Annoteren
|
.annotate = Annoteren
|
||||||
.insert = Invoegen
|
.insert = Invoegen
|
||||||
.model = Model
|
.model = Modelleren
|
||||||
.layout = Indeling
|
.layout = Indeling
|
||||||
.manage = Beheren
|
.manage = Beheren
|
||||||
.view = Beeld
|
.view = Beeld
|
||||||
@@ -2035,6 +2035,15 @@ insert =
|
|||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ object(en) naar het klembord geknipt.
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ object(en) naar het klembord geknipt.
|
||||||
|
|
||||||
model =
|
model =
|
||||||
|
.create = Maken
|
||||||
|
.boolean = Booleaanse bewerkingen
|
||||||
|
.pyramid = Piramide
|
||||||
|
.polysolid = Polysolid
|
||||||
|
.extrude = Extruderen
|
||||||
|
.revolve = Omwentelen
|
||||||
|
.loft = Loft
|
||||||
|
.sweep = Vegen
|
||||||
|
.presspull = Duwen/trekken
|
||||||
.n-specify-center-point = __ocs_arg_n__ Centrumpunt opgeven:
|
.n-specify-center-point = __ocs_arg_n__ Centrumpunt opgeven:
|
||||||
.n-specify-first-corner = __ocs_arg_n__ Geef eerste hoek:
|
.n-specify-first-corner = __ocs_arg_n__ Geef eerste hoek:
|
||||||
.n-specify-height-enter-for-default = __ocs_arg_n__ Hoogte <Enter voor standaard> opgeven:
|
.n-specify-height-enter-for-default = __ocs_arg_n__ Hoogte <Enter voor standaard> opgeven:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ribbon-tab =
|
|||||||
.draw = Rysowanie
|
.draw = Rysowanie
|
||||||
.annotate = Opisy
|
.annotate = Opisy
|
||||||
.insert = Wstaw
|
.insert = Wstaw
|
||||||
.model = Model
|
.model = Modelowanie
|
||||||
.layout = Układ
|
.layout = Układ
|
||||||
.manage = Zarządzaj
|
.manage = Zarządzaj
|
||||||
.view = Widok
|
.view = Widok
|
||||||
@@ -2034,6 +2034,15 @@ insert =
|
|||||||
.object-s-cut-to-clipboard = Obiekt(y) __ocs_fmt_0__ wycięty do schowka.
|
.object-s-cut-to-clipboard = Obiekt(y) __ocs_fmt_0__ wycięty do schowka.
|
||||||
|
|
||||||
model =
|
model =
|
||||||
|
.create = Utwórz
|
||||||
|
.boolean = Operacje boolowskie
|
||||||
|
.pyramid = Ostrosłup
|
||||||
|
.polysolid = Bryła wielosegmentowa
|
||||||
|
.extrude = Wyciągnij
|
||||||
|
.revolve = Obróć
|
||||||
|
.loft = Loft
|
||||||
|
.sweep = Przeciągnij
|
||||||
|
.presspull = Naciśnij/wyciągnij
|
||||||
.n-specify-center-point = __ocs_arg_n__ Określ punkt środkowy:
|
.n-specify-center-point = __ocs_arg_n__ Określ punkt środkowy:
|
||||||
.n-specify-first-corner = __ocs_arg_n__ Określ pierwszy narożnik:
|
.n-specify-first-corner = __ocs_arg_n__ Określ pierwszy narożnik:
|
||||||
.n-specify-height-enter-for-default = __ocs_arg_n__ Podaj wysokość <Enter for default>:
|
.n-specify-height-enter-for-default = __ocs_arg_n__ Podaj wysokość <Enter for default>:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ribbon-tab =
|
|||||||
.draw = Desenhar
|
.draw = Desenhar
|
||||||
.annotate = Anotar
|
.annotate = Anotar
|
||||||
.insert = Inserir
|
.insert = Inserir
|
||||||
.model = Modelo
|
.model = Modelagem
|
||||||
.layout = Layout
|
.layout = Layout
|
||||||
.manage = Gerenciar
|
.manage = Gerenciar
|
||||||
.view = Exibir
|
.view = Exibir
|
||||||
@@ -2035,6 +2035,15 @@ insert =
|
|||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ objetos recortados para a área de transferência.
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ objetos recortados para a área de transferência.
|
||||||
|
|
||||||
model =
|
model =
|
||||||
|
.create = Criar
|
||||||
|
.boolean = Operações booleanas
|
||||||
|
.pyramid = Pirâmide
|
||||||
|
.polysolid = Polissólido
|
||||||
|
.extrude = Extrusão
|
||||||
|
.revolve = Revolução
|
||||||
|
.loft = Loft
|
||||||
|
.sweep = Varredura
|
||||||
|
.presspull = Pressionar/puxar
|
||||||
.n-specify-center-point = __ocs_arg_n__ Especifique o ponto central:
|
.n-specify-center-point = __ocs_arg_n__ Especifique o ponto central:
|
||||||
.n-specify-first-corner = __ocs_arg_n__ Especifique o primeiro canto:
|
.n-specify-first-corner = __ocs_arg_n__ Especifique o primeiro canto:
|
||||||
.n-specify-height-enter-for-default = __ocs_arg_n__ Especifique a altura <Enter para o padrão>:
|
.n-specify-height-enter-for-default = __ocs_arg_n__ Especifique a altura <Enter para o padrão>:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ribbon-tab =
|
|||||||
.draw = Начертить
|
.draw = Начертить
|
||||||
.annotate = Аннотации
|
.annotate = Аннотации
|
||||||
.insert = Вставка
|
.insert = Вставка
|
||||||
.model = Модель
|
.model = Моделирование
|
||||||
.layout = Расположение
|
.layout = Расположение
|
||||||
.manage = Управление
|
.manage = Управление
|
||||||
.view = Просмотр
|
.view = Просмотр
|
||||||
@@ -2038,6 +2038,15 @@ insert =
|
|||||||
.object-s-cut-to-clipboard = В буфер обмена вырезано объектов: __ocs_fmt_0__.
|
.object-s-cut-to-clipboard = В буфер обмена вырезано объектов: __ocs_fmt_0__.
|
||||||
|
|
||||||
model =
|
model =
|
||||||
|
.create = Создание
|
||||||
|
.boolean = Булевы операции
|
||||||
|
.pyramid = Пирамида
|
||||||
|
.polysolid = Политело
|
||||||
|
.extrude = Выдавливание
|
||||||
|
.revolve = Вращение
|
||||||
|
.loft = Лофт
|
||||||
|
.sweep = Сдвиг по траектории
|
||||||
|
.presspull = Нажать/вытянуть
|
||||||
.n-specify-center-point = __ocs_arg_n__ Укажите центральную точку:
|
.n-specify-center-point = __ocs_arg_n__ Укажите центральную точку:
|
||||||
.n-specify-first-corner = __ocs_arg_n__ Укажите первый угол:
|
.n-specify-first-corner = __ocs_arg_n__ Укажите первый угол:
|
||||||
.n-specify-height-enter-for-default = __ocs_arg_n__ Укажите высоту <Введите по умолчанию>:
|
.n-specify-height-enter-for-default = __ocs_arg_n__ Укажите высоту <Введите по умолчанию>:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ribbon-tab =
|
|||||||
.draw = Çizim
|
.draw = Çizim
|
||||||
.annotate = Açıklama
|
.annotate = Açıklama
|
||||||
.insert = Ekle
|
.insert = Ekle
|
||||||
.model = Model
|
.model = Modelleme
|
||||||
.layout = Yerleşim
|
.layout = Yerleşim
|
||||||
.manage = Yönet
|
.manage = Yönet
|
||||||
.view = Görünüm
|
.view = Görünüm
|
||||||
@@ -2023,6 +2023,15 @@ insert =
|
|||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ nesne panoya kesildi.
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ nesne panoya kesildi.
|
||||||
|
|
||||||
model =
|
model =
|
||||||
|
.create = Oluştur
|
||||||
|
.boolean = Boole işlemleri
|
||||||
|
.pyramid = Piramit
|
||||||
|
.polysolid = Çoklu katı
|
||||||
|
.extrude = Katılaştır
|
||||||
|
.revolve = Döndür
|
||||||
|
.loft = Kesitler arası
|
||||||
|
.sweep = Yol boyunca süpür
|
||||||
|
.presspull = İt/Çek
|
||||||
.n-specify-center-point = __ocs_arg_n__ Merkez noktasını belirtin:
|
.n-specify-center-point = __ocs_arg_n__ Merkez noktasını belirtin:
|
||||||
.n-specify-first-corner = __ocs_arg_n__ İlk köşeyi belirtin:
|
.n-specify-first-corner = __ocs_arg_n__ İlk köşeyi belirtin:
|
||||||
.n-specify-height-enter-for-default = __ocs_arg_n__ Yüksekliği belirtin <varsayılan için Enter>:
|
.n-specify-height-enter-for-default = __ocs_arg_n__ Yüksekliği belirtin <varsayılan için Enter>:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ribbon-tab =
|
|||||||
.draw = 绘制
|
.draw = 绘制
|
||||||
.annotate = 注释
|
.annotate = 注释
|
||||||
.insert = 插入
|
.insert = 插入
|
||||||
.model = 模型
|
.model = 建模
|
||||||
.layout = 布局
|
.layout = 布局
|
||||||
.manage = 管理
|
.manage = 管理
|
||||||
.view = 视图
|
.view = 视图
|
||||||
@@ -2018,6 +2018,15 @@ insert =
|
|||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ 对象切入剪贴板。
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ 对象切入剪贴板。
|
||||||
|
|
||||||
model =
|
model =
|
||||||
|
.create = 创建
|
||||||
|
.boolean = 布尔运算
|
||||||
|
.pyramid = 棱锥
|
||||||
|
.polysolid = 多段体
|
||||||
|
.extrude = 拉伸
|
||||||
|
.revolve = 旋转
|
||||||
|
.loft = 放样
|
||||||
|
.sweep = 扫掠
|
||||||
|
.presspull = 按拉
|
||||||
.n-specify-center-point = __ocs_arg_n__ 指定中心点:
|
.n-specify-center-point = __ocs_arg_n__ 指定中心点:
|
||||||
.n-specify-first-corner = __ocs_arg_n__ 指定第一个角点:
|
.n-specify-first-corner = __ocs_arg_n__ 指定第一个角点:
|
||||||
.n-specify-height-enter-for-default = __ocs_arg_n__ 指定高度(按 Enter 使用默认值):
|
.n-specify-height-enter-for-default = __ocs_arg_n__ 指定高度(按 Enter 使用默认值):
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ ribbon-tab =
|
|||||||
.draw = 繪製
|
.draw = 繪製
|
||||||
.annotate = 註解
|
.annotate = 註解
|
||||||
.insert = 插入
|
.insert = 插入
|
||||||
.model = 模型
|
.model = 建模
|
||||||
.layout = 配置
|
.layout = 配置
|
||||||
.manage = 管理
|
.manage = 管理
|
||||||
.view = 視圖
|
.view = 視圖
|
||||||
@@ -2030,6 +2030,15 @@ insert =
|
|||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ 物件切入剪貼簿。
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ 物件切入剪貼簿。
|
||||||
|
|
||||||
model =
|
model =
|
||||||
|
.create = 建立
|
||||||
|
.boolean = 布林運算
|
||||||
|
.pyramid = 角錐體
|
||||||
|
.polysolid = 多段實體
|
||||||
|
.extrude = 擠出
|
||||||
|
.revolve = 旋轉
|
||||||
|
.loft = 放樣
|
||||||
|
.sweep = 掃掠
|
||||||
|
.presspull = 按拉
|
||||||
.n-specify-center-point = __ocs_arg_n__ 指定中心點:
|
.n-specify-center-point = __ocs_arg_n__ 指定中心點:
|
||||||
.n-specify-first-corner = __ocs_arg_n__ 指定第一個角點:
|
.n-specify-first-corner = __ocs_arg_n__ 指定第一個角點:
|
||||||
.n-specify-height-enter-for-default = __ocs_arg_n__ 指定高度(按 Enter 使用預設值):
|
.n-specify-height-enter-for-default = __ocs_arg_n__ 指定高度(按 Enter 使用預設值):
|
||||||
|
|||||||
@@ -1204,15 +1204,17 @@ impl OpenCADStudio {
|
|||||||
} => {
|
} => {
|
||||||
let label = self.history_label_from_active_cmd(i, "SOLID");
|
let label = self.history_label_from_active_cmd(i, "SOLID");
|
||||||
let pending = self.begin_undo(i, label, 1, true);
|
let pending = self.begin_undo(i, label, 1, true);
|
||||||
self.add_solid_model(entity, *solid, history);
|
let handle = self.add_solid_model(entity, *solid, history);
|
||||||
self.tabs[i].dirty = true;
|
if !handle.is_null() {
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
if let Some(pd) = pending {
|
||||||
|
self.commit_undo_delta(i, pd);
|
||||||
|
}
|
||||||
|
}
|
||||||
self.tabs[i].scene.clear_preview_wire();
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
self.restore_pre_cmd_tangent();
|
self.restore_pre_cmd_tangent();
|
||||||
if let Some(pd) = pending {
|
|
||||||
self.commit_undo_delta(i, pd);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
CmdResult::CommitAndEditText(entity) => {
|
CmdResult::CommitAndEditText(entity) => {
|
||||||
let label = self.history_label_from_active_cmd(i, "ENTITY");
|
let label = self.history_label_from_active_cmd(i, "ENTITY");
|
||||||
@@ -3001,80 +3003,32 @@ impl OpenCADStudio {
|
|||||||
self.commit_undo_delta(i, pending);
|
self.commit_undo_delta(i, pending);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// ── Solid3D creation (BOX / SPHERE / CYLINDER) ────────────────
|
|
||||||
CmdResult::CommitSolid3D { mesh_fn } => {
|
|
||||||
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
|
||||||
let pending = self.begin_undo(i, "SOLID3D", 1, true);
|
|
||||||
let entity = empty_solid3d();
|
|
||||||
let handle = self.tabs[i].scene.add_entity(entity);
|
|
||||||
if !handle.is_null() {
|
|
||||||
let name = format!("{}", handle.value());
|
|
||||||
let color = [0.6f32, 0.6, 0.8, 1.0]; // default colour; command embedded it
|
|
||||||
let _ = color; // color is captured inside mesh_fn
|
|
||||||
if let Some(mesh) = mesh_fn(name) {
|
|
||||||
let set = crate::scene::MeshLodSet::from_single(mesh);
|
|
||||||
if let Some(acadrust::EntityType::Solid3D(entity)) =
|
|
||||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
|
||||||
{
|
|
||||||
let center = set.metrics.centroid;
|
|
||||||
entity.point_of_reference = acadrust::types::Vector3::new(
|
|
||||||
center[0], center[1], center[2],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
self.tabs[i].scene.meshes.insert(handle, set);
|
|
||||||
}
|
|
||||||
self.tabs[i].dirty = true;
|
|
||||||
self.command_line.push_output(crate::t!("Solid created.").as_ref());
|
|
||||||
}
|
|
||||||
if let Some(pd) = pending {
|
|
||||||
self.commit_undo_delta(i, pd);
|
|
||||||
}
|
|
||||||
self.tabs[i].active_cmd = None;
|
|
||||||
self.tabs[i].snap_result = None;
|
|
||||||
self.tabs[i].scene.clear_preview_wire();
|
|
||||||
self.restore_pre_cmd_tangent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── EXTRUDE ────────────────────────────────────────────────────
|
// ── EXTRUDE ────────────────────────────────────────────────────
|
||||||
CmdResult::ExtrudeEntity {
|
CmdResult::ExtrudeEntity {
|
||||||
handle,
|
handle,
|
||||||
height,
|
height,
|
||||||
color,
|
color: _,
|
||||||
} => {
|
} => {
|
||||||
if self.reject_locked_edit(i, handle) {
|
if self.reject_locked_edit(i, handle) {
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
||||||
use crate::scene::model::{solid_model, sweep_model};
|
use crate::scene::model::sweep_model;
|
||||||
|
|
||||||
let entity_opt = self.tabs[i].scene.document.get_entity(handle).cloned();
|
let entity_opt = self.tabs[i].scene.document.get_entity(handle).cloned();
|
||||||
if let Some(entity) = entity_opt {
|
if let Some(entity) = entity_opt {
|
||||||
// The kernel sweeps the profile into analytic surfaces —
|
if let Some(solid) = sweep_model::extruded(&entity, height) {
|
||||||
// a straight run becomes a plane and an arc a cylinder —
|
let history = crate::scene::model::solid_history::brep_op(&solid);
|
||||||
// so the solid saves as exact ACIS rather than facets.
|
|
||||||
let result = sweep_model::extruded(&entity, height as f64)
|
|
||||||
.and_then(|body| Some((solid_model::mesh_from_solid(&body, color)?, body)));
|
|
||||||
if let Some((mesh, solid)) = result {
|
|
||||||
let history = crate::scene::model::solid_history::extrusion_op(
|
|
||||||
&entity,
|
|
||||||
height as f64,
|
|
||||||
);
|
|
||||||
let pending = self.begin_undo(i, "EXTRUDE", 1, true);
|
let pending = self.begin_undo(i, "EXTRUDE", 1, true);
|
||||||
let mut s3d = empty_solid3d();
|
let created = self.add_solid_model(empty_solid3d(), solid, history);
|
||||||
if let acadrust::EntityType::Solid3D(inner) = &mut s3d {
|
if !created.is_null() {
|
||||||
inner.wires = solid_model::edge_wires(&solid);
|
self.tabs[i].dirty = true;
|
||||||
}
|
self.command_line
|
||||||
let new_handle = self.tabs[i].scene.add_entity(s3d);
|
.push_output(crate::t!("EXTRUDE: solid created.").as_ref());
|
||||||
self.tabs[i]
|
if let Some(pd) = pending {
|
||||||
.scene
|
self.commit_undo_delta(i, pd);
|
||||||
.create_solid_history(new_handle, history);
|
}
|
||||||
self.tabs[i].scene.register_solid_model(new_handle, solid);
|
|
||||||
let _ = mesh;
|
|
||||||
self.tabs[i].dirty = true;
|
|
||||||
self.command_line.push_output(crate::t!("EXTRUDE: solid created.").as_ref());
|
|
||||||
if let Some(pd) = pending {
|
|
||||||
self.commit_undo_delta(i, pd);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.command_line.push_error(crate::t!("EXTRUDE: could not build profile. Select a closed 2D entity (Circle, LwPolyline, etc.).").as_ref());
|
self.command_line.push_error(crate::t!("EXTRUDE: could not build profile. Select a closed 2D entity (Circle, LwPolyline, etc.).").as_ref());
|
||||||
@@ -3088,26 +3042,80 @@ impl OpenCADStudio {
|
|||||||
self.restore_pre_cmd_tangent();
|
self.restore_pre_cmd_tangent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CmdResult::PresspullEntity {
|
||||||
|
handle,
|
||||||
|
pick,
|
||||||
|
distance,
|
||||||
|
drag,
|
||||||
|
color,
|
||||||
|
} => {
|
||||||
|
if matches!(
|
||||||
|
self.tabs[i].scene.document.get_entity(handle),
|
||||||
|
Some(acadrust::EntityType::Solid3D(_))
|
||||||
|
) {
|
||||||
|
let task = self.solid_face_presspull(handle, pick, distance, drag);
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
self.restore_pre_cmd_tangent();
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
if self.reject_locked_edit(i, handle) {
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
||||||
|
use crate::scene::model::{solid_history, sweep_model};
|
||||||
|
|
||||||
|
let result = self.tabs[i].scene.document.get_entity(handle).and_then(|entity| {
|
||||||
|
let distance = match drag {
|
||||||
|
Some(point) => sweep_model::projected_drag(entity, pick, point)?,
|
||||||
|
None => distance,
|
||||||
|
};
|
||||||
|
sweep_model::extruded(entity, distance)
|
||||||
|
});
|
||||||
|
if let Some(solid) = result {
|
||||||
|
let pending = self.begin_undo(i, "PRESSPULL", 1, true);
|
||||||
|
let history = solid_history::brep_op(&solid);
|
||||||
|
let created = self.add_solid_model(empty_solid3d(), solid, history);
|
||||||
|
let _ = color;
|
||||||
|
if !created.is_null() {
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.command_line
|
||||||
|
.push_output(crate::t!("PRESSPULL: solid created.").as_ref());
|
||||||
|
if let Some(delta) = pending {
|
||||||
|
self.commit_undo_delta(i, delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.command_line.push_error(
|
||||||
|
crate::t!("PRESSPULL: select a closed profile or planar solid face.")
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
self.restore_pre_cmd_tangent();
|
||||||
|
}
|
||||||
|
|
||||||
// ── REVOLVE ────────────────────────────────────────────────────
|
// ── REVOLVE ────────────────────────────────────────────────────
|
||||||
CmdResult::RevolveEntity {
|
CmdResult::RevolveEntity {
|
||||||
handle,
|
handle,
|
||||||
axis_start,
|
axis_start,
|
||||||
axis_end,
|
axis_end,
|
||||||
angle_deg,
|
angle_deg,
|
||||||
color,
|
color: _,
|
||||||
} => {
|
} => {
|
||||||
if self.reject_locked_edit(i, handle) {
|
if self.reject_locked_edit(i, handle) {
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
||||||
use crate::scene::model::{solid_model, sweep_model};
|
use crate::scene::model::sweep_model;
|
||||||
|
|
||||||
let entity_opt = self.tabs[i].scene.document.get_entity(handle).cloned();
|
let entity_opt = self.tabs[i].scene.document.get_entity(handle).cloned();
|
||||||
if let Some(entity) = entity_opt {
|
if let Some(entity) = entity_opt {
|
||||||
// A line turned about the axis sweeps into a plane, a
|
|
||||||
// cylinder or a cone, and an arc into a sphere or a
|
|
||||||
// torus, so a revolved solid keeps exact geometry too.
|
|
||||||
let result = sweep_model::revolved(
|
let result = sweep_model::revolved(
|
||||||
&entity,
|
&entity,
|
||||||
[
|
[
|
||||||
@@ -3117,31 +3125,19 @@ impl OpenCADStudio {
|
|||||||
],
|
],
|
||||||
[axis_end.x as f64, axis_end.y as f64, axis_end.z as f64],
|
[axis_end.x as f64, axis_end.y as f64, axis_end.z as f64],
|
||||||
(angle_deg as f64).to_radians(),
|
(angle_deg as f64).to_radians(),
|
||||||
)
|
);
|
||||||
.and_then(|body| Some((solid_model::mesh_from_solid(&body, color)?, body)));
|
if let Some(solid) = result {
|
||||||
if let Some((mesh, solid)) = result {
|
let history = crate::scene::model::solid_history::brep_op(&solid);
|
||||||
let history = crate::scene::model::solid_history::revolve_op(
|
|
||||||
&entity,
|
|
||||||
axis_start.to_array(),
|
|
||||||
axis_end.to_array(),
|
|
||||||
(angle_deg as f64).to_radians(),
|
|
||||||
);
|
|
||||||
let pending = self.begin_undo(i, "REVOLVE", 1, true);
|
let pending = self.begin_undo(i, "REVOLVE", 1, true);
|
||||||
let mut s3d = empty_solid3d();
|
let created = self.add_solid_model(empty_solid3d(), solid, history);
|
||||||
if let acadrust::EntityType::Solid3D(inner) = &mut s3d {
|
if !created.is_null() {
|
||||||
inner.wires = solid_model::edge_wires(&solid);
|
self.tabs[i].dirty = true;
|
||||||
}
|
self.command_line.push_output(
|
||||||
let new_handle = self.tabs[i].scene.add_entity(s3d);
|
crate::tf!("REVOLVE: solid created ({:.0}°).", angle_deg).as_ref(),
|
||||||
self.tabs[i]
|
);
|
||||||
.scene
|
if let Some(pd) = pending {
|
||||||
.create_solid_history(new_handle, history);
|
self.commit_undo_delta(i, pd);
|
||||||
self.tabs[i].scene.register_solid_model(new_handle, solid);
|
}
|
||||||
let _ = mesh;
|
|
||||||
self.tabs[i].dirty = true;
|
|
||||||
self.command_line
|
|
||||||
.push_output(crate::tf!("REVOLVE: solid created ({:.0}°).", angle_deg).as_ref());
|
|
||||||
if let Some(pd) = pending {
|
|
||||||
self.commit_undo_delta(i, pd);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.command_line
|
self.command_line
|
||||||
@@ -3160,7 +3156,7 @@ impl OpenCADStudio {
|
|||||||
CmdResult::SweepEntity {
|
CmdResult::SweepEntity {
|
||||||
profile_handle,
|
profile_handle,
|
||||||
path_handle,
|
path_handle,
|
||||||
color,
|
color: _,
|
||||||
} => {
|
} => {
|
||||||
if self.reject_locked_edit(i, profile_handle)
|
if self.reject_locked_edit(i, profile_handle)
|
||||||
|| self.reject_locked_edit(i, path_handle)
|
|| self.reject_locked_edit(i, path_handle)
|
||||||
@@ -3169,7 +3165,7 @@ impl OpenCADStudio {
|
|||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
||||||
use crate::scene::model::sweep_model;
|
use crate::scene::model::{solid_history, sweep_model};
|
||||||
|
|
||||||
let profile_ent = self.tabs[i]
|
let profile_ent = self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
@@ -3177,39 +3173,21 @@ impl OpenCADStudio {
|
|||||||
.get_entity(profile_handle)
|
.get_entity(profile_handle)
|
||||||
.cloned();
|
.cloned();
|
||||||
let path_ent = self.tabs[i].scene.document.get_entity(path_handle).cloned();
|
let path_ent = self.tabs[i].scene.document.get_entity(path_handle).cloned();
|
||||||
let history = profile_ent
|
|
||||||
.as_ref()
|
|
||||||
.zip(path_ent.as_ref())
|
|
||||||
.map(|(profile, path)| {
|
|
||||||
crate::scene::model::solid_history::sweep_op(profile, path)
|
|
||||||
});
|
|
||||||
let result = profile_ent
|
let result = profile_ent
|
||||||
.zip(path_ent)
|
.zip(path_ent)
|
||||||
.and_then(|(profile, path)| sweep_model::swept(&profile, &path, color));
|
.and_then(|(profile, path)| sweep_model::swept(&profile, &path));
|
||||||
|
|
||||||
if let Some(mut set) = result {
|
if let Some(solid) = result {
|
||||||
let pending = self.begin_undo(i, "SWEEP", 1, true);
|
let pending = self.begin_undo(i, "SWEEP", 1, true);
|
||||||
let mut entity = empty_solid3d();
|
let history = solid_history::brep_op(&solid);
|
||||||
if let acadrust::EntityType::Solid3D(solid) = &mut entity {
|
let created = self.add_solid_model(empty_solid3d(), solid, history);
|
||||||
let center = set.metrics.centroid;
|
if !created.is_null() {
|
||||||
solid.point_of_reference = acadrust::types::Vector3::new(
|
self.tabs[i].dirty = true;
|
||||||
center[0], center[1], center[2],
|
self.command_line
|
||||||
);
|
.push_output(crate::t!("SWEEP: solid created.").as_ref());
|
||||||
}
|
if let Some(pd) = pending {
|
||||||
let new_handle = self.tabs[i].scene.add_entity(entity);
|
self.commit_undo_delta(i, pd);
|
||||||
if let Some(history) = history {
|
}
|
||||||
self.tabs[i]
|
|
||||||
.scene
|
|
||||||
.create_solid_history(new_handle, history);
|
|
||||||
}
|
|
||||||
for mesh in &mut set.lods {
|
|
||||||
mesh.name = format!("{}", new_handle.value());
|
|
||||||
}
|
|
||||||
self.tabs[i].scene.meshes.insert(new_handle, set);
|
|
||||||
self.tabs[i].dirty = true;
|
|
||||||
self.command_line.push_output(crate::t!("SWEEP: solid created.").as_ref());
|
|
||||||
if let Some(pd) = pending {
|
|
||||||
self.commit_undo_delta(i, pd);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.command_line.push_error(crate::t!("SWEEP: could not sweep the profile along the path.").as_ref());
|
self.command_line.push_error(crate::t!("SWEEP: could not sweep the profile along the path.").as_ref());
|
||||||
@@ -3221,7 +3199,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── LOFT ──────────────────────────────────────────────────────
|
// ── LOFT ──────────────────────────────────────────────────────
|
||||||
CmdResult::LoftEntities { handles, color } => {
|
CmdResult::LoftEntities { handles, color: _ } => {
|
||||||
if let Some(handle) = handles
|
if let Some(handle) = handles
|
||||||
.iter()
|
.iter()
|
||||||
.find(|handle| self.tabs[i].scene.is_layer_locked(**handle))
|
.find(|handle| self.tabs[i].scene.is_layer_locked(**handle))
|
||||||
@@ -3232,34 +3210,23 @@ impl OpenCADStudio {
|
|||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
||||||
use crate::scene::model::sweep_model;
|
use crate::scene::model::{solid_history, sweep_model};
|
||||||
|
|
||||||
let profiles: Vec<acadrust::EntityType> = handles
|
let profiles: Vec<acadrust::EntityType> = handles
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|handle| self.tabs[i].scene.document.get_entity(*handle).cloned())
|
.filter_map(|handle| self.tabs[i].scene.document.get_entity(*handle).cloned())
|
||||||
.collect();
|
.collect();
|
||||||
if let Some(mut set) = sweep_model::lofted(&profiles, color) {
|
if let Some(solid) = sweep_model::lofted(&profiles) {
|
||||||
let history = crate::scene::model::solid_history::loft_op(&profiles);
|
|
||||||
let pending = self.begin_undo(i, "LOFT", 1, true);
|
let pending = self.begin_undo(i, "LOFT", 1, true);
|
||||||
let mut entity = empty_solid3d();
|
let history = solid_history::brep_op(&solid);
|
||||||
if let acadrust::EntityType::Solid3D(solid) = &mut entity {
|
let created = self.add_solid_model(empty_solid3d(), solid, history);
|
||||||
let center = set.metrics.centroid;
|
if !created.is_null() {
|
||||||
solid.point_of_reference = acadrust::types::Vector3::new(
|
self.tabs[i].dirty = true;
|
||||||
center[0], center[1], center[2],
|
self.command_line
|
||||||
);
|
.push_output(crate::t!("LOFT: solid created.").as_ref());
|
||||||
}
|
if let Some(pd) = pending {
|
||||||
let new_handle = self.tabs[i].scene.add_entity(entity);
|
self.commit_undo_delta(i, pd);
|
||||||
self.tabs[i]
|
}
|
||||||
.scene
|
|
||||||
.create_solid_history(new_handle, history);
|
|
||||||
for mesh in &mut set.lods {
|
|
||||||
mesh.name = format!("{}", new_handle.value());
|
|
||||||
}
|
|
||||||
self.tabs[i].scene.meshes.insert(new_handle, set);
|
|
||||||
self.tabs[i].dirty = true;
|
|
||||||
self.command_line.push_output(crate::t!("LOFT: solid created.").as_ref());
|
|
||||||
if let Some(pd) = pending {
|
|
||||||
self.commit_undo_delta(i, pd);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.command_line.push_error(crate::t!("LOFT: select at least two closed profiles.").as_ref());
|
self.command_line.push_error(crate::t!("LOFT: select at least two closed profiles.").as_ref());
|
||||||
@@ -3270,6 +3237,20 @@ impl OpenCADStudio {
|
|||||||
self.restore_pre_cmd_tangent();
|
self.restore_pre_cmd_tangent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CmdResult::SolidEdgeBlend {
|
||||||
|
handle,
|
||||||
|
pick,
|
||||||
|
value,
|
||||||
|
fillet,
|
||||||
|
} => {
|
||||||
|
let task = self.solid_edge_blend(handle, pick, value, fillet);
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
self.restore_pre_cmd_tangent();
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
CmdResult::HatcheditApply {
|
CmdResult::HatcheditApply {
|
||||||
handle,
|
handle,
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option<acadrust::Handle> {
|
||||||
|
let selected = app.tabs[tab].scene.selected_handles_in_order();
|
||||||
|
(selected.len() == 1
|
||||||
|
&& matches!(
|
||||||
|
app.tabs[tab].scene.document.get_entity(selected[0]),
|
||||||
|
Some(acadrust::EntityType::Solid3D(_))
|
||||||
|
))
|
||||||
|
.then_some(selected[0])
|
||||||
|
}
|
||||||
|
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
pub(super) fn dispatch_dim(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
pub(super) fn dispatch_dim(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||||
match cmd {
|
match cmd {
|
||||||
@@ -950,6 +960,20 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"SOLIDFILLET" => {
|
||||||
|
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand};
|
||||||
|
let command = SolidEdgeCommand::new(EdgeOperation::Fillet, selected_solid(self, i));
|
||||||
|
self.command_line.push_info(&command.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||||
|
}
|
||||||
|
|
||||||
|
"FILLET" if selected_solid(self, i).is_some() => {
|
||||||
|
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand};
|
||||||
|
let command = SolidEdgeCommand::new(EdgeOperation::Fillet, selected_solid(self, i));
|
||||||
|
self.command_line.push_info(&command.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||||
|
}
|
||||||
|
|
||||||
"FILLET" => {
|
"FILLET" => {
|
||||||
use crate::modules::draw::modify::fillet::FilletCommand;
|
use crate::modules::draw::modify::fillet::FilletCommand;
|
||||||
let entities: Vec<_> = self.tabs[i]
|
let entities: Vec<_> = self.tabs[i]
|
||||||
@@ -1083,6 +1107,20 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"SOLIDCHAMFER" => {
|
||||||
|
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand};
|
||||||
|
let command = SolidEdgeCommand::new(EdgeOperation::Chamfer, selected_solid(self, i));
|
||||||
|
self.command_line.push_info(&command.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||||
|
}
|
||||||
|
|
||||||
|
"CHAMFER" if selected_solid(self, i).is_some() => {
|
||||||
|
use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand};
|
||||||
|
let command = SolidEdgeCommand::new(EdgeOperation::Chamfer, selected_solid(self, i));
|
||||||
|
self.command_line.push_info(&command.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||||
|
}
|
||||||
|
|
||||||
"CHAMFER" => {
|
"CHAMFER" => {
|
||||||
use crate::modules::draw::modify::fillet::ChamferCommand;
|
use crate::modules::draw::modify::fillet::ChamferCommand;
|
||||||
let entities: Vec<_> = self.tabs[i]
|
let entities: Vec<_> = self.tabs[i]
|
||||||
|
|||||||
@@ -466,31 +466,55 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BOX / SPHERE / CYLINDER / CONE / WEDGE / TORUS are handled by the
|
|
||||||
// Model-tab primitive command above (with the kernel boolean caching).
|
|
||||||
|
|
||||||
// ── EXTRUDE ────────────────────────────────────────────────────
|
// ── EXTRUDE ────────────────────────────────────────────────────
|
||||||
// PRESSPULL on a closed boundary creates a solid by extruding it to a
|
"EXTRUDE" | "THICKEN" => {
|
||||||
// height — the same operation as EXTRUDE. THICKEN turns a closed planar
|
|
||||||
// profile into a solid of the given thickness, which is also an extrude.
|
|
||||||
"EXTRUDE" | "PRESSPULL" | "THICKEN" => {
|
|
||||||
use crate::modules::insert::solid3d_cmds::ExtrudeCommand;
|
use crate::modules::insert::solid3d_cmds::ExtrudeCommand;
|
||||||
// If a single entity is already selected, skip the pick step.
|
// If a single entity is already selected, skip the pick step.
|
||||||
let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect();
|
let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect();
|
||||||
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
|
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
|
||||||
if selected.len() == 1 {
|
if selected.len() == 1 {
|
||||||
let handle = selected[0].0;
|
let handle = selected[0].0;
|
||||||
let mut cmd = ExtrudeCommand::new(color);
|
let mut cmd = ExtrudeCommand::new_named(cmd, color);
|
||||||
cmd.on_entity_pick(handle, glam::DVec3::ZERO);
|
if let Some(curve) = crate::entities::curve::entity_curve(selected[0].1) {
|
||||||
|
cmd.set_entity_pick_direction(
|
||||||
|
curve.plane.normal().map(glam::DVec3::from_array),
|
||||||
|
);
|
||||||
|
cmd.on_entity_pick(
|
||||||
|
handle,
|
||||||
|
glam::DVec3::from_array(curve.plane.origin),
|
||||||
|
);
|
||||||
|
}
|
||||||
self.command_line.push_info(&cmd.prompt());
|
self.command_line.push_info(&cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||||
} else {
|
} else {
|
||||||
let cmd = ExtrudeCommand::new(color);
|
let cmd = ExtrudeCommand::new_named(cmd, color);
|
||||||
self.command_line.push_info(&cmd.prompt());
|
self.command_line.push_info(&cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"PRESSPULL" => {
|
||||||
|
use crate::modules::insert::solid3d_cmds::PresspullCommand;
|
||||||
|
let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect();
|
||||||
|
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
|
||||||
|
let mut command = PresspullCommand::new(color);
|
||||||
|
if selected.len() == 1
|
||||||
|
&& !matches!(selected[0].1, acadrust::EntityType::Solid3D(_))
|
||||||
|
{
|
||||||
|
if let Some(curve) = crate::entities::curve::entity_curve(selected[0].1) {
|
||||||
|
command.set_entity_pick_direction(
|
||||||
|
curve.plane.normal().map(glam::DVec3::from_array),
|
||||||
|
);
|
||||||
|
command.on_entity_pick(
|
||||||
|
selected[0].0,
|
||||||
|
glam::DVec3::from_array(curve.plane.origin),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.command_line.push_info(&command.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||||
|
}
|
||||||
|
|
||||||
// ── REVOLVE ────────────────────────────────────────────────────
|
// ── REVOLVE ────────────────────────────────────────────────────
|
||||||
"REVOLVE" => {
|
"REVOLVE" => {
|
||||||
use crate::modules::insert::solid3d_cmds::RevolveCommand;
|
use crate::modules::insert::solid3d_cmds::RevolveCommand;
|
||||||
|
|||||||
@@ -925,14 +925,15 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Model commands (3D primitives) ─────────────────────────────
|
// ── Model commands (3D primitives) ─────────────────────────────
|
||||||
"BOX" | "WEDGE" | "CYLINDER" | "CONE" | "SPHERE" | "TORUS" => {
|
"BOX" | "WEDGE" | "CYLINDER" | "CONE" | "SPHERE" | "PYRAMID" | "PYR"
|
||||||
|
| "TORUS" => {
|
||||||
use crate::modules::model::primitive_cmd::PrimitiveCommand;
|
use crate::modules::model::primitive_cmd::PrimitiveCommand;
|
||||||
let new_cmd = PrimitiveCommand::new(cmd);
|
let new_cmd = PrimitiveCommand::new(cmd);
|
||||||
self.command_line.push_info(&new_cmd.prompt());
|
self.command_line.push_info(&new_cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Design commands (solid booleans) ───────────────────────────
|
// ── Solid booleans ─────────────────────────────────────────────
|
||||||
"UNION" | "SUBTRACT" | "INTERSECT" => {
|
"UNION" | "SUBTRACT" | "INTERSECT" => {
|
||||||
use crate::modules::model::boolean_cmd::BoolOp;
|
use crate::modules::model::boolean_cmd::BoolOp;
|
||||||
if let Some(op) = BoolOp::from_id(cmd) {
|
if let Some(op) = BoolOp::from_id(cmd) {
|
||||||
@@ -1033,17 +1034,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PYRAMID <radius> <height> [sides] — create an n-sided pyramid mesh.
|
// PYRAMID <radius> <height> [sides] keeps the direct numeric form.
|
||||||
"PYRAMID" | "PYR" => {
|
|
||||||
use crate::command::TwoValuePromptCommand;
|
|
||||||
let c = TwoValuePromptCommand::new(
|
|
||||||
"PYRAMID",
|
|
||||||
"PYRAMID base radius:",
|
|
||||||
"PYRAMID height (add sides by typing a 3rd number):",
|
|
||||||
);
|
|
||||||
self.command_line.push_info(&c.prompt());
|
|
||||||
self.tabs[i].active_cmd = Some(Box::new(c));
|
|
||||||
}
|
|
||||||
cmd if cmd.starts_with("PYRAMID ") || cmd.starts_with("PYR ") => {
|
cmd if cmd.starts_with("PYRAMID ") || cmd.starts_with("PYR ") => {
|
||||||
let nums: Vec<f64> = cmd
|
let nums: Vec<f64> = cmd
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
// 3D solid modelling support on the App: committing Model-tab primitives,
|
// Kernel B-rep solid modelling and exact ACIS persistence.
|
||||||
// and the Design-group boolean operations over the scene's cached
|
|
||||||
// B-reps.
|
|
||||||
//
|
|
||||||
// The bodies come from the geometry kernel, so every operation here is on
|
|
||||||
// analytic surfaces rather than on facets — a turned cylinder is still a
|
|
||||||
// cylinder afterwards, and saves as one.
|
|
||||||
|
|
||||||
use acadrust::entities::Solid3D;
|
use acadrust::{
|
||||||
use acadrust::objects::SolidHistoryOperation;
|
entities::Solid3D, objects::SolidHistoryOperation, EntityType, Handle,
|
||||||
|
};
|
||||||
use cadkernel::brep::Body;
|
use cadkernel::brep::Body;
|
||||||
use acadrust::{EntityType, Handle};
|
|
||||||
use iced::Task;
|
use iced::Task;
|
||||||
|
|
||||||
use super::Message;
|
use super::Message;
|
||||||
@@ -18,16 +12,25 @@ use crate::scene::model::solid_history;
|
|||||||
use crate::scene::model::solid_model::{self, Bool};
|
use crate::scene::model::solid_model::{self, Bool};
|
||||||
|
|
||||||
impl super::OpenCADStudio {
|
impl super::OpenCADStudio {
|
||||||
/// Commit a Model-tab solid: add its acadrust entity to the document, then
|
/// Add a solid and register its persistent B-rep.
|
||||||
/// register the B-rep (caches it for booleans + tessellates it into the
|
|
||||||
/// shaded mesh pipeline). Returns the new entity handle.
|
|
||||||
pub(super) fn add_solid_model(
|
pub(super) fn add_solid_model(
|
||||||
&mut self,
|
&mut self,
|
||||||
entity: EntityType,
|
mut entity: EntityType,
|
||||||
solid: Body,
|
solid: Body,
|
||||||
history: SolidHistoryOperation,
|
history: SolidHistoryOperation,
|
||||||
) -> Handle {
|
) -> Handle {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
let EntityType::Solid3D(inner) = &mut entity else {
|
||||||
|
return Handle::NULL;
|
||||||
|
};
|
||||||
|
inner.wires = solid_model::edge_wires(&solid);
|
||||||
|
let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&solid)
|
||||||
|
else {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("The solid could not be encoded as ACIS.").as_ref());
|
||||||
|
return Handle::NULL;
|
||||||
|
};
|
||||||
|
inner.set_sat_document(&document);
|
||||||
let Some(handle) = self.commit_entity_handle(entity) else {
|
let Some(handle) = self.commit_entity_handle(entity) else {
|
||||||
return Handle::NULL;
|
return Handle::NULL;
|
||||||
};
|
};
|
||||||
@@ -55,35 +58,183 @@ impl super::OpenCADStudio {
|
|||||||
handles
|
handles
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run a boolean (`union` / `subtract` / `intersect`) on exactly two
|
fn replace_solid_body(&mut self, handle: Handle, result: Body, label: &str) -> bool {
|
||||||
/// selected solids whose B-reps can be restored by the kernel.
|
let i = self.active_tab;
|
||||||
|
let Some(document) =
|
||||||
|
crate::scene::convert::acis_export::solid_to_sat(&result)
|
||||||
|
else {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("The result could not be encoded as ACIS.").as_ref());
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(EntityType::Solid3D(mut entity)) =
|
||||||
|
self.tabs[i].scene.document.get_entity(handle).cloned()
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
entity.wires = solid_model::edge_wires(&result);
|
||||||
|
entity.silhouettes.clear();
|
||||||
|
entity.history_handle = None;
|
||||||
|
entity.set_sat_document(&document);
|
||||||
|
|
||||||
|
self.push_undo_snapshot(i, label);
|
||||||
|
self.tabs[i].scene.delete_solid_history(handle);
|
||||||
|
if !self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.update_entity(EntityType::Solid3D(entity))
|
||||||
|
{
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("The solid could not be updated.").as_ref());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let history = solid_history::brep_op(&result);
|
||||||
|
self.tabs[i].scene.create_solid_history(handle, history);
|
||||||
|
self.tabs[i].scene.register_solid_model(handle, result);
|
||||||
|
self.tabs[i].scene.deselect_all();
|
||||||
|
self.tabs[i].scene.select_entity(handle, false);
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.refresh_properties();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn solid_edge_blend(
|
||||||
|
&mut self,
|
||||||
|
handle: Handle,
|
||||||
|
pick: glam::DVec3,
|
||||||
|
value: f64,
|
||||||
|
fillet: bool,
|
||||||
|
) -> Task<Message> {
|
||||||
|
let i = self.active_tab;
|
||||||
|
if self.reject_locked_edit(i, handle) {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
if !matches!(
|
||||||
|
self.tabs[i].scene.document.get_entity(handle),
|
||||||
|
Some(EntityType::Solid3D(_))
|
||||||
|
) {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("Select a 3D solid edge.").as_ref());
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
self.tabs[i].scene.restore_solid_models(&[handle]);
|
||||||
|
let Some(body) = self.tabs[i].scene.solid_models.get(&handle).cloned() else {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("The solid geometry could not be restored.").as_ref());
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let Some(edge) = solid_model::nearest_edge(&body, pick.to_array()) else {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("Select a solid edge.").as_ref());
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let result = if fillet {
|
||||||
|
cadkernel::brep::fillet(&body, edge, value)
|
||||||
|
} else {
|
||||||
|
cadkernel::brep::chamfer(&body, edge, value)
|
||||||
|
};
|
||||||
|
let Some(result) = result else {
|
||||||
|
self.command_line.push_error(
|
||||||
|
crate::t!("Edge operation failed. Use a convex planar solid and a smaller value.")
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let label = if fillet { "SOLIDFILLET" } else { "SOLIDCHAMFER" };
|
||||||
|
if self.replace_solid_body(handle, result, label) {
|
||||||
|
self.command_line
|
||||||
|
.push_output(crate::tf!("{label}: solid updated.").as_ref());
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn solid_face_presspull(
|
||||||
|
&mut self,
|
||||||
|
handle: Handle,
|
||||||
|
pick: glam::DVec3,
|
||||||
|
distance: f64,
|
||||||
|
drag: Option<glam::DVec3>,
|
||||||
|
) -> Task<Message> {
|
||||||
|
let i = self.active_tab;
|
||||||
|
if self.reject_locked_edit(i, handle) {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
self.tabs[i].scene.restore_solid_models(&[handle]);
|
||||||
|
let Some(body) = self.tabs[i].scene.solid_models.get(&handle).cloned() else {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("The solid geometry could not be restored.").as_ref());
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let Some(face) = solid_model::nearest_planar_face(&body, pick.to_array()) else {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("Select a planar solid face.").as_ref());
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let distance = match drag {
|
||||||
|
Some(point) => {
|
||||||
|
let Some(normal) = solid_model::planar_face_normal(&body, face) else {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("Select a planar solid face.").as_ref());
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
(point - pick).dot(glam::DVec3::from_array(normal))
|
||||||
|
}
|
||||||
|
None => distance,
|
||||||
|
};
|
||||||
|
if !distance.is_finite() || distance.abs() <= 1e-6 {
|
||||||
|
self.command_line.push_error(
|
||||||
|
crate::t!("PRESSPULL: drag along the selected face normal.").as_ref(),
|
||||||
|
);
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
let Some(result) = cadkernel::brep::presspull(&body, face, distance) else {
|
||||||
|
self.command_line.push_error(
|
||||||
|
crate::t!("PRESSPULL: the face move would collapse or invalidate the solid.")
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
if self.replace_solid_body(handle, result, "PRESSPULL") {
|
||||||
|
self.command_line
|
||||||
|
.push_output(crate::t!("PRESSPULL: solid updated.").as_ref());
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a boolean over the selected solids in selection order.
|
||||||
pub(super) fn solid_boolean(&mut self, op: BoolOp) -> Task<Message> {
|
pub(super) fn solid_boolean(&mut self, op: BoolOp) -> Task<Message> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let handles = self.selected_solid_handles();
|
let handles = self.selected_solid_handles();
|
||||||
if handles.len() != 2 {
|
if handles.len() < 2 {
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_error(crate::t!("Boolean: select exactly two solids created this session.").as_ref());
|
.push_error(crate::t!("Boolean: select at least two solids.").as_ref());
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
let a = self.tabs[i].scene.solid_models[&handles[0]].clone();
|
|
||||||
let b = self.tabs[i].scene.solid_models[&handles[1]].clone();
|
|
||||||
let kind = match op {
|
let kind = match op {
|
||||||
BoolOp::Union => Bool::Union,
|
BoolOp::Union => Bool::Union,
|
||||||
BoolOp::Subtract => Bool::Subtract,
|
BoolOp::Subtract => Bool::Subtract,
|
||||||
BoolOp::Intersect => Bool::Intersect,
|
BoolOp::Intersect => Bool::Intersect,
|
||||||
};
|
};
|
||||||
let Some(result) = solid_model::boolean(kind, &a, &b) else {
|
let mut operands = handles
|
||||||
|
.iter()
|
||||||
|
.filter_map(|handle| self.tabs[i].scene.solid_models.get(handle).cloned());
|
||||||
|
let mut result = operands.next().expect("at least two restored solids");
|
||||||
|
for operand in operands {
|
||||||
|
let Some(combined) = solid_model::boolean(kind, &result, &operand) else {
|
||||||
|
self.command_line.push_error(
|
||||||
|
crate::t!("Boolean failed while combining the selected solids.").as_ref(),
|
||||||
|
);
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
result = combined;
|
||||||
|
}
|
||||||
|
if crate::scene::convert::acis_export::solid_to_sat(&result).is_none() {
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_error(crate::t!("Boolean failed — the solids may not overlap.").as_ref());
|
.push_error(crate::t!("The boolean result could not be encoded as ACIS.").as_ref());
|
||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
}
|
||||||
|
|
||||||
self.push_undo_snapshot(i, "BOOLEAN");
|
self.push_undo_snapshot(i, "BOOLEAN");
|
||||||
// Remove the two operands (entity + mesh + cached B-rep).
|
|
||||||
self.tabs[i].scene.erase_entities(&handles);
|
self.tabs[i].scene.erase_entities(&handles);
|
||||||
// The result is freshly combined geometry with no ACIS parametrisation,
|
|
||||||
// so it lives as a Solid3D whose render/boolean data is the injected
|
|
||||||
// mesh + cached B-rep; its edge wires make it pickable.
|
|
||||||
let mut s3d = Solid3D::new();
|
let mut s3d = Solid3D::new();
|
||||||
s3d.wires = solid_model::edge_wires(&result);
|
s3d.wires = solid_model::edge_wires(&result);
|
||||||
let history = solid_history::brep_op(&result);
|
let history = solid_history::brep_op(&result);
|
||||||
@@ -100,7 +251,7 @@ impl super::OpenCADStudio {
|
|||||||
/// Slice the one selected solid with an axis-aligned plane (axis 0/1/2 =
|
/// Slice the one selected solid with an axis-aligned plane (axis 0/1/2 =
|
||||||
/// X/Y/Z at `value`), keeping the lower side when `keep_low` is true. The
|
/// X/Y/Z at `value`), keeping the lower side when `keep_low` is true. The
|
||||||
/// kept half is the intersection of the solid with a half-space box, reusing
|
/// kept half is the intersection of the solid with a half-space box, reusing
|
||||||
/// the same boolean path the Design-group tools use.
|
/// the same boolean path as the modelling tools.
|
||||||
pub(super) fn solid_slice(&mut self, axis: usize, value: f64, keep_low: bool) -> Task<Message> {
|
pub(super) fn solid_slice(&mut self, axis: usize, value: f64, keep_low: bool) -> Task<Message> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let handles = self.selected_solid_handles();
|
let handles = self.selected_solid_handles();
|
||||||
@@ -243,63 +394,34 @@ impl super::OpenCADStudio {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// POLYSOLID — build a wall-like solid from a selected polyline: one box per
|
/// POLYSOLID — build a wall solid around an exact polyline offset.
|
||||||
/// segment (oriented along it, `width` wide, `height` tall) unioned together,
|
|
||||||
/// reusing the box primitive + the boolean union path.
|
|
||||||
pub(super) fn solid_polysolid(&mut self, width: f64, height: f64) -> Task<Message> {
|
pub(super) fn solid_polysolid(&mut self, width: f64, height: f64) -> Task<Message> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let found: Option<(Handle, Vec<[f64; 3]>)> = self.tabs[i]
|
let found: Option<(Handle, EntityType)> = self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.selected_entities()
|
.selected_entities()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(h, _)| !self.tabs[i].scene.is_layer_locked(*h))
|
.filter(|(h, _)| !self.tabs[i].scene.is_layer_locked(*h))
|
||||||
.find_map(|(h, e)| match e {
|
.find_map(|(h, e)| match e {
|
||||||
EntityType::LwPolyline(_) => crate::entities::curve::entity_curve(e)
|
EntityType::LwPolyline(_) => Some((*h, (*e).clone())),
|
||||||
.map(|curve| (*h, crate::entities::curve::curve_points(&curve))),
|
|
||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
let Some((handle, pts)) = found else {
|
let Some((handle, entity)) = found else {
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_error(crate::t!("POLYSOLID: select a polyline first.").as_ref());
|
.push_error(crate::t!("POLYSOLID: select a polyline first.").as_ref());
|
||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
let segs: Vec<([f64; 3], [f64; 3])> = pts.windows(2).map(|w| (w[0], w[1])).collect();
|
let Some(result) = crate::scene::model::sweep_model::polysolid(&entity, width, height)
|
||||||
let mut acc: Option<Body> = None;
|
else {
|
||||||
let mut used = 0usize;
|
|
||||||
for (a, b) in &segs {
|
|
||||||
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
|
|
||||||
let len = (dx * dx + dy * dy).sqrt();
|
|
||||||
if len < 1e-9 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let angle = dy.atan2(dx);
|
|
||||||
// Local box: X 0..len, Y -w/2..w/2, Z 0..h, then turned to lie
|
|
||||||
// along the segment and moved onto its start.
|
|
||||||
let (sin, cos) = angle.sin_cos();
|
|
||||||
let Some(bx) = solid_model::box_solid([len / 2.0, 0.0, height / 2.0], len, width, height)
|
|
||||||
.and_then(|bx| {
|
|
||||||
solid_model::placed(
|
|
||||||
&bx,
|
|
||||||
[cos, sin, 0.0],
|
|
||||||
[-sin, cos, 0.0],
|
|
||||||
[0.0, 0.0, 1.0],
|
|
||||||
[a[0], a[1], a[2]],
|
|
||||||
)
|
|
||||||
})
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
acc = Some(match acc {
|
|
||||||
None => bx,
|
|
||||||
Some(prev) => solid_model::boolean(Bool::Union, &prev, &bx).unwrap_or(prev),
|
|
||||||
});
|
|
||||||
used += 1;
|
|
||||||
}
|
|
||||||
let Some(result) = acc else {
|
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_error(crate::t!("POLYSOLID: the polyline has no usable segments.").as_ref());
|
.push_error(crate::t!("POLYSOLID: the kernel could not offset the polyline.").as_ref());
|
||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
|
if crate::scene::convert::acis_export::solid_to_sat(&result).is_none() {
|
||||||
|
self.command_line
|
||||||
|
.push_error(crate::t!("POLYSOLID: the result could not be encoded as ACIS.").as_ref());
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
self.push_undo_snapshot(i, "POLYSOLID");
|
self.push_undo_snapshot(i, "POLYSOLID");
|
||||||
self.tabs[i].scene.erase_entities(&[handle]);
|
self.tabs[i].scene.erase_entities(&[handle]);
|
||||||
let mut s3d = Solid3D::new();
|
let mut s3d = Solid3D::new();
|
||||||
@@ -310,11 +432,14 @@ impl super::OpenCADStudio {
|
|||||||
if !h.is_null() {
|
if !h.is_null() {
|
||||||
self.tabs[i].scene.select_entity(h, false);
|
self.tabs[i].scene.select_entity(h, false);
|
||||||
}
|
}
|
||||||
self.tabs[i].dirty = true;
|
if !h.is_null() {
|
||||||
self.refresh_properties();
|
self.tabs[i].dirty = true;
|
||||||
self.command_line.push_output(crate::tf!(
|
self.refresh_properties();
|
||||||
"POLYSOLID: built a wall solid from {used} segment(s) (width {width}, height {height})."
|
self.command_line.push_output(
|
||||||
).as_ref());
|
crate::tf!("POLYSOLID: created a wall solid (width {width}, height {height}).")
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,12 +632,12 @@ impl super::OpenCADStudio {
|
|||||||
self.tabs[i].scene.deselect_all();
|
self.tabs[i].scene.deselect_all();
|
||||||
if !handle.is_null() {
|
if !handle.is_null() {
|
||||||
self.tabs[i].scene.select_entity(handle, false);
|
self.tabs[i].scene.select_entity(handle, false);
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.refresh_properties();
|
||||||
|
self.command_line.push_output(crate::tf!(
|
||||||
|
"PYRAMID: created a {n}-sided pyramid (radius {radius}, height {height})."
|
||||||
|
).as_ref());
|
||||||
}
|
}
|
||||||
self.tabs[i].dirty = true;
|
|
||||||
self.refresh_properties();
|
|
||||||
self.command_line.push_output(crate::tf!(
|
|
||||||
"PYRAMID: created a {n}-sided pyramid (radius {radius}, height {height})."
|
|
||||||
).as_ref());
|
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ impl OpenCADStudio {
|
|||||||
let body = scene.solid_models.get(&h);
|
let body = scene.solid_models.get(&h);
|
||||||
let sat = needs_acis
|
let sat = needs_acis
|
||||||
.then(|| {
|
.then(|| {
|
||||||
body.and_then(crate::scene::convert::acis_export::planar_solid_to_sat)
|
body.and_then(crate::scene::convert::acis_export::solid_to_sat)
|
||||||
})
|
})
|
||||||
.flatten();
|
.flatten();
|
||||||
let wires = needs_wires.then(|| {
|
let wires = needs_wires.then(|| {
|
||||||
|
|||||||
@@ -3454,6 +3454,38 @@ impl OpenCADStudio {
|
|||||||
})?
|
})?
|
||||||
});
|
});
|
||||||
if let Some(handle) = hit {
|
if let Some(handle) = hit {
|
||||||
|
let uses_surface_point = self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|command| command.entity_pick_uses_surface_point());
|
||||||
|
let entity_pick_point = if uses_surface_point {
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.solid_click_point_for(p, view_rot2, eye2, bounds, handle)
|
||||||
|
.unwrap_or(pick_wcs)
|
||||||
|
} else {
|
||||||
|
pick_wcs
|
||||||
|
};
|
||||||
|
let entity_pick_direction = if uses_surface_point {
|
||||||
|
if matches!(
|
||||||
|
self.tabs[i].scene.document.get_entity(handle),
|
||||||
|
Some(acadrust::EntityType::Solid3D(_))
|
||||||
|
) {
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.solid_planar_face_normal_at(handle, entity_pick_point)
|
||||||
|
} else {
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.get_entity(handle)
|
||||||
|
.and_then(crate::entities::curve::entity_curve)
|
||||||
|
.and_then(|curve| curve.plane.normal())
|
||||||
|
.map(glam::DVec3::from_array)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
// Some commands (e.g. SS_CATCHMENT) need the entity
|
// Some commands (e.g. SS_CATCHMENT) need the entity
|
||||||
// body before `on_entity_pick` can advance.
|
// body before `on_entity_pick` can advance.
|
||||||
let inject_first = self.tabs[i]
|
let inject_first = self.tabs[i]
|
||||||
@@ -3484,7 +3516,8 @@ impl OpenCADStudio {
|
|||||||
let result = self.tabs[i].active_cmd.as_mut().map(|c| {
|
let result = self.tabs[i].active_cmd.as_mut().map(|c| {
|
||||||
// Shift-swap state for TRIM/EXTEND (#336).
|
// Shift-swap state for TRIM/EXTEND (#336).
|
||||||
c.set_shift(shift);
|
c.set_shift(shift);
|
||||||
c.on_entity_pick(handle, pick_wcs)
|
c.set_entity_pick_direction(entity_pick_direction);
|
||||||
|
c.on_entity_pick(handle, entity_pick_point)
|
||||||
});
|
});
|
||||||
// HATCHEDIT: after pick, inject hatch model data into the command.
|
// HATCHEDIT: after pick, inject hatch model data into the command.
|
||||||
if self.tabs[i]
|
if self.tabs[i]
|
||||||
@@ -4011,8 +4044,7 @@ impl OpenCADStudio {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
// Block-internal hatch: resolve to the
|
// Block-internal hatch: resolve to the parent Insert.
|
||||||
// parent Insert (AutoCAD behaviour).
|
|
||||||
scene::pick::hit_test::click_hit_insert_hatch(
|
scene::pick::hit_test::click_hit_insert_hatch(
|
||||||
p,
|
p,
|
||||||
self.tabs[i].scene.insert_hatches_for_click().as_ref(),
|
self.tabs[i].scene.insert_hatches_for_click().as_ref(),
|
||||||
|
|||||||
@@ -1425,17 +1425,20 @@ pub enum CmdResult {
|
|||||||
/// Translation vector applied once to every selected point.
|
/// Translation vector applied once to every selected point.
|
||||||
delta: DVec3,
|
delta: DVec3,
|
||||||
},
|
},
|
||||||
/// Create a Solid3D placeholder entity + associated MeshModel.
|
/// Extrude the profile entity `handle` along its plane normal.
|
||||||
/// `mesh_fn` is called with the entity's handle string to build the mesh.
|
|
||||||
CommitSolid3D {
|
|
||||||
mesh_fn: Box<dyn FnOnce(String) -> Option<crate::scene::model::mesh_model::MeshModel> + Send>,
|
|
||||||
},
|
|
||||||
/// Extrude the profile entity `handle` by `height` along Z.
|
|
||||||
ExtrudeEntity {
|
ExtrudeEntity {
|
||||||
handle: Handle,
|
handle: Handle,
|
||||||
height: f64,
|
height: f64,
|
||||||
color: [f32; 4],
|
color: [f32; 4],
|
||||||
},
|
},
|
||||||
|
/// Pull a closed profile or a planar solid face by a signed distance.
|
||||||
|
PresspullEntity {
|
||||||
|
handle: Handle,
|
||||||
|
pick: DVec3,
|
||||||
|
distance: f64,
|
||||||
|
drag: Option<DVec3>,
|
||||||
|
color: [f32; 4],
|
||||||
|
},
|
||||||
/// Revolve the profile entity `handle` around the given axis by `angle_deg`.
|
/// Revolve the profile entity `handle` around the given axis by `angle_deg`.
|
||||||
RevolveEntity {
|
RevolveEntity {
|
||||||
handle: Handle,
|
handle: Handle,
|
||||||
@@ -1455,6 +1458,13 @@ pub enum CmdResult {
|
|||||||
handles: Vec<Handle>,
|
handles: Vec<Handle>,
|
||||||
color: [f32; 4],
|
color: [f32; 4],
|
||||||
},
|
},
|
||||||
|
/// Round or bevel the straight edge nearest `pick` on a solid.
|
||||||
|
SolidEdgeBlend {
|
||||||
|
handle: Handle,
|
||||||
|
pick: DVec3,
|
||||||
|
value: f64,
|
||||||
|
fillet: bool,
|
||||||
|
},
|
||||||
/// INSERT landed on a block that has AttributeDefinitions.
|
/// INSERT landed on a block that has AttributeDefinitions.
|
||||||
/// The host should look up the attdefs for `block_name` from the document
|
/// The host should look up the attdefs for `block_name` from the document
|
||||||
/// and call `attreq_set_attdefs()` on the command, then loop on text input.
|
/// and call `attreq_set_attdefs()` on the command, then loop on text input.
|
||||||
@@ -1767,6 +1777,14 @@ pub trait CadCommand: Send {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Supply the mesh surface hit instead of the working-plane projection.
|
||||||
|
fn entity_pick_uses_surface_point(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supply the picked surface or profile direction when available.
|
||||||
|
fn set_entity_pick_direction(&mut self, _direction: Option<DVec3>) {}
|
||||||
|
|
||||||
/// Render the entity under the cursor through the normal rollover
|
/// Render the entity under the cursor through the normal rollover
|
||||||
/// highlight while this command is waiting for an entity pick.
|
/// highlight while this command is waiting for an entity pick.
|
||||||
fn entity_pick_highlights_hover(&self) -> bool {
|
fn entity_pick_highlights_hover(&self) -> bool {
|
||||||
|
|||||||
@@ -3,6 +3,15 @@
|
|||||||
|
|
||||||
pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static str)> {
|
pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static str)> {
|
||||||
match source {
|
match source {
|
||||||
|
"Boolean" => Some(("model", "boolean")),
|
||||||
|
"Create" => Some(("model", "create")),
|
||||||
|
"Extrude" => Some(("model", "extrude")),
|
||||||
|
"Loft" => Some(("model", "loft")),
|
||||||
|
"Polysolid" => Some(("model", "polysolid")),
|
||||||
|
"Presspull" => Some(("model", "presspull")),
|
||||||
|
"Pyramid" => Some(("model", "pyramid")),
|
||||||
|
"Revolve" => Some(("model", "revolve")),
|
||||||
|
"Sweep" => Some(("model", "sweep")),
|
||||||
" {:?}" => Some(("view", "message")),
|
" {:?}" => Some(("view", "message")),
|
||||||
" (none — use: SHORTCUTS SET <key> <command>)" => Some(("common", "none-use-shortcuts-set-key-command")),
|
" (none — use: SHORTCUTS SET <key> <command>)" => Some(("common", "none-use-shortcuts-set-key-command")),
|
||||||
" Alignment:" => Some(("common", "alignment-indented-label-title-case")),
|
" Alignment:" => Some(("common", "alignment-indented-label-title-case")),
|
||||||
|
|||||||
@@ -1,27 +1,19 @@
|
|||||||
// 2D→3D modelling commands — EXTRUDE, REVOLVE, SWEEP, LOFT.
|
// Profile-based kernel solid commands stored as exact ACIS data.
|
||||||
//
|
|
||||||
// Each picks profile/path entities and emits a `CmdResult` whose handler
|
|
||||||
// builds a solid and inserts its MeshModel into scene.meshes under a
|
|
||||||
// minimal placeholder Solid3D entity (empty ACIS — saving will not restore
|
|
||||||
// the mesh). The standalone primitives (BOX/CYLINDER/CONE/SPHERE/WEDGE/TORUS)
|
|
||||||
// live in the Model tab (`modules::model::primitive_cmd`).
|
|
||||||
|
|
||||||
use acadrust::{entities::Solid3D, EntityType};
|
use acadrust::{entities::Solid3D, EntityType};
|
||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
use crate::t;
|
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult};
|
use crate::command::{CadCommand, CmdResult};
|
||||||
|
use crate::t;
|
||||||
// BOX / SPHERE / CYLINDER (and CONE / WEDGE / TORUS) now live in the Model tab
|
|
||||||
// (`modules::model::primitive_cmd`), which builds them as B-reps cached
|
|
||||||
// for the Design-group boolean tools. EXTRUDE / REVOLVE / SWEEP / LOFT remain
|
|
||||||
// here as 2D→3D operations.
|
|
||||||
|
|
||||||
// ── EXTRUDE command ────────────────────────────────────────────────────────
|
// ── EXTRUDE command ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub struct ExtrudeCommand {
|
pub struct ExtrudeCommand {
|
||||||
|
command_name: &'static str,
|
||||||
step: ExtrudeStep,
|
step: ExtrudeStep,
|
||||||
pub target_handle: acadrust::Handle,
|
pub target_handle: acadrust::Handle,
|
||||||
|
anchor: DVec3,
|
||||||
|
direction: Option<DVec3>,
|
||||||
color: [f32; 4],
|
color: [f32; 4],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,10 +24,16 @@ enum ExtrudeStep {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ExtrudeCommand {
|
impl ExtrudeCommand {
|
||||||
pub fn new(color: [f32; 4]) -> Self {
|
pub fn new_named(name: &str, color: [f32; 4]) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
command_name: match name {
|
||||||
|
"THICKEN" => "THICKEN",
|
||||||
|
_ => "EXTRUDE",
|
||||||
|
},
|
||||||
step: ExtrudeStep::Pick,
|
step: ExtrudeStep::Pick,
|
||||||
target_handle: acadrust::Handle::NULL,
|
target_handle: acadrust::Handle::NULL,
|
||||||
|
anchor: DVec3::ZERO,
|
||||||
|
direction: None,
|
||||||
color,
|
color,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,7 +41,7 @@ impl ExtrudeCommand {
|
|||||||
|
|
||||||
impl CadCommand for ExtrudeCommand {
|
impl CadCommand for ExtrudeCommand {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
"EXTRUDE"
|
self.command_name
|
||||||
}
|
}
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
match self.step {
|
match self.step {
|
||||||
@@ -55,19 +53,33 @@ impl CadCommand for ExtrudeCommand {
|
|||||||
fn needs_entity_pick(&self) -> bool {
|
fn needs_entity_pick(&self) -> bool {
|
||||||
self.step == ExtrudeStep::Pick
|
self.step == ExtrudeStep::Pick
|
||||||
}
|
}
|
||||||
fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: DVec3) -> CmdResult {
|
fn entity_pick_uses_surface_point(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
fn set_entity_pick_direction(&mut self, direction: Option<DVec3>) {
|
||||||
|
self.direction = direction.and_then(DVec3::try_normalize);
|
||||||
|
}
|
||||||
|
fn on_entity_pick(&mut self, handle: acadrust::Handle, point: DVec3) -> CmdResult {
|
||||||
if handle.is_null() {
|
if handle.is_null() {
|
||||||
return CmdResult::NeedPoint;
|
return CmdResult::NeedPoint;
|
||||||
}
|
}
|
||||||
self.target_handle = handle;
|
self.target_handle = handle;
|
||||||
|
self.anchor = point;
|
||||||
self.step = ExtrudeStep::Height;
|
self.step = ExtrudeStep::Height;
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||||
if self.step == ExtrudeStep::Height {
|
if self.step == ExtrudeStep::Height {
|
||||||
|
let height = self
|
||||||
|
.direction
|
||||||
|
.map(|direction| (pt - self.anchor).dot(direction))
|
||||||
|
.unwrap_or_else(|| pt.distance(self.anchor));
|
||||||
|
if !height.is_finite() || height.abs() <= 1e-6 {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
return CmdResult::ExtrudeEntity {
|
return CmdResult::ExtrudeEntity {
|
||||||
handle: self.target_handle,
|
handle: self.target_handle,
|
||||||
height: pt.y.abs().max(1e-4),
|
height,
|
||||||
color: self.color,
|
color: self.color,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -81,13 +93,151 @@ impl CadCommand for ExtrudeCommand {
|
|||||||
.filter(|&h| h.abs() > 1e-6)
|
.filter(|&h| h.abs() > 1e-6)
|
||||||
.map(|h| CmdResult::ExtrudeEntity {
|
.map(|h| CmdResult::ExtrudeEntity {
|
||||||
handle: self.target_handle,
|
handle: self.target_handle,
|
||||||
height: h.abs(),
|
height: h,
|
||||||
color: self.color,
|
color: self.color,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
fn cursor_axis(&self) -> Option<(DVec3, DVec3)> {
|
||||||
|
(self.step == ExtrudeStep::Height).then_some((self.anchor, self.direction?))
|
||||||
|
}
|
||||||
|
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
|
||||||
|
(self.step == ExtrudeStep::Height).then_some(crate::command::DynSpec {
|
||||||
|
anchor: crate::command::DynAnchor::Point(self.anchor),
|
||||||
|
fields: vec![crate::command::DynFieldSpec::new(
|
||||||
|
crate::command::DynRole::Distance,
|
||||||
|
)],
|
||||||
|
guide: crate::command::DynGuide::Radius,
|
||||||
|
ref_point: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
|
||||||
|
Some((cursor - self.anchor).dot(self.direction?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PRESSPULL command ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub struct PresspullCommand {
|
||||||
|
picked: Option<(acadrust::Handle, DVec3)>,
|
||||||
|
direction: Option<DVec3>,
|
||||||
|
color: [f32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PresspullCommand {
|
||||||
|
pub fn new(color: [f32; 4]) -> Self {
|
||||||
|
Self {
|
||||||
|
picked: None,
|
||||||
|
direction: None,
|
||||||
|
color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CadCommand for PresspullCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"PRESSPULL"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
if self.picked.is_none() {
|
||||||
|
t!("PRESSPULL Select a closed profile or planar solid face:").into_owned()
|
||||||
|
} else {
|
||||||
|
t!("PRESSPULL Signed distance:").into_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn needs_entity_pick(&self) -> bool {
|
||||||
|
self.picked.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entity_pick_includes_fills(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entity_pick_uses_surface_point(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_entity_pick_direction(&mut self, direction: Option<DVec3>) {
|
||||||
|
self.direction = direction.and_then(|value| value.try_normalize());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entity_pick_highlights_hover(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_entity_pick(&mut self, handle: acadrust::Handle, point: DVec3) -> CmdResult {
|
||||||
|
if handle.is_null() {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
|
self.picked = Some((handle, point));
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||||
|
let Some((handle, pick)) = self.picked else {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
};
|
||||||
|
let distance = self
|
||||||
|
.direction
|
||||||
|
.map(|direction| (point - pick).dot(direction))
|
||||||
|
.unwrap_or_else(|| point.distance(pick));
|
||||||
|
if !distance.is_finite() || distance.abs() <= 1e-6 {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
|
CmdResult::PresspullEntity {
|
||||||
|
handle,
|
||||||
|
pick,
|
||||||
|
distance,
|
||||||
|
drag: Some(point),
|
||||||
|
color: self.color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wants_text_input(&self) -> bool {
|
||||||
|
self.picked.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
|
let (handle, pick) = self.picked?;
|
||||||
|
crate::entities::common::parse_typed_length(text)
|
||||||
|
.filter(|distance| distance.abs() > 1e-6)
|
||||||
|
.map(|distance| CmdResult::PresspullEntity {
|
||||||
|
handle,
|
||||||
|
pick,
|
||||||
|
distance,
|
||||||
|
drag: None,
|
||||||
|
color: self.color,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
CmdResult::Cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor_axis(&self) -> Option<(DVec3, DVec3)> {
|
||||||
|
Some((self.picked?.1, self.direction?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
|
||||||
|
let (_, anchor) = self.picked?;
|
||||||
|
Some(crate::command::DynSpec {
|
||||||
|
anchor: crate::command::DynAnchor::Point(anchor),
|
||||||
|
fields: vec![crate::command::DynFieldSpec::new(
|
||||||
|
crate::command::DynRole::Distance,
|
||||||
|
)],
|
||||||
|
guide: crate::command::DynGuide::Radius,
|
||||||
|
ref_point: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
|
||||||
|
let (_, anchor) = self.picked?;
|
||||||
|
Some((cursor - anchor).dot(self.direction?))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── REVOLVE command ────────────────────────────────────────────────────────
|
// ── REVOLVE command ────────────────────────────────────────────────────────
|
||||||
@@ -171,7 +321,7 @@ impl CadCommand for RevolveCommand {
|
|||||||
.ok()
|
.ok()
|
||||||
.filter(|&a| a.abs() > 1e-3)?
|
.filter(|&a| a.abs() > 1e-3)?
|
||||||
};
|
};
|
||||||
Some(self.make_revolve(angle.abs()))
|
Some(self.make_revolve(angle))
|
||||||
}
|
}
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
if self.step == RevolveStep::Angle {
|
if self.step == RevolveStep::Angle {
|
||||||
@@ -323,16 +473,16 @@ impl CadCommand for LoftCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Placeholder Solid3D entity construction ────────────────────────────────
|
// ── Solid3D entity construction ────────────────────────────────────────────
|
||||||
|
|
||||||
/// Create a minimal Solid3D entity with empty ACIS data (placeholder only).
|
|
||||||
pub fn empty_solid3d() -> EntityType {
|
pub fn empty_solid3d() -> EntityType {
|
||||||
EntityType::Solid3D(Solid3D::new())
|
EntityType::Solid3D(Solid3D::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ── Autocomplete registry ─────────────────────────────────
|
// ── Autocomplete registry ─────────────────────────────────
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["EXTRUDE"] }); // ExtrudeCommand
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["LOFT"] }); // LoftCommand
|
names: &["EXTRUDE", "THICKEN", "PRESSPULL"]
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["REVOLVE"] }); // RevolveCommand
|
});
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["SWEEP"] }); // SweepCommand
|
inventory::submit!(crate::command::CommandRegistration { names: &["LOFT"] });
|
||||||
|
inventory::submit!(crate::command::CommandRegistration { names: &["REVOLVE"] });
|
||||||
|
inventory::submit!(crate::command::CommandRegistration { names: &["SWEEP"] });
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
// Boolean operations on 3D solids (Design group). The actual CSG runs in
|
// Boolean operations on 3D solids. The actual CSG runs in
|
||||||
// `App::solid_boolean` (src/app/model_ops.rs) using the kernel on the
|
// `App::solid_boolean` (src/app/model_ops.rs) using the kernel on the
|
||||||
// cached B-reps; this module just names the operations.
|
// cached B-reps; this module just names the operations.
|
||||||
|
|
||||||
/// Which boolean a Design-group tool performs on the two selected solids.
|
/// Which boolean operation to apply to selected solids.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
pub enum BoolOp {
|
pub enum BoolOp {
|
||||||
Union,
|
Union,
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
use acadrust::Handle;
|
||||||
|
use glam::DVec3;
|
||||||
|
|
||||||
|
use crate::command::{
|
||||||
|
CadCommand, CmdResult, DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum EdgeOperation {
|
||||||
|
Fillet,
|
||||||
|
Chamfer,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SolidEdgeCommand {
|
||||||
|
operation: EdgeOperation,
|
||||||
|
target: Option<Handle>,
|
||||||
|
picked: Option<(Handle, DVec3)>,
|
||||||
|
default_value: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SolidEdgeCommand {
|
||||||
|
pub fn new(operation: EdgeOperation, target: Option<Handle>) -> Self {
|
||||||
|
Self {
|
||||||
|
operation,
|
||||||
|
target,
|
||||||
|
picked: None,
|
||||||
|
default_value: 1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(&self, value: f64) -> CmdResult {
|
||||||
|
let Some((handle, pick)) = self.picked else {
|
||||||
|
return CmdResult::Cancel;
|
||||||
|
};
|
||||||
|
CmdResult::SolidEdgeBlend {
|
||||||
|
handle,
|
||||||
|
pick,
|
||||||
|
value,
|
||||||
|
fillet: matches!(self.operation, EdgeOperation::Fillet),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CadCommand for SolidEdgeCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
match self.operation {
|
||||||
|
EdgeOperation::Fillet => "SOLIDFILLET",
|
||||||
|
EdgeOperation::Chamfer => "SOLIDCHAMFER",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
if self.picked.is_none() {
|
||||||
|
return crate::t!("Select a solid edge:").into_owned();
|
||||||
|
}
|
||||||
|
match self.operation {
|
||||||
|
EdgeOperation::Fillet => {
|
||||||
|
crate::tf!("Specify fillet radius <{:.3}>:", self.default_value).into_owned()
|
||||||
|
}
|
||||||
|
EdgeOperation::Chamfer => {
|
||||||
|
crate::tf!("Specify chamfer distance <{:.3}>:", self.default_value).into_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn needs_entity_pick(&self) -> bool {
|
||||||
|
self.picked.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entity_pick_includes_fills(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entity_pick_uses_surface_point(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entity_pick_highlights_hover(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
|
||||||
|
if handle.is_null() || self.target.is_some_and(|target| target != handle) {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
|
self.picked = Some((handle, point));
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||||
|
let Some((_, pick)) = self.picked else {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
};
|
||||||
|
let value = point.distance(pick);
|
||||||
|
if value > 0.0 && value.is_finite() {
|
||||||
|
self.finish(value)
|
||||||
|
} else {
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
|
if self.picked.is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let value = text.trim().parse::<f64>().ok()?;
|
||||||
|
(value > 0.0 && value.is_finite()).then(|| self.finish(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
if self.picked.is_some() {
|
||||||
|
self.finish(self.default_value)
|
||||||
|
} else {
|
||||||
|
CmdResult::Cancel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wants_text_input(&self) -> bool {
|
||||||
|
self.picked.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dyn_commit_as_text(&self) -> bool {
|
||||||
|
self.picked.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dyn_spec(&self) -> Option<DynSpec> {
|
||||||
|
let (_, pick) = self.picked?;
|
||||||
|
Some(DynSpec {
|
||||||
|
anchor: DynAnchor::Point(pick),
|
||||||
|
fields: vec![DynFieldSpec::new(match self.operation {
|
||||||
|
EdgeOperation::Fillet => DynRole::Radius,
|
||||||
|
EdgeOperation::Chamfer => DynRole::Distance,
|
||||||
|
})],
|
||||||
|
guide: DynGuide::Radius,
|
||||||
|
ref_point: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
|
||||||
|
self.picked.map(|(_, pick)| cursor.distance(pick))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
|
names: &["SOLIDFILLET", "SOLIDCHAMFER"]
|
||||||
|
});
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
// Model module — 3D solid modelling.
|
// Solid creation and kernel modelling tools.
|
||||||
//
|
|
||||||
// Model group : create primitive solids (box, cylinder, cone, sphere, …)
|
|
||||||
// as ACIS Solid3D entities via acadrust's primitive builders.
|
|
||||||
// Design group : combine solids with the kernel boolean operations
|
|
||||||
// (union / subtract / intersect).
|
|
||||||
|
|
||||||
pub mod boolean_cmd;
|
pub mod boolean_cmd;
|
||||||
|
pub mod edge_cmd;
|
||||||
pub mod primitive_cmd;
|
pub mod primitive_cmd;
|
||||||
|
|
||||||
use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef};
|
use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef};
|
||||||
@@ -16,11 +12,20 @@ const BOX_ICON: &[u8] = include_bytes!("../../../assets/icons/box3d.svg");
|
|||||||
const CYLINDER_ICON: &[u8] = include_bytes!("../../../assets/icons/cylinder3d.svg");
|
const CYLINDER_ICON: &[u8] = include_bytes!("../../../assets/icons/cylinder3d.svg");
|
||||||
const CONE_ICON: &[u8] = include_bytes!("../../../assets/icons/cone3d.svg");
|
const CONE_ICON: &[u8] = include_bytes!("../../../assets/icons/cone3d.svg");
|
||||||
const SPHERE_ICON: &[u8] = include_bytes!("../../../assets/icons/sphere3d.svg");
|
const SPHERE_ICON: &[u8] = include_bytes!("../../../assets/icons/sphere3d.svg");
|
||||||
|
const PYRAMID_ICON: &[u8] = include_bytes!("../../../assets/icons/pyramid3d.svg");
|
||||||
const WEDGE_ICON: &[u8] = include_bytes!("../../../assets/icons/wedge3d.svg");
|
const WEDGE_ICON: &[u8] = include_bytes!("../../../assets/icons/wedge3d.svg");
|
||||||
const TORUS_ICON: &[u8] = include_bytes!("../../../assets/icons/torus3d.svg");
|
const TORUS_ICON: &[u8] = include_bytes!("../../../assets/icons/torus3d.svg");
|
||||||
|
const POLYSOLID_ICON: &[u8] = include_bytes!("../../../assets/icons/polysolid.svg");
|
||||||
|
const EXTRUDE_ICON: &[u8] = include_bytes!("../../../assets/icons/extrude.svg");
|
||||||
|
const REVOLVE_ICON: &[u8] = include_bytes!("../../../assets/icons/revolve.svg");
|
||||||
|
const LOFT_ICON: &[u8] = include_bytes!("../../../assets/icons/loft.svg");
|
||||||
|
const SWEEP_ICON: &[u8] = include_bytes!("../../../assets/icons/sweep.svg");
|
||||||
|
const PRESSPULL_ICON: &[u8] = include_bytes!("../../../assets/icons/presspull.svg");
|
||||||
const UNION_ICON: &[u8] = include_bytes!("../../../assets/icons/union.svg");
|
const UNION_ICON: &[u8] = include_bytes!("../../../assets/icons/union.svg");
|
||||||
const SUBTRACT_ICON: &[u8] = include_bytes!("../../../assets/icons/subtract.svg");
|
const SUBTRACT_ICON: &[u8] = include_bytes!("../../../assets/icons/subtract.svg");
|
||||||
const INTERSECT_ICON: &[u8] = include_bytes!("../../../assets/icons/intersect.svg");
|
const INTERSECT_ICON: &[u8] = include_bytes!("../../../assets/icons/intersect.svg");
|
||||||
|
const FILLET_ICON: &[u8] = include_bytes!("../../../assets/icons/fillet.svg");
|
||||||
|
const CHAMFER_ICON: &[u8] = include_bytes!("../../../assets/icons/chamfer.svg");
|
||||||
|
|
||||||
/// Helper to declare a ribbon tool that fires a named command.
|
/// Helper to declare a ribbon tool that fires a named command.
|
||||||
fn tool(id: &'static str, label: &'static str, icon: &'static [u8]) -> ToolDef {
|
fn tool(id: &'static str, label: &'static str, icon: &'static [u8]) -> ToolDef {
|
||||||
@@ -37,7 +42,7 @@ impl CadModule for ModelModule {
|
|||||||
"model"
|
"model"
|
||||||
}
|
}
|
||||||
fn title(&self) -> &'static str {
|
fn title(&self) -> &'static str {
|
||||||
"Model"
|
"Modelling"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ribbon_groups(&self) -> &[RibbonGroup] {
|
fn ribbon_groups(&self) -> &[RibbonGroup] {
|
||||||
@@ -45,31 +50,46 @@ impl CadModule for ModelModule {
|
|||||||
GROUPS.get_or_init(|| {
|
GROUPS.get_or_init(|| {
|
||||||
vec![
|
vec![
|
||||||
RibbonGroup {
|
RibbonGroup {
|
||||||
title: "Model",
|
title: "Create",
|
||||||
tools: vec![
|
tools: vec![
|
||||||
RibbonItem::LargeTool(tool("BOX", "Box", BOX_ICON)),
|
RibbonItem::LargeDropdown {
|
||||||
RibbonItem::LargeTool(tool("CYLINDER", "Cylinder", CYLINDER_ICON)),
|
id: "MODEL_PRIMITIVES",
|
||||||
RibbonItem::LargeTool(tool("CONE", "Cone", CONE_ICON)),
|
label: "Box",
|
||||||
RibbonItem::LargeTool(tool("SPHERE", "Sphere", SPHERE_ICON)),
|
icon: IconKind::Svg(BOX_ICON),
|
||||||
RibbonItem::Dropdown {
|
|
||||||
id: "MODEL_MORE",
|
|
||||||
icon: IconKind::Svg(WEDGE_ICON),
|
|
||||||
items: vec![
|
items: vec![
|
||||||
|
("BOX", "Box", IconKind::Svg(BOX_ICON)),
|
||||||
|
("CYLINDER", "Cylinder", IconKind::Svg(CYLINDER_ICON)),
|
||||||
|
("CONE", "Cone", IconKind::Svg(CONE_ICON)),
|
||||||
|
("SPHERE", "Sphere", IconKind::Svg(SPHERE_ICON)),
|
||||||
|
("PYRAMID", "Pyramid", IconKind::Svg(PYRAMID_ICON)),
|
||||||
("WEDGE", "Wedge", IconKind::Svg(WEDGE_ICON)),
|
("WEDGE", "Wedge", IconKind::Svg(WEDGE_ICON)),
|
||||||
("TORUS", "Torus", IconKind::Svg(TORUS_ICON)),
|
("TORUS", "Torus", IconKind::Svg(TORUS_ICON)),
|
||||||
|
("POLYSOLID", "Polysolid", IconKind::Svg(POLYSOLID_ICON)),
|
||||||
],
|
],
|
||||||
default: "WEDGE",
|
default: "BOX",
|
||||||
},
|
},
|
||||||
|
RibbonItem::LargeTool(tool("EXTRUDE", "Extrude", EXTRUDE_ICON)),
|
||||||
|
RibbonItem::LargeTool(tool("REVOLVE", "Revolve", REVOLVE_ICON)),
|
||||||
|
RibbonItem::LargeTool(tool("LOFT", "Loft", LOFT_ICON)),
|
||||||
|
RibbonItem::LargeTool(tool("SWEEP", "Sweep", SWEEP_ICON)),
|
||||||
|
RibbonItem::LargeTool(tool("PRESSPULL", "Presspull", PRESSPULL_ICON)),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
RibbonGroup {
|
RibbonGroup {
|
||||||
title: "Design",
|
title: "Boolean",
|
||||||
tools: vec![
|
tools: vec![
|
||||||
RibbonItem::LargeTool(tool("UNION", "Union", UNION_ICON)),
|
RibbonItem::LargeTool(tool("UNION", "Union", UNION_ICON)),
|
||||||
RibbonItem::LargeTool(tool("SUBTRACT", "Subtract", SUBTRACT_ICON)),
|
RibbonItem::LargeTool(tool("SUBTRACT", "Subtract", SUBTRACT_ICON)),
|
||||||
RibbonItem::LargeTool(tool("INTERSECT", "Intersect", INTERSECT_ICON)),
|
RibbonItem::LargeTool(tool("INTERSECT", "Intersect", INTERSECT_ICON)),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
RibbonGroup {
|
||||||
|
title: "Edges",
|
||||||
|
tools: vec![
|
||||||
|
RibbonItem::LargeTool(tool("SOLIDFILLET", "Fillet", FILLET_ICON)),
|
||||||
|
RibbonItem::LargeTool(tool("SOLIDCHAMFER", "Chamfer", CHAMFER_ICON)),
|
||||||
|
],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,15 @@
|
|||||||
// 3D primitive creation — BOX / CYLINDER / CONE / SPHERE / WEDGE / PYRAMID /
|
// Interactive kernel primitives stored as ACIS Solid3D entities.
|
||||||
// TORUS. Each is placed CAD-style with a few clicks (planar footprint first,
|
|
||||||
// then a height value), then built as a real ACIS `Solid3D` via acadrust's
|
|
||||||
// `acis::primitives` builders. `Scene::add_entity` tessellates the SAT B-rep
|
|
||||||
// into the 3D mesh pipeline, so the solid renders, selects, and saves to DXF.
|
|
||||||
//
|
|
||||||
// A matching the kernel `Solid` is cached on the scene (see model/mod.rs) when the
|
|
||||||
// entity is committed, so the Design-group boolean tools can combine it.
|
|
||||||
|
|
||||||
use acadrust::entities::Solid3D;
|
use acadrust::entities::Solid3D;
|
||||||
use acadrust::objects::SolidHistoryOperation;
|
use acadrust::objects::SolidHistoryOperation;
|
||||||
use acadrust::{primitives, EntityType};
|
use acadrust::EntityType;
|
||||||
use glam::DVec3;
|
|
||||||
use crate::t;
|
|
||||||
use cadkernel::brep::Body;
|
use cadkernel::brep::Body;
|
||||||
|
use glam::DVec3;
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
||||||
use crate::scene::model::solid_model;
|
use crate::scene::model::solid_model;
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
|
use crate::t;
|
||||||
|
|
||||||
/// Which primitive a `PrimitiveCommand` builds.
|
/// Which primitive a `PrimitiveCommand` builds.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -26,6 +19,7 @@ pub enum Shape {
|
|||||||
Cylinder,
|
Cylinder,
|
||||||
Cone,
|
Cone,
|
||||||
Sphere,
|
Sphere,
|
||||||
|
Pyramid,
|
||||||
Torus,
|
Torus,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +31,7 @@ impl Shape {
|
|||||||
"CYLINDER" => Shape::Cylinder,
|
"CYLINDER" => Shape::Cylinder,
|
||||||
"CONE" => Shape::Cone,
|
"CONE" => Shape::Cone,
|
||||||
"SPHERE" => Shape::Sphere,
|
"SPHERE" => Shape::Sphere,
|
||||||
|
"PYRAMID" | "PYR" => Shape::Pyramid,
|
||||||
"TORUS" => Shape::Torus,
|
"TORUS" => Shape::Torus,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
@@ -48,6 +43,7 @@ impl Shape {
|
|||||||
Shape::Cylinder => "CYLINDER",
|
Shape::Cylinder => "CYLINDER",
|
||||||
Shape::Cone => "CONE",
|
Shape::Cone => "CONE",
|
||||||
Shape::Sphere => "SPHERE",
|
Shape::Sphere => "SPHERE",
|
||||||
|
Shape::Pyramid => "PYRAMID",
|
||||||
Shape::Torus => "TORUS",
|
Shape::Torus => "TORUS",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,12 +125,11 @@ impl PrimitiveCommand {
|
|||||||
.to_cols_array()
|
.to_cols_array()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build both the acadrust `Solid3D` (ACIS, for persistence) and the
|
/// Build the persistent kernel body and its history node.
|
||||||
/// kernel `Body` (rendering + booleans) from the footprint + `height`.
|
fn build(&self, height: f64) -> Option<(Body, SolidHistoryOperation)> {
|
||||||
fn build(&self, height: f64) -> Option<(EntityType, Body, SolidHistoryOperation)> {
|
|
||||||
use crate::scene::model::solid_history;
|
use crate::scene::model::solid_history;
|
||||||
|
|
||||||
let (doc, solid, history) = match self.shape {
|
let (solid, history) = match self.shape {
|
||||||
Shape::Box | Shape::Wedge => {
|
Shape::Box | Shape::Wedge => {
|
||||||
let (a, b) = (self.pts[0], self.pts[1]);
|
let (a, b) = (self.pts[0], self.pts[1]);
|
||||||
let length = (b.x - a.x).abs();
|
let length = (b.x - a.x).abs();
|
||||||
@@ -150,7 +145,6 @@ impl PrimitiveCommand {
|
|||||||
a.z + height / 2.0,
|
a.z + height / 2.0,
|
||||||
];
|
];
|
||||||
(
|
(
|
||||||
primitives::build_box(center, length, width, height),
|
|
||||||
solid_model::box_solid(center, length, width, height),
|
solid_model::box_solid(center, length, width, height),
|
||||||
solid_history::box_op(
|
solid_history::box_op(
|
||||||
self.history_transform(origin),
|
self.history_transform(origin),
|
||||||
@@ -162,7 +156,6 @@ impl PrimitiveCommand {
|
|||||||
} else {
|
} else {
|
||||||
let origin = [a.x.min(b.x), a.y.min(b.y), a.z];
|
let origin = [a.x.min(b.x), a.y.min(b.y), a.z];
|
||||||
(
|
(
|
||||||
primitives::build_wedge(origin, length, width, height),
|
|
||||||
solid_model::wedge_solid(origin, length, width, height),
|
solid_model::wedge_solid(origin, length, width, height),
|
||||||
solid_history::wedge_op(
|
solid_history::wedge_op(
|
||||||
self.history_transform(DVec3::from_array(origin)),
|
self.history_transform(DVec3::from_array(origin)),
|
||||||
@@ -182,7 +175,6 @@ impl PrimitiveCommand {
|
|||||||
let center = [c.x, c.y, c.z];
|
let center = [c.x, c.y, c.z];
|
||||||
if self.shape == Shape::Cylinder {
|
if self.shape == Shape::Cylinder {
|
||||||
(
|
(
|
||||||
primitives::build_cylinder(center, r, height),
|
|
||||||
solid_model::cylinder_solid(center, r, height),
|
solid_model::cylinder_solid(center, r, height),
|
||||||
solid_history::cylinder_op(
|
solid_history::cylinder_op(
|
||||||
self.history_transform(c),
|
self.history_transform(c),
|
||||||
@@ -192,7 +184,6 @@ impl PrimitiveCommand {
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
primitives::build_cone(center, r, height),
|
|
||||||
solid_model::cone_solid(center, r, height),
|
solid_model::cone_solid(center, r, height),
|
||||||
solid_history::cone_op(
|
solid_history::cone_op(
|
||||||
self.history_transform(c),
|
self.history_transform(c),
|
||||||
@@ -210,11 +201,21 @@ impl PrimitiveCommand {
|
|||||||
}
|
}
|
||||||
let center = [c.x, c.y, c.z];
|
let center = [c.x, c.y, c.z];
|
||||||
(
|
(
|
||||||
primitives::build_sphere(center, r),
|
|
||||||
solid_model::sphere_solid(center, r),
|
solid_model::sphere_solid(center, r),
|
||||||
solid_history::sphere_op(self.history_transform(c), r),
|
solid_history::sphere_op(self.history_transform(c), r),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Shape::Pyramid => {
|
||||||
|
let c = self.pts[0];
|
||||||
|
let r = (self.pts[1] - c).length();
|
||||||
|
if r < 1e-6 || height < 1e-6 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
(
|
||||||
|
solid_model::pyramid_solid([c.x, c.y, c.z], r, height, 4),
|
||||||
|
solid_history::pyramid_op(self.history_transform(c), r, height, 4),
|
||||||
|
)
|
||||||
|
}
|
||||||
Shape::Torus => {
|
Shape::Torus => {
|
||||||
let c = self.pts[0];
|
let c = self.pts[0];
|
||||||
let first = (self.pts[1] - c).length();
|
let first = (self.pts[1] - c).length();
|
||||||
@@ -228,7 +229,6 @@ impl PrimitiveCommand {
|
|||||||
let minor = (outer - inner) * 0.5;
|
let minor = (outer - inner) * 0.5;
|
||||||
let center = [c.x, c.y, c.z];
|
let center = [c.x, c.y, c.z];
|
||||||
(
|
(
|
||||||
primitives::build_torus(center, major, minor),
|
|
||||||
solid_model::torus_solid(center, major, minor),
|
solid_model::torus_solid(center, major, minor),
|
||||||
solid_history::torus_op(
|
solid_history::torus_op(
|
||||||
self.history_transform(c),
|
self.history_transform(c),
|
||||||
@@ -239,14 +239,12 @@ impl PrimitiveCommand {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let solid = solid?;
|
let solid = solid?;
|
||||||
let mut s3d = Solid3D::new();
|
Some((solid, history))
|
||||||
s3d.set_sat_document(&doc);
|
|
||||||
Some((EntityType::Solid3D(s3d), solid, history))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commit(&self, height: f64) -> CmdResult {
|
fn commit(&self, height: f64) -> CmdResult {
|
||||||
match self.build(height) {
|
match self.build(height) {
|
||||||
Some((entity, solid, history)) => {
|
Some((solid, history)) => {
|
||||||
// Built upright in its own frame, then put on the working
|
// Built upright in its own frame, then put on the working
|
||||||
// plane — the same move `place_entity` makes for the ACIS
|
// plane — the same move `place_entity` makes for the ACIS
|
||||||
// copy, so the two stay on top of each other.
|
// copy, so the two stay on top of each other.
|
||||||
@@ -262,11 +260,21 @@ impl PrimitiveCommand {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
match placed {
|
match placed {
|
||||||
Some(placed) => CmdResult::CommitSolid {
|
Some(placed) => {
|
||||||
entity: self.plane.place_entity(entity),
|
let Some(document) =
|
||||||
solid: Box::new(placed),
|
crate::scene::convert::acis_export::solid_to_sat(&placed)
|
||||||
history,
|
else {
|
||||||
},
|
return CmdResult::Cancel;
|
||||||
|
};
|
||||||
|
let mut entity = Solid3D::new();
|
||||||
|
entity.set_sat_document(&document);
|
||||||
|
entity.wires = solid_model::edge_wires(&placed);
|
||||||
|
CmdResult::CommitSolid {
|
||||||
|
entity: EntityType::Solid3D(entity),
|
||||||
|
solid: Box::new(placed),
|
||||||
|
history,
|
||||||
|
}
|
||||||
|
}
|
||||||
None => CmdResult::Cancel,
|
None => CmdResult::Cancel,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -390,7 +398,15 @@ impl CadCommand for PrimitiveCommand {
|
|||||||
|
|
||||||
fn footprint_wire(shape: Shape, pts: &[DVec3]) -> WireModel {
|
fn footprint_wire(shape: Shape, pts: &[DVec3]) -> WireModel {
|
||||||
let mut points: Vec<[f32; 3]> = Vec::new();
|
let mut points: Vec<[f32; 3]> = Vec::new();
|
||||||
if shape.radial() {
|
if shape == Shape::Pyramid {
|
||||||
|
let center = pts[0];
|
||||||
|
let radius = (pts[1] - center).length();
|
||||||
|
let corners: [DVec3; 4] = std::array::from_fn(|index| {
|
||||||
|
let angle = index as f64 * std::f64::consts::FRAC_PI_2;
|
||||||
|
center + DVec3::new(radius * angle.cos(), radius * angle.sin(), 0.0)
|
||||||
|
});
|
||||||
|
push_loop(&mut points, &corners);
|
||||||
|
} else if shape.radial() {
|
||||||
let c = pts[0];
|
let c = pts[0];
|
||||||
let r = (pts[1] - c).length();
|
let r = (pts[1] - c).length();
|
||||||
circle_points(&mut points, c, r);
|
circle_points(&mut points, c, r);
|
||||||
@@ -446,9 +462,21 @@ fn height_wire(shape: Shape, pts: &[DVec3], height: f64) -> WireModel {
|
|||||||
push_segment(&mut points, low[i], high[i]);
|
push_segment(&mut points, low[i], high[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Shape::Cylinder | Shape::Cone => {
|
Shape::Cylinder | Shape::Cone | Shape::Pyramid => {
|
||||||
let center = pts[0];
|
let center = pts[0];
|
||||||
let radius = (pts[1] - center).length();
|
let radius = (pts[1] - center).length();
|
||||||
|
if shape == Shape::Pyramid {
|
||||||
|
let base: [DVec3; 4] = std::array::from_fn(|index| {
|
||||||
|
let angle = index as f64 * std::f64::consts::FRAC_PI_2;
|
||||||
|
center + DVec3::new(radius * angle.cos(), radius * angle.sin(), 0.0)
|
||||||
|
});
|
||||||
|
push_loop(&mut points, &base);
|
||||||
|
let apex = center + DVec3::Z * height;
|
||||||
|
for corner in base {
|
||||||
|
push_segment(&mut points, corner, apex);
|
||||||
|
}
|
||||||
|
return wire("primitive_height_preview", points);
|
||||||
|
}
|
||||||
push_circle(&mut points, center, radius);
|
push_circle(&mut points, center, radius);
|
||||||
if shape == Shape::Cylinder {
|
if shape == Shape::Cylinder {
|
||||||
push_circle(&mut points, center + DVec3::Z * height, radius);
|
push_circle(&mut points, center + DVec3::Z * height, radius);
|
||||||
@@ -506,7 +534,7 @@ fn circle_points(out: &mut Vec<[f32; 3]>, c: DVec3, r: f64) {
|
|||||||
|
|
||||||
// ── Autocomplete registry ─────────────────────────────────
|
// ── Autocomplete registry ─────────────────────────────────
|
||||||
inventory::submit!(crate::command::CommandRegistration {
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
names: &["BOX", "WEDGE", "CYLINDER", "CONE", "SPHERE", "TORUS"]
|
names: &["BOX", "WEDGE", "CYLINDER", "CONE", "SPHERE", "PYRAMID", "PYR", "TORUS"]
|
||||||
});
|
});
|
||||||
|
|
||||||
fn wire(name: &str, points: Vec<[f32; 3]>) -> WireModel {
|
fn wire(name: &str, points: Vec<[f32; 3]>) -> WireModel {
|
||||||
@@ -522,9 +550,9 @@ fn wire(name: &str, points: Vec<[f32; 3]>) -> WireModel {
|
|||||||
render_instance: None,
|
render_instance: None,
|
||||||
pick_tris: Vec::new(),
|
pick_tris: Vec::new(),
|
||||||
pick_tris_low: Vec::new(),
|
pick_tris_low: Vec::new(),
|
||||||
dash_from_start: false,
|
dash_from_start: false,
|
||||||
dash_align_end: None,
|
dash_align_end: None,
|
||||||
text_verts: Vec::new(),
|
text_verts: Vec::new(),
|
||||||
name: name.into(),
|
name: name.into(),
|
||||||
points,
|
points,
|
||||||
points_low: Vec::new(),
|
points_low: Vec::new(),
|
||||||
|
|||||||
@@ -3,12 +3,21 @@
|
|||||||
//! Analytic surfaces remain analytic instead of becoming facets.
|
//! Analytic surfaces remain analytic instead of becoming facets.
|
||||||
|
|
||||||
use cadkernel::acis::append;
|
use cadkernel::acis::append;
|
||||||
use acadrust::entities::acis::SatDocument;
|
use acadrust::entities::acis::{SabReader, SabWriter, SatDocument};
|
||||||
use cadkernel::brep::Body;
|
use cadkernel::brep::Body;
|
||||||
|
|
||||||
/// Returns `None` when the body contains an unsupported record form.
|
/// Returns `None` when the body contains an unsupported record form.
|
||||||
pub fn planar_solid_to_sat(body: &Body) -> Option<SatDocument> {
|
pub fn solid_to_sat(body: &Body) -> Option<SatDocument> {
|
||||||
let mut document = SatDocument::new();
|
let mut document = SatDocument::new();
|
||||||
append(body, &mut document).ok()?;
|
append(body, &mut document).ok()?;
|
||||||
Some(document)
|
let document = SatDocument::parse(&document.to_sat_string()).ok()?;
|
||||||
|
let valid = |candidate: &SatDocument| {
|
||||||
|
let (restored, loss) = cadkernel::acis::lift(candidate);
|
||||||
|
loss.is_empty() && restored.len() == 1 && restored[0].validate().is_empty()
|
||||||
|
};
|
||||||
|
if !valid(&document) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let binary = SabWriter::write(&document);
|
||||||
|
valid(&SabReader::read(&binary).ok()?).then_some(document)
|
||||||
}
|
}
|
||||||
@@ -518,10 +518,8 @@ pub(crate) fn wire_gpu_patch_enabled() -> bool {
|
|||||||
/// Resolve a viewport's paper-to-model scale ratio from its two
|
/// Resolve a viewport's paper-to-model scale ratio from its two
|
||||||
/// DXF-derived sources.
|
/// DXF-derived sources.
|
||||||
///
|
///
|
||||||
/// `view_height` (model-space view extent) is the canonical source — it
|
/// `view_height` is canonical. `custom_scale` is the fallback when it is
|
||||||
/// is what AutoCAD actually uses to draw, and what we keep in sync on
|
/// missing or zero.
|
||||||
/// every write. `custom_scale` is consulted only when `view_height` is
|
|
||||||
/// missing or zero (some third-party exporters omit it).
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn vp_effective_scale(custom_scale: f64, view_height: f64, vp_height: f64) -> f64 {
|
pub fn vp_effective_scale(custom_scale: f64, view_height: f64, vp_height: f64) -> f64 {
|
||||||
if view_height.abs() > 1e-9 {
|
if view_height.abs() > 1e-9 {
|
||||||
@@ -2945,8 +2943,8 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Guarantee that a paper layout has its full-screen overall (`id == 1`)
|
/// Guarantee that a paper layout has its full-screen overall (`id == 1`)
|
||||||
/// sheet viewport. AutoCAD always writes one, and `add_layout` creates it,
|
/// sheet viewport. `add_layout` creates it; this is a safety net for layouts
|
||||||
/// but this is a safety net for layouts that arrive without it. The sheet
|
/// that arrive without it. The sheet
|
||||||
/// viewport is the authoritative paper-space view and the canvas every
|
/// viewport is the authoritative paper-space view and the canvas every
|
||||||
/// floating viewport overlays.
|
/// floating viewport overlays.
|
||||||
pub fn ensure_sheet_viewport(&mut self, layout_name: &str) {
|
pub fn ensure_sheet_viewport(&mut self, layout_name: &str) {
|
||||||
@@ -3016,9 +3014,8 @@ impl Scene {
|
|||||||
let mut vp = acadrust::entities::Viewport::new();
|
let mut vp = acadrust::entities::Viewport::new();
|
||||||
vp.id = 1;
|
vp.id = 1;
|
||||||
vp.status = acadrust::entities::ViewportStatusFlags::default_on();
|
vp.status = acadrust::entities::ViewportStatusFlags::default_on();
|
||||||
// Paper-space center is a 2D (x, y) point with z = 0 — the same
|
// Paper-space center is a 2D (x, y) point with z = 0. Putting the
|
||||||
// convention MVIEW uses for floating viewports. AutoCAD/TrueView read
|
// paper-height midpoint in z
|
||||||
// the viewport center as (x, y); putting the paper-height midpoint in z
|
|
||||||
// (with y = 0) left the sheet view centered at y = 0, shifting the whole
|
// (with y = 0) left the sheet view centered at y = 0, shifting the whole
|
||||||
// layout half a page down. See issue #156.
|
// layout half a page down. See issue #156.
|
||||||
vp.center = acadrust::types::Vector3::new(
|
vp.center = acadrust::types::Vector3::new(
|
||||||
@@ -3242,8 +3239,7 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Dashed rectangle marking the printable area — the paper inset by the
|
/// Dashed rectangle marking the printable area — the paper inset by the
|
||||||
/// layout's plot margins. AutoCAD draws this guide on every layout; with the
|
/// layout's plot margins. `None` in model space, when
|
||||||
/// margins now preserved we can reflect it too. `None` in model space, when
|
|
||||||
/// the layout has no margins, or when the inset would be degenerate.
|
/// the layout has no margins, or when the inset would be degenerate.
|
||||||
pub(super) fn printable_area_wire(&self) -> Option<WireModel> {
|
pub(super) fn printable_area_wire(&self) -> Option<WireModel> {
|
||||||
if self.current_layout == "Model" {
|
if self.current_layout == "Model" {
|
||||||
@@ -3311,8 +3307,7 @@ impl Scene {
|
|||||||
/// The effective plot settings for the current layout: a standalone
|
/// The effective plot settings for the current layout: a standalone
|
||||||
/// PlotSettings page setup if one exists, otherwise the settings embedded in
|
/// PlotSettings page setup if one exists, otherwise the settings embedded in
|
||||||
/// the LAYOUT object (paper size, margins, origin, rotation, scale). Loaded
|
/// the LAYOUT object (paper size, margins, origin, rotation, scale). Loaded
|
||||||
/// AutoCAD files keep their settings embedded, so without this fallback the
|
/// The fallback preserves rotation, origin and scale from loaded files.
|
||||||
/// plot/PDF path would ignore the file's rotation, origin and scale.
|
|
||||||
pub fn effective_plot_settings(&self) -> Option<acadrust::objects::PlotSettings> {
|
pub fn effective_plot_settings(&self) -> Option<acadrust::objects::PlotSettings> {
|
||||||
self.plot_settings_for(&self.current_layout)
|
self.plot_settings_for(&self.current_layout)
|
||||||
}
|
}
|
||||||
@@ -7974,6 +7969,44 @@ impl Scene {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn solid_click_point_for(
|
||||||
|
&self,
|
||||||
|
cursor: iced::Point,
|
||||||
|
view_rot: glam::Mat4,
|
||||||
|
eye: glam::DVec3,
|
||||||
|
bounds: iced::Rectangle,
|
||||||
|
target: Handle,
|
||||||
|
) -> Option<glam::DVec3> {
|
||||||
|
let meshes = self.interaction_meshes_arc();
|
||||||
|
pick::hit_test::mesh_click_point(
|
||||||
|
cursor,
|
||||||
|
meshes.iter().filter_map(|set| {
|
||||||
|
(set.entity_handle()? == target).then_some(())?;
|
||||||
|
let mesh = set.geometry_lods().first()?;
|
||||||
|
Some((
|
||||||
|
target,
|
||||||
|
mesh,
|
||||||
|
set.instance_transform,
|
||||||
|
mesh_interaction_aabb(set)?,
|
||||||
|
))
|
||||||
|
}),
|
||||||
|
view_rot,
|
||||||
|
eye,
|
||||||
|
bounds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn solid_planar_face_normal_at(
|
||||||
|
&mut self,
|
||||||
|
handle: Handle,
|
||||||
|
pick: glam::DVec3,
|
||||||
|
) -> Option<glam::DVec3> {
|
||||||
|
self.restore_solid_models(&[handle]);
|
||||||
|
let body = self.solid_models.get(&handle)?;
|
||||||
|
let face = model::solid_model::nearest_planar_face(body, pick.to_array())?;
|
||||||
|
model::solid_model::planar_face_normal(body, face).map(glam::DVec3::from_array)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn view_center_surface_pivot(
|
pub fn view_center_surface_pivot(
|
||||||
&self,
|
&self,
|
||||||
bounds: iced::Rectangle,
|
bounds: iced::Rectangle,
|
||||||
@@ -9374,9 +9407,7 @@ impl Scene {
|
|||||||
if any {
|
if any {
|
||||||
return Some((min, max));
|
return Some((min, max));
|
||||||
}
|
}
|
||||||
// Last-resort: the header's saved EXTMIN/EXTMAX. AutoCAD writes these
|
// Last resort: saved EXTMIN/EXTMAX before the wire cache is built.
|
||||||
// on save so opening a file gives ZOOM EXTENTS a useful answer before
|
|
||||||
// the wire cache is built.
|
|
||||||
const SANE_EXTENT: f64 = 1.0e16;
|
const SANE_EXTENT: f64 = 1.0e16;
|
||||||
let h = &self.document.header;
|
let h = &self.document.header;
|
||||||
let hmin = h.model_space_extents_min;
|
let hmin = h.model_space_extents_min;
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
use acadrust::entities::{EmbeddedEntity, Solid3D};
|
use acadrust::entities::Solid3D;
|
||||||
use acadrust::objects::{
|
use acadrust::objects::{
|
||||||
SolidHistoryBox, SolidHistoryBrep, SolidHistoryCylinder, SolidHistoryLoft,
|
SolidHistoryBox, SolidHistoryBrep, SolidHistoryCylinder,
|
||||||
SolidHistoryNodeBase, SolidHistoryOperation, SolidHistoryPyramid,
|
SolidHistoryNodeBase, SolidHistoryOperation, SolidHistoryPyramid,
|
||||||
SolidHistoryRevolve, SolidHistorySphere, SolidHistorySweep, SolidHistoryTorus,
|
SolidHistorySphere, SolidHistoryTorus,
|
||||||
};
|
};
|
||||||
use acadrust::types::{Vector2, Vector3};
|
|
||||||
use acadrust::EntityType;
|
|
||||||
use cadkernel::brep::Body;
|
use cadkernel::brep::Body;
|
||||||
|
|
||||||
use crate::command::EntityTransform;
|
use crate::command::EntityTransform;
|
||||||
@@ -474,21 +472,6 @@ fn base(transform: [f64; 16]) -> SolidHistoryNodeBase {
|
|||||||
base
|
base
|
||||||
}
|
}
|
||||||
|
|
||||||
fn embedded(entity: &EntityType) -> Option<EmbeddedEntity> {
|
|
||||||
Some(match entity {
|
|
||||||
EntityType::Point(value) => EmbeddedEntity::Point(value.clone()),
|
|
||||||
EntityType::Line(value) => EmbeddedEntity::Line(value.clone()),
|
|
||||||
EntityType::Arc(value) => EmbeddedEntity::Arc(value.clone()),
|
|
||||||
EntityType::Circle(value) => EmbeddedEntity::Circle(value.clone()),
|
|
||||||
EntityType::Ellipse(value) => EmbeddedEntity::Ellipse(value.clone()),
|
|
||||||
EntityType::Spline(value) => EmbeddedEntity::Spline(value.clone()),
|
|
||||||
EntityType::LwPolyline(value) => EmbeddedEntity::LwPolyline(value.clone()),
|
|
||||||
EntityType::Ray(value) => EmbeddedEntity::Ray(value.clone()),
|
|
||||||
EntityType::XLine(value) => EmbeddedEntity::XLine(value.clone()),
|
|
||||||
_ => return None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn box_op(
|
pub fn box_op(
|
||||||
transform: [f64; 16],
|
transform: [f64; 16],
|
||||||
length: f64,
|
length: f64,
|
||||||
@@ -593,7 +576,7 @@ pub fn pyramid_op(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn brep_op(body: &Body) -> SolidHistoryOperation {
|
pub fn brep_op(body: &Body) -> SolidHistoryOperation {
|
||||||
let acis_data = crate::scene::convert::acis_export::planar_solid_to_sat(body)
|
let acis_data = crate::scene::convert::acis_export::solid_to_sat(body)
|
||||||
.map(|document| {
|
.map(|document| {
|
||||||
let mut solid = Solid3D::new();
|
let mut solid = Solid3D::new();
|
||||||
solid.set_sat_document(&document);
|
solid.set_sat_document(&document);
|
||||||
@@ -607,56 +590,3 @@ pub fn brep_op(body: &Body) -> SolidHistoryOperation {
|
|||||||
..SolidHistoryBrep::default()
|
..SolidHistoryBrep::default()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extrusion_op(profile: &EntityType, height: f64) -> SolidHistoryOperation {
|
|
||||||
SolidHistoryOperation::Extrusion(SolidHistorySweep {
|
|
||||||
base: base(glam::DMat4::IDENTITY.to_cols_array()),
|
|
||||||
operation_major: 1,
|
|
||||||
direction: Vector3::new(0.0, 0.0, height),
|
|
||||||
sweep_entity: embedded(profile),
|
|
||||||
scale_factor: 1.0,
|
|
||||||
sweep_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
|
|
||||||
path_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
|
|
||||||
..SolidHistorySweep::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sweep_op(profile: &EntityType, path: &EntityType) -> SolidHistoryOperation {
|
|
||||||
SolidHistoryOperation::Sweep(SolidHistorySweep {
|
|
||||||
base: base(glam::DMat4::IDENTITY.to_cols_array()),
|
|
||||||
operation_major: 1,
|
|
||||||
sweep_entity: embedded(profile),
|
|
||||||
path_entity: embedded(path),
|
|
||||||
scale_factor: 1.0,
|
|
||||||
sweep_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
|
|
||||||
path_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
|
|
||||||
..SolidHistorySweep::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn loft_op(profiles: &[EntityType]) -> SolidHistoryOperation {
|
|
||||||
SolidHistoryOperation::Loft(SolidHistoryLoft {
|
|
||||||
base: base(glam::DMat4::IDENTITY.to_cols_array()),
|
|
||||||
operation_major: 1,
|
|
||||||
cross_sections: profiles.iter().filter_map(embedded).collect(),
|
|
||||||
..SolidHistoryLoft::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn revolve_op(
|
|
||||||
profile: &EntityType,
|
|
||||||
axis_start: [f64; 3],
|
|
||||||
axis_end: [f64; 3],
|
|
||||||
angle: f64,
|
|
||||||
) -> SolidHistoryOperation {
|
|
||||||
let direction = glam::DVec3::from_array(axis_end) - glam::DVec3::from_array(axis_start);
|
|
||||||
SolidHistoryOperation::Revolve(SolidHistoryRevolve {
|
|
||||||
base: base(glam::DMat4::IDENTITY.to_cols_array()),
|
|
||||||
operation_major: 1,
|
|
||||||
axis_point: Vector3::new(axis_start[0], axis_start[1], axis_start[2]),
|
|
||||||
direction: Vector2::new(direction.x, direction.y),
|
|
||||||
revolve_angle: angle,
|
|
||||||
sweep_entity: embedded(profile),
|
|
||||||
..SolidHistoryRevolve::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,6 @@
|
|||||||
// B-rep construction for the Model tab's primitives, plus tessellation into
|
// Kernel B-rep construction and display tessellation.
|
||||||
// the renderer's `MeshLodSet`.
|
|
||||||
//
|
|
||||||
// The bodies come from the geometry kernel, which builds each primitive the
|
|
||||||
// way ACIS records it: an analytic surface with singular vertices where the
|
|
||||||
// surface has them, rather than a mesh or a spline that happens to look like
|
|
||||||
// one. That is what lets a solid built here be written back out as exact
|
|
||||||
// geometry instead of a facetted approximation — see `acis_bridge`.
|
|
||||||
//
|
|
||||||
// Everything is oriented Z-up with the footprint on the z = base plane, to
|
|
||||||
// match acadrust's `acis::primitives`.
|
|
||||||
//
|
|
||||||
// The resulting `Body` is cached per entity handle on the Scene so the
|
|
||||||
// Design-group boolean tools can run on it.
|
|
||||||
|
|
||||||
use cadkernel::brep::{self, Body};
|
use cadkernel::brep::{self, Body, Curve3, EdgeKey, FaceKey, Surface};
|
||||||
|
|
||||||
use crate::scene::model::mesh_model::{MeshLodSet, MeshModel};
|
use crate::scene::model::mesh_model::{MeshLodSet, MeshModel};
|
||||||
|
|
||||||
@@ -223,6 +210,65 @@ pub fn edge_wires(body: &Body) -> Vec<acadrust::entities::Wire> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// B-rep edge nearest a world-space surface pick.
|
||||||
|
pub fn nearest_edge(body: &Body, pick: [f64; 3]) -> Option<EdgeKey> {
|
||||||
|
let pick = cadkernel::space::Vec3::from(pick);
|
||||||
|
body.edge_keys()
|
||||||
|
.filter_map(|key| {
|
||||||
|
let edge = body.edges.get(key)?;
|
||||||
|
let curve = body.curves.get(edge.curve)?;
|
||||||
|
let nearest = if matches!(curve, Curve3::Line(_)) {
|
||||||
|
let start = cadkernel::space::Vec3::from(curve.point_at(edge.start_parameter));
|
||||||
|
let end = cadkernel::space::Vec3::from(curve.point_at(edge.end_parameter));
|
||||||
|
let span = end - start;
|
||||||
|
let length2 = span.dot(span);
|
||||||
|
let along = if length2 > 0.0 {
|
||||||
|
(pick - start).dot(span) / length2
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
.clamp(0.0, 1.0);
|
||||||
|
pick.distance(start + span * along)
|
||||||
|
} else {
|
||||||
|
(0..=64)
|
||||||
|
.map(|step| {
|
||||||
|
let t = edge.start_parameter
|
||||||
|
+ (edge.end_parameter - edge.start_parameter) * step as f64 / 64.0;
|
||||||
|
pick.distance(cadkernel::space::Vec3::from(curve.point_at(t)))
|
||||||
|
})
|
||||||
|
.fold(f64::INFINITY, f64::min)
|
||||||
|
};
|
||||||
|
Some((key, nearest))
|
||||||
|
})
|
||||||
|
.min_by(|a, b| a.1.total_cmp(&b.1))
|
||||||
|
.map(|(key, _)| key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Planar face nearest a world-space surface pick.
|
||||||
|
pub fn nearest_planar_face(body: &Body, pick: [f64; 3]) -> Option<FaceKey> {
|
||||||
|
let face = body
|
||||||
|
.face_keys()
|
||||||
|
.filter_map(|key| {
|
||||||
|
let face = body.faces.get(key)?;
|
||||||
|
let surface = body.surfaces.get(face.surface)?;
|
||||||
|
Some((key, surface.distance_to(pick).abs()))
|
||||||
|
})
|
||||||
|
.min_by(|a, b| a.1.total_cmp(&b.1))
|
||||||
|
.map(|(key, _)| key)?;
|
||||||
|
let node = body.faces.get(face)?;
|
||||||
|
matches!(body.surfaces.get(node.surface)?, Surface::Plane(_)).then_some(face)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Outward normal of a planar face.
|
||||||
|
pub fn planar_face_normal(body: &Body, face: FaceKey) -> Option<[f64; 3]> {
|
||||||
|
let face = body.faces.get(face)?;
|
||||||
|
let Surface::Plane(plane) = body.surfaces.get(face.surface)? else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let normal = cadkernel::space::Vec3::from(plane.normal()?);
|
||||||
|
Some(if face.forward { normal } else { -normal }.to_array())
|
||||||
|
}
|
||||||
|
|
||||||
// ── Boolean operations ──────────────────────────────────────────────────────
|
// ── Boolean operations ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Which CSG to apply. Mirrors `model::boolean_cmd::BoolOp` but kept local so
|
/// Which CSG to apply. Mirrors `model::boolean_cmd::BoolOp` but kept local so
|
||||||
@@ -246,17 +292,12 @@ pub fn boolean(op: Bool, a: &Body, b: &Body) -> Option<Body> {
|
|||||||
Bool::Subtract => brep::Operation::Difference,
|
Bool::Subtract => brep::Operation::Difference,
|
||||||
Bool::Intersect => brep::Operation::Intersection,
|
Bool::Intersect => brep::Operation::Intersection,
|
||||||
};
|
};
|
||||||
brep::combine(a.clone(), b.clone(), how, TOL).ok()
|
let tolerance = brep::operation_tolerance(&[a, b]);
|
||||||
|
brep::combine(a.clone(), b.clone(), how, tolerance).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tessellation ────────────────────────────────────────────────────────────
|
// ── Tessellation ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Tessellate a `Body` into a single-LOD `MeshLodSet` (world-space, before
|
|
||||||
/// world_offset is applied by the caller).
|
|
||||||
pub fn mesh_from_solid(body: &Body, color: [f32; 4]) -> Option<MeshLodSet> {
|
|
||||||
mesh_from_tessellation(tessellation(body), color)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mesh_from_tessellation(
|
fn mesh_from_tessellation(
|
||||||
tessellation: brep::mesh::BodyMesh,
|
tessellation: brep::mesh::BodyMesh,
|
||||||
color: [f32; 4],
|
color: [f32; 4],
|
||||||
|
|||||||
@@ -1,20 +1,8 @@
|
|||||||
// EXTRUDE / REVOLVE: turning a drawn profile into a solid.
|
// Exact profile sweeps stored as kernel B-reps and ACIS.
|
||||||
//
|
|
||||||
// The profile comes from `entities::curve`, which is where every entity's
|
|
||||||
// geometry is already defined once, so a circle, an arc-bulged polyline and a
|
|
||||||
// closed spline all arrive as the same thing: a plane plus a chain of curves
|
|
||||||
// in that plane's coordinates. The kernel sweeps that chain into analytic
|
|
||||||
// surfaces — a straight run into a plane, an arc into a cylinder, a profile
|
|
||||||
// turned about an axis into a cone, a sphere or a torus — so the result can
|
|
||||||
// be written back out as ACIS rather than as facets.
|
|
||||||
//
|
|
||||||
// A spline profile has no analytic side wall, so it is refused rather than
|
|
||||||
// approximated into a surface nobody asked for. That is the kernel's answer
|
|
||||||
// and this module passes it on.
|
|
||||||
|
|
||||||
use cadkernel::brep::{self, Body};
|
use cadkernel::brep::{self, Body};
|
||||||
use cadkernel::geom2d::Curve;
|
use cadkernel::geom2d::{offset_polyline, Arc, Curve, EllipseArc, Line, Polyline};
|
||||||
use cadkernel::space::{PlanarCurve, Plane};
|
use cadkernel::space::{PlanarCurve, Plane, Vec3};
|
||||||
use acadrust::EntityType;
|
use acadrust::EntityType;
|
||||||
|
|
||||||
use crate::entities::curve::entity_curve;
|
use crate::entities::curve::entity_curve;
|
||||||
@@ -40,13 +28,17 @@ pub fn profile_of(entity: &EntityType) -> Option<Profile> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let pieces = match &planar.curve {
|
let pieces = match &planar.curve {
|
||||||
// A polyline is already a chain. Its own segments carry the bulges,
|
|
||||||
// so an arc stays an arc rather than becoming a run of chords.
|
|
||||||
Curve::Polyline(_) => planar.curve.segments(),
|
Curve::Polyline(_) => planar.curve.segments(),
|
||||||
// Everything else closed on itself is cut into quarters, which is
|
|
||||||
// both the fewest pieces a chain may have and the fewest that leave
|
|
||||||
// each one unambiguous about which way round it goes.
|
|
||||||
Curve::Circle(circle) => quarters(circle.centre, circle.radius),
|
Curve::Circle(circle) => quarters(circle.centre, circle.radius),
|
||||||
|
Curve::Arc(arc) => split_arc(*arc),
|
||||||
|
Curve::Ellipse(arc) => split_ellipse(*arc),
|
||||||
|
Curve::Nurbs(curve) => (0..4)
|
||||||
|
.map(|part| {
|
||||||
|
curve
|
||||||
|
.trimmed(part as f64 / 4.0, (part + 1) as f64 / 4.0)
|
||||||
|
.map(Curve::Nurbs)
|
||||||
|
})
|
||||||
|
.collect::<Option<Vec<_>>>()?,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
(pieces.len() >= 3).then_some(Profile {
|
(pieces.len() >= 3).then_some(Profile {
|
||||||
@@ -57,7 +49,6 @@ pub fn profile_of(entity: &EntityType) -> Option<Profile> {
|
|||||||
|
|
||||||
/// A circle as four arcs.
|
/// A circle as four arcs.
|
||||||
fn quarters(centre: [f64; 2], radius: f64) -> Vec<Curve> {
|
fn quarters(centre: [f64; 2], radius: f64) -> Vec<Curve> {
|
||||||
use cadkernel::geom2d::Arc;
|
|
||||||
use std::f64::consts::FRAC_PI_2;
|
use std::f64::consts::FRAC_PI_2;
|
||||||
(0..4)
|
(0..4)
|
||||||
.map(|quarter| {
|
.map(|quarter| {
|
||||||
@@ -72,6 +63,34 @@ fn quarters(centre: [f64; 2], radius: f64) -> Vec<Curve> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn split_arc(arc: Arc) -> Vec<Curve> {
|
||||||
|
let start = arc.start_angle;
|
||||||
|
let step = arc.sweep() / 4.0;
|
||||||
|
(0..4)
|
||||||
|
.map(|part| {
|
||||||
|
Curve::Arc(Arc {
|
||||||
|
start_angle: start + step * part as f64,
|
||||||
|
end_angle: start + step * (part + 1) as f64,
|
||||||
|
..arc
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_ellipse(arc: EllipseArc) -> Vec<Curve> {
|
||||||
|
let start = arc.start_parameter;
|
||||||
|
let step = arc.sweep() / 4.0;
|
||||||
|
(0..4)
|
||||||
|
.map(|part| {
|
||||||
|
Curve::Ellipse(EllipseArc {
|
||||||
|
ellipse: arc.ellipse,
|
||||||
|
start_parameter: start + step * part as f64,
|
||||||
|
end_parameter: start + step * (part + 1) as f64,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// EXTRUDE: drag the profile `height` along its own plane's normal.
|
/// EXTRUDE: drag the profile `height` along its own plane's normal.
|
||||||
///
|
///
|
||||||
/// `None` for a profile that does not close, encloses nothing, or holds a
|
/// `None` for a profile that does not close, encloses nothing, or holds a
|
||||||
@@ -86,6 +105,13 @@ pub fn extruded(entity: &EntityType, height: f64) -> Option<Body> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Signed distance of a drag along a profile's normal.
|
||||||
|
pub fn projected_drag(entity: &EntityType, from: glam::DVec3, to: glam::DVec3) -> Option<f64> {
|
||||||
|
let normal = entity_curve(entity)?.plane.normal()?;
|
||||||
|
let distance = (to - from).dot(glam::DVec3::from_array(normal));
|
||||||
|
(distance.is_finite() && distance.abs() > 1e-6).then_some(distance)
|
||||||
|
}
|
||||||
|
|
||||||
/// REVOLVE: turn the profile about the axis from `from` to `to` by `angle`
|
/// REVOLVE: turn the profile about the axis from `from` to `to` by `angle`
|
||||||
/// radians.
|
/// radians.
|
||||||
///
|
///
|
||||||
@@ -108,85 +134,191 @@ pub fn revolved(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::scene::model::mesh_model::{MeshLodSet, MeshModel};
|
/// SWEEP as an exact B-rep along straight and circular path pieces.
|
||||||
|
pub fn swept(profile: &EntityType, path: &EntityType) -> Option<Body> {
|
||||||
/// SWEEP through the kernel's tolerance-driven mesh API.
|
let profile = profile_of(profile)?;
|
||||||
pub fn swept(profile: &EntityType, path: &EntityType, color: [f32; 4]) -> Option<MeshLodSet> {
|
let path = entity_curve(path)?;
|
||||||
let max_angle = cadkernel::tessellation::DEFAULT_ANGLE;
|
let pieces = match &path.curve {
|
||||||
let surface = brep::mesh::sweep_surface(
|
Curve::Line(line) => vec![Curve::Line(*line)],
|
||||||
&entity_curve(profile)?,
|
Curve::Arc(arc) => vec![Curve::Arc(*arc)],
|
||||||
&entity_curve(path)?,
|
Curve::Polyline(_) => path.curve.segments(),
|
||||||
max_angle,
|
_ => return None,
|
||||||
)?;
|
};
|
||||||
mesh_set(surface, color, 1e-9)
|
brep::sweep_along(profile.plane, &profile.pieces, path.plane, &pieces)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// LOFT through the kernel's tolerance-driven mesh API.
|
/// LOFT as an exact B-rep through compatible closed sections.
|
||||||
pub fn lofted(profiles: &[EntityType], color: [f32; 4]) -> Option<MeshLodSet> {
|
pub fn lofted(profiles: &[EntityType]) -> Option<Body> {
|
||||||
let curves: Vec<PlanarCurve> = profiles.iter().filter_map(entity_curve).collect();
|
if let Some(body) = circular_loft(profiles) {
|
||||||
let max_angle = cadkernel::tessellation::DEFAULT_ANGLE;
|
return Some(body);
|
||||||
mesh_set(brep::mesh::loft_surface(&curves, max_angle)?, color, 1e-9)
|
}
|
||||||
|
let sections = profiles
|
||||||
|
.iter()
|
||||||
|
.map(|entity| {
|
||||||
|
let profile = profile_of(entity)?;
|
||||||
|
Some((profile.plane, profile.pieces))
|
||||||
|
})
|
||||||
|
.collect::<Option<Vec<_>>>()?;
|
||||||
|
brep::loft(§ions)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mesh_set(
|
/// Builds an exact wall solid around a polyline centreline.
|
||||||
surface: brep::mesh::SurfaceMesh,
|
pub fn polysolid(entity: &EntityType, width: f64, height: f64) -> Option<Body> {
|
||||||
color: [f32; 4],
|
if !width.is_finite() || !height.is_finite() || width <= 0.0 || height.abs() <= 1e-12 {
|
||||||
precision: f64,
|
|
||||||
) -> Option<MeshLodSet> {
|
|
||||||
if surface.mesh.is_empty() {
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut verts = Vec::with_capacity(surface.mesh.positions.len());
|
let planar = entity_curve(entity)?;
|
||||||
let mut verts_low = Vec::with_capacity(surface.mesh.positions.len());
|
let Curve::Polyline(source) = planar.curve else {
|
||||||
for point in &surface.mesh.positions {
|
return None;
|
||||||
push_point(&mut verts, &mut verts_low, *point);
|
};
|
||||||
|
let (left_pick, right_pick) = offset_picks(&source, width)?;
|
||||||
|
let mut left = offset_polyline(&source, width * 0.5, left_pick);
|
||||||
|
let mut right = offset_polyline(&source, width * 0.5, right_pick);
|
||||||
|
if left.len() != 1 || right.len() != 1 {
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
let silhouette = surface.silhouette_source(precision);
|
let left = left.remove(0);
|
||||||
let mut set = MeshLodSet::from_single(MeshModel {
|
let right = right.remove(0);
|
||||||
name: String::new(),
|
let normal = Vec3::from(planar.plane.normal()?);
|
||||||
verts,
|
let direction = (normal * height).to_array();
|
||||||
verts_low,
|
|
||||||
normals: surface
|
if source.closed {
|
||||||
.mesh
|
if !left.closed || !right.closed {
|
||||||
.normals
|
return None;
|
||||||
.iter()
|
|
||||||
.map(|normal| [normal[0] as f32, normal[1] as f32, normal[2] as f32])
|
|
||||||
.collect(),
|
|
||||||
indices: surface
|
|
||||||
.mesh
|
|
||||||
.triangles
|
|
||||||
.iter()
|
|
||||||
.flatten()
|
|
||||||
.map(|index| *index as u32)
|
|
||||||
.collect(),
|
|
||||||
triangle_material_handles: Vec::new(),
|
|
||||||
triangle_colors: Vec::new(),
|
|
||||||
color,
|
|
||||||
selected: false,
|
|
||||||
});
|
|
||||||
{
|
|
||||||
let (high, low) = (&mut set.edge_verts, &mut set.edge_verts_low);
|
|
||||||
for edge in surface.edges {
|
|
||||||
for segment in edge.windows(2) {
|
|
||||||
for point in segment {
|
|
||||||
push_point(high, low, *point);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
let mut profiles = [left, right]
|
||||||
|
.into_iter()
|
||||||
|
.map(|polyline| {
|
||||||
|
let area = Curve::Polyline(polyline.clone()).enclosed_area().abs();
|
||||||
|
(area, Curve::Polyline(polyline).segments())
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
profiles.sort_by(|first, second| second.0.total_cmp(&first.0));
|
||||||
|
if profiles[0].0 <= profiles[1].0 || profiles[1].0 <= 1e-12 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
return brep::extrude_region(
|
||||||
|
planar.plane,
|
||||||
|
&[profiles.remove(0).1, profiles.remove(0).1],
|
||||||
|
direction,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
set.curved_gens
|
|
||||||
.push(super::mesh_model::CurvedGen { source: silhouette });
|
if left.closed || right.closed {
|
||||||
Some(set)
|
return None;
|
||||||
|
}
|
||||||
|
let left_start = left.vertices.first()?.position;
|
||||||
|
let left_end = left.vertices.last()?.position;
|
||||||
|
let right_start = right.vertices.first()?.position;
|
||||||
|
let right_end = right.vertices.last()?.position;
|
||||||
|
let mut outline = Curve::Polyline(left).segments();
|
||||||
|
outline.push(Curve::Line(Line {
|
||||||
|
start: left_end,
|
||||||
|
end: right_end,
|
||||||
|
}));
|
||||||
|
let mut back = Curve::Polyline(right).segments();
|
||||||
|
back.reverse();
|
||||||
|
outline.extend(back);
|
||||||
|
outline.push(Curve::Line(Line {
|
||||||
|
start: right_start,
|
||||||
|
end: left_start,
|
||||||
|
}));
|
||||||
|
brep::extrude(planar.plane, &outline, direction)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_point(high: &mut Vec<[f32; 3]>, low: &mut Vec<[f32; 3]>, point: [f64; 3]) {
|
fn offset_picks(polyline: &Polyline, width: f64) -> Option<([f64; 2], [f64; 2])> {
|
||||||
let coarse = [point[0] as f32, point[1] as f32, point[2] as f32];
|
let start = polyline.vertices.first()?.position;
|
||||||
high.push(coarse);
|
let next = polyline.vertices.get(1)?.position;
|
||||||
low.push([
|
let along = if let Some(arc) = polyline.segment_arc(0) {
|
||||||
(point[0] - coarse[0] as f64) as f32,
|
let near = arc.sample(1e-6);
|
||||||
(point[1] - coarse[1] as f64) as f32,
|
[near[0] - start[0], near[1] - start[1]]
|
||||||
(point[2] - coarse[2] as f64) as f32,
|
} else {
|
||||||
]);
|
[next[0] - start[0], next[1] - start[1]]
|
||||||
|
};
|
||||||
|
let length = along[0].hypot(along[1]);
|
||||||
|
if length <= 1e-12 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let side = [-along[1] / length * width, along[0] / length * width];
|
||||||
|
Some((
|
||||||
|
[start[0] + side[0], start[1] + side[1]],
|
||||||
|
[start[0] - side[0], start[1] - side[1]],
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn circular_loft(profiles: &[EntityType]) -> Option<Body> {
|
||||||
|
if profiles.len() < 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let sections = profiles
|
||||||
|
.iter()
|
||||||
|
.map(|entity| {
|
||||||
|
let planar = entity_curve(entity)?;
|
||||||
|
let Curve::Circle(circle) = planar.curve else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some((
|
||||||
|
Vec3::from(planar.plane.point_at(circle.centre)),
|
||||||
|
circle.radius,
|
||||||
|
Vec3::from(planar.plane.normal()?),
|
||||||
|
Vec3::from(planar.plane.x_axis),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<Option<Vec<_>>>()?;
|
||||||
|
let first = sections.first()?;
|
||||||
|
let direction = (sections.last()?.0 - first.0).normalize()?;
|
||||||
|
let scale = sections
|
||||||
|
.iter()
|
||||||
|
.map(|(centre, radius, _, _)| centre.length().max(*radius))
|
||||||
|
.fold(1.0_f64, f64::max);
|
||||||
|
let tolerance = scale * 1e-9;
|
||||||
|
let mut heights = Vec::with_capacity(sections.len());
|
||||||
|
for (centre, radius, normal, _) in §ions {
|
||||||
|
if !radius.is_finite()
|
||||||
|
|| *radius <= tolerance
|
||||||
|
|| normal.dot(direction).abs() < 1.0 - 1e-9
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let offset = *centre - first.0;
|
||||||
|
if (offset - direction * offset.dot(direction)).length() > tolerance {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
heights.push(offset.dot(direction));
|
||||||
|
}
|
||||||
|
if heights
|
||||||
|
.windows(2)
|
||||||
|
.any(|pair| pair[1] <= pair[0] + tolerance)
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let radial_seed = first.3 - direction * first.3.dot(direction);
|
||||||
|
let radial = radial_seed.normalize()?;
|
||||||
|
let profile_plane = Plane::from_axes(first.0.to_array(), radial.to_array(), direction.to_array());
|
||||||
|
let mut points = Vec::with_capacity(sections.len() + 2);
|
||||||
|
points.push([0.0, 0.0]);
|
||||||
|
points.extend(
|
||||||
|
sections
|
||||||
|
.iter()
|
||||||
|
.zip(&heights)
|
||||||
|
.map(|((_, radius, _, _), height)| [*radius, *height]),
|
||||||
|
);
|
||||||
|
points.push([0.0, *heights.last()?]);
|
||||||
|
let profile = (0..points.len())
|
||||||
|
.map(|index| {
|
||||||
|
Curve::Line(Line {
|
||||||
|
start: points[index],
|
||||||
|
end: points[(index + 1) % points.len()],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
brep::revolve(
|
||||||
|
profile_plane,
|
||||||
|
&profile,
|
||||||
|
first.0.to_array(),
|
||||||
|
direction.to_array(),
|
||||||
|
std::f64::consts::TAU,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -807,7 +807,7 @@ impl Scene {
|
|||||||
let Ok(body) = cadkernel::acis::rebuild_body(&operation) else {
|
let Ok(body) = cadkernel::acis::rebuild_body(&operation) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let Some(document) = crate::scene::convert::acis_export::planar_solid_to_sat(&body) else {
|
let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&body) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
self.record_solid_history_before(handle);
|
self.record_solid_history_before(handle);
|
||||||
@@ -833,7 +833,7 @@ impl Scene {
|
|||||||
let Ok(body) = cadkernel::acis::rebuild_body(&operation) else {
|
let Ok(body) = cadkernel::acis::rebuild_body(&operation) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let Some(document) = crate::scene::convert::acis_export::planar_solid_to_sat(&body) else {
|
let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&body) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let Some(EntityType::Solid3D(entity)) = self.document.get_entity_mut(handle) else {
|
let Some(EntityType::Solid3D(entity)) = self.document.get_entity_mut(handle) else {
|
||||||
|
|||||||