diff --git a/Cargo.lock b/Cargo.lock
index 32a1413b..189c1b22 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -72,7 +72,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
[[package]]
name = "acadrust"
version = "0.4.1"
-source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=db73dab#db73dab540dbfb690c93f4416014262e6cf7bf42"
+source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd"
dependencies = [
"ahash 0.8.12",
"anyhow",
@@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cadkernel"
version = "0.1.0"
-source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=467c3fd#467c3fd02f419ab5af4030691699b34b25d2f51a"
+source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=76de3b3#76de3b35e55a45f3ffb813c499cf3affe176351d"
dependencies = [
"acadrust",
"cavalier_contours",
diff --git a/Cargo.toml b/Cargo.toml
index ec1734bb..4cc5ae75 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -27,8 +27,8 @@ glam = { version = "0.33", features = ["bytemuck"] }
rfd = "0.17"
clap = { version = "4", features = ["derive"] }
env_logger = "0.11"
-acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "db73dab", features = ["serde"] }
-cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "467c3fd", features = ["acis", "offset"] }
+acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "94df2c3", features = ["serde"] }
+cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "76de3b3", features = ["acis", "offset"] }
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
flate2 = "1"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
diff --git a/assets/icons/extrude.svg b/assets/icons/extrude.svg
new file mode 100644
index 00000000..d864bce3
--- /dev/null
+++ b/assets/icons/extrude.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/loft.svg b/assets/icons/loft.svg
new file mode 100644
index 00000000..99e9cf62
--- /dev/null
+++ b/assets/icons/loft.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/polysolid.svg b/assets/icons/polysolid.svg
new file mode 100644
index 00000000..8532c376
--- /dev/null
+++ b/assets/icons/polysolid.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/presspull.svg b/assets/icons/presspull.svg
new file mode 100644
index 00000000..189e6909
--- /dev/null
+++ b/assets/icons/presspull.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/pyramid3d.svg b/assets/icons/pyramid3d.svg
new file mode 100644
index 00000000..a7d26f91
--- /dev/null
+++ b/assets/icons/pyramid3d.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/revolve.svg b/assets/icons/revolve.svg
new file mode 100644
index 00000000..48073ba9
--- /dev/null
+++ b/assets/icons/revolve.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/sweep.svg b/assets/icons/sweep.svg
new file mode 100644
index 00000000..4db7d373
--- /dev/null
+++ b/assets/icons/sweep.svg
@@ -0,0 +1,4 @@
+
diff --git a/crates/ocs_plugin_api/Cargo.toml b/crates/ocs_plugin_api/Cargo.toml
index bba21d9c..6f6feee5 100644
--- a/crates/ocs_plugin_api/Cargo.toml
+++ b/crates/ocs_plugin_api/Cargo.toml
@@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] }
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
# `HostApi` runtime surface. The default crate stays dependency-free so engine
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
-acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "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).
interprocess = { version = "2", optional = true }
@@ -37,7 +37,7 @@ serde_json = "1"
serde = { version = "1", features = ["derive"] }
cargo-lock = "11"
# acadrust is scanned at build time to generate the embedded type registry.
-acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "db73dab", features = ["serde"] }
+acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "94df2c3", features = ["serde"] }
[dev-dependencies]
serde_json = "1"
diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml
index ff034dd7..b3a052b0 100644
--- a/crates/ocs_web_worker/Cargo.toml
+++ b/crates/ocs_web_worker/Cargo.toml
@@ -8,7 +8,7 @@ publish = false
crate-type = ["cdylib"]
[dependencies]
-acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "db73dab", features = ["serde"] }
+acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "94df2c3", features = ["serde"] }
bincode = "1.3"
serde = { version = "1", features = ["derive"] }
console_error_panic_hook = "0.1"
diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl
index 62a03272..5c48a3ce 100644
--- a/locales/ar-SA/opencadstudio.ftl
+++ b/locales/ar-SA/opencadstudio.ftl
@@ -30,7 +30,7 @@ ribbon-tab =
.draw = رسم
.annotate = تعليقات توضيحية
.insert = إدراج
- .model = نموذج
+ .model = النمذجة
.layout = تخطيط
.manage = إدارة
.view = عرض
@@ -2047,6 +2047,15 @@ insert =
.object-s-cut-to-clipboard = قُص __ocs_fmt_0__ كائن إلى الحافظة.
model =
+ .create = إنشاء
+ .boolean = عمليات منطقية
+ .pyramid = هرم
+ .polysolid = مجسّم متعدد
+ .extrude = بثق
+ .revolve = تدوير
+ .loft = تشكيل انتقالي
+ .sweep = سحب على مسار
+ .presspull = دفع/سحب
.n-specify-center-point = __ocs_arg_n__ حدد نقطة المركز:
.n-specify-first-corner = __ocs_arg_n__ حدد الزاوية الأولى:
.n-specify-height-enter-for-default = __ocs_arg_n__ حدد الارتفاع :
diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl
index fe6994b8..2df4f555 100644
--- a/locales/bg-BG/opencadstudio.ftl
+++ b/locales/bg-BG/opencadstudio.ftl
@@ -30,7 +30,7 @@ ribbon-tab =
.draw = Чертане
.annotate = Анотиране
.insert = Вмъкване
- .model = Модел
+ .model = Моделиране
.layout = Оформление
.manage = Управление
.view = Изглед
@@ -2034,6 +2034,15 @@ insert =
.object-s-cut-to-clipboard = __ocs_fmt_0__ обект(а) изрязани в клипборда.
model =
+ .create = Създаване
+ .boolean = Булеви операции
+ .pyramid = Пирамида
+ .polysolid = Полисолид
+ .extrude = Екструдиране
+ .revolve = Завъртане
+ .loft = Лофт
+ .sweep = Изтегляне по траектория
+ .presspull = Натискане/издърпване
.n-specify-center-point = __ocs_arg_n__ Посочи централна точка:
.n-specify-first-corner = __ocs_arg_n__ Посочи първи ъгъл:
.n-specify-height-enter-for-default = __ocs_arg_n__ Посочи височина :
diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl
index 92c95855..f3a9b7f4 100644
--- a/locales/cs-CZ/opencadstudio.ftl
+++ b/locales/cs-CZ/opencadstudio.ftl
@@ -30,7 +30,7 @@ ribbon-tab =
.draw = Kreslení
.annotate = Poznámky
.insert = Vložit
- .model = Model
+ .model = Modelování
.layout = Rozvržení
.manage = Správa
.view = Zobrazení
@@ -2034,6 +2034,15 @@ insert =
.object-s-cut-to-clipboard = Objekt(y) __ocs_fmt_0__ vyříznutý do schránky.
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-first-corner = __ocs_arg_n__ Určete první roh:
.n-specify-height-enter-for-default = __ocs_arg_n__ Zadejte výšku :
diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl
index 87f3b152..ac4eb8dc 100644
--- a/locales/de-DE/opencadstudio.ftl
+++ b/locales/de-DE/opencadstudio.ftl
@@ -30,7 +30,7 @@ ribbon-tab =
.draw = Zeichnen
.annotate = Beschriften
.insert = Einsatz
- .model = Modell
+ .model = Modellierung
.layout = Layout
.manage = Verwalten
.view = Ansicht
@@ -2033,6 +2033,15 @@ insert =
.object-s-cut-to-clipboard = __ocs_fmt_0__ Objekt(e) in die Zwischenablage geschnitten.
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-first-corner = __ocs_arg_n__ Erste Ecke angeben:
.n-specify-height-enter-for-default = __ocs_arg_n__ Höhe angeben :
diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl
index 6386d0d6..71bd8ff2 100644
--- a/locales/en-US/opencadstudio.ftl
+++ b/locales/en-US/opencadstudio.ftl
@@ -30,7 +30,7 @@ ribbon-tab =
.draw = Draw
.annotate = Annotate
.insert = Insert
- .model = Model
+ .model = Modelling
.layout = Layout
.manage = Manage
.view = View
@@ -2063,6 +2063,15 @@ insert =
.object-s-cut-to-clipboard = __ocs_fmt_0__ object(s) cut to clipboard.
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-first-corner = __ocs_arg_n__ Specify first corner:
.n-specify-height-enter-for-default = __ocs_arg_n__ Specify height :
diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl
index 3f4a04fe..a929a1f0 100644
--- a/locales/es-ES/opencadstudio.ftl
+++ b/locales/es-ES/opencadstudio.ftl
@@ -30,7 +30,7 @@ ribbon-tab =
.draw = Dibujo
.annotate = Anotar
.insert = Insertar
- .model = Modelo
+ .model = Modelado
.layout = Presentación
.manage = Administrar
.view = Vista
@@ -2035,6 +2035,15 @@ insert =
.object-s-cut-to-clipboard = Se cortaron __ocs_fmt_0__ objetos al portapapeles.
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-first-corner = __ocs_arg_n__ Especifique la primera esquina:
.n-specify-height-enter-for-default = __ocs_arg_n__ Especifique la altura :
diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl
index 08717ec7..8399b91f 100644
--- a/locales/fi-FI/opencadstudio.ftl
+++ b/locales/fi-FI/opencadstudio.ftl
@@ -30,7 +30,7 @@ ribbon-tab =
.draw = Piirrä
.annotate = Merkinnät
.insert = Lisää
- .model = Malli
+ .model = Mallinnus
.layout = Asettelu
.manage = Hallinta
.view = Näkymä
@@ -2034,6 +2034,15 @@ insert =
.object-s-cut-to-clipboard = __ocs_fmt_0__ objekti(t) leikattu leikepöydälle.
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-first-corner = __ocs_arg_n__ Määritä ensimmäinen kulma:
.n-specify-height-enter-for-default = __ocs_arg_n__ Määritä korkeus :
diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl
index d2dbe5e9..f0ced22a 100644
--- a/locales/fr-FR/opencadstudio.ftl
+++ b/locales/fr-FR/opencadstudio.ftl
@@ -30,7 +30,7 @@ ribbon-tab =
.draw = Dessin
.annotate = Annoter
.insert = Insérer
- .model = Modèle
+ .model = Modélisation
.layout = Mise en page
.manage = Gérer
.view = Affichage
@@ -2035,6 +2035,15 @@ insert =
.object-s-cut-to-clipboard = Objets coupés vers le presse-papiers : __ocs_fmt_0__.
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-first-corner = __ocs_arg_n__ Spécifiez le premier coin :
.n-specify-height-enter-for-default = __ocs_arg_n__ Spécifiez la hauteur :
diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl
index d8f40a6c..4356de5c 100644
--- a/locales/hi-IN/opencadstudio.ftl
+++ b/locales/hi-IN/opencadstudio.ftl
@@ -30,7 +30,7 @@ ribbon-tab =
.draw = ड्रॉ
.annotate = एनोटेट
.insert = सम्मिलित करें
- .model = मॉडल
+ .model = मॉडलिंग
.layout = लेआउट
.manage = प्रबंधित करें
.view = देखें
@@ -2023,6 +2023,15 @@ insert =
.object-s-cut-to-clipboard = __ocs_fmt_0__ ऑब्जेक्ट क्लिपबोर्ड पर काटे गए।
model =
+ .create = बनाएँ
+ .boolean = बूलियन संक्रियाएँ
+ .pyramid = पिरामिड
+ .polysolid = बहुखंड ठोस
+ .extrude = एक्सट्रूड
+ .revolve = घुमाएँ
+ .loft = लॉफ्ट
+ .sweep = स्वीप
+ .presspull = दबाएँ/खींचें
.n-specify-center-point = __ocs_arg_n__ केंद्र बिंदु निर्दिष्ट करें:
.n-specify-first-corner = __ocs_arg_n__ पहले कोने को निर्दिष्ट करें:
.n-specify-height-enter-for-default = __ocs_arg_n__ ऊंचाई निर्दिष्ट करें के लिए दर्ज करें>:
diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl
index 0b7a8f7b..af07478f 100644
--- a/locales/hu-HU/opencadstudio.ftl
+++ b/locales/hu-HU/opencadstudio.ftl
@@ -30,7 +30,7 @@ ribbon-tab =
.draw = Rajz
.annotate = Feliratozás
.insert = Beszúrás
- .model = Modell
+ .model = Modellezés
.layout = Elrendezés
.manage = Kezelés
.view = Nézet
@@ -2034,6 +2034,15 @@ insert =
.object-s-cut-to-clipboard = __ocs_fmt_0__ objektum(ok) vágólapra vágva.
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-first-corner = __ocs_arg_n__ Adja meg az első sarkot:
.n-specify-height-enter-for-default = __ocs_arg_n__ Adja meg a magasságot:
diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl
index 8c915bb8..6ab5b504 100644
--- a/locales/it-IT/opencadstudio.ftl
+++ b/locales/it-IT/opencadstudio.ftl
@@ -30,7 +30,7 @@ ribbon-tab =
.draw = Disegno
.annotate = Annotazione
.insert = Inserisci
- .model = Modello
+ .model = Modellazione
.layout = Disposizione
.manage = Gestisci
.view = Vista
@@ -2034,6 +2034,15 @@ insert =
.object-s-cut-to-clipboard = __ocs_fmt_0__ oggetto/i tagliato negli appunti.
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-first-corner = __ocs_arg_n__ Specificare il primo angolo:
.n-specify-height-enter-for-default = __ocs_arg_n__ Specificare l'altezza :
diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl
index 4abf157c..7531bc56 100644
--- a/locales/ja-JP/opencadstudio.ftl
+++ b/locales/ja-JP/opencadstudio.ftl
@@ -30,7 +30,7 @@ ribbon-tab =
.draw = 作図
.annotate = 注釈
.insert = 挿入
- .model = モデル
+ .model = モデリング
.layout = レイアウト
.manage = 管理
.view = 表示
@@ -2023,6 +2023,15 @@ insert =
.object-s-cut-to-clipboard = __ocs_fmt_0__ 個のオブジェクトを切り取ってクリップボードに保存しました。
model =
+ .create = 作成
+ .boolean = ブール演算
+ .pyramid = ピラミッド
+ .polysolid = ポリソリッド
+ .extrude = 押し出し
+ .revolve = 回転
+ .loft = ロフト
+ .sweep = スイープ
+ .presspull = プレスプル
.n-specify-center-point = __ocs_arg_n__ 中心点を指定:
.n-specify-first-corner = __ocs_arg_n__ 最初のコーナーを指定:
.n-specify-height-enter-for-default = __ocs_arg_n__ 高さを指定 :
diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl
index 9c1b5738..0fd664f8 100644
--- a/locales/ko-KR/opencadstudio.ftl
+++ b/locales/ko-KR/opencadstudio.ftl
@@ -30,7 +30,7 @@ ribbon-tab =
.draw = 그리기
.annotate = 주석
.insert = 삽입
- .model = 모델
+ .model = 모델링
.layout = 배치
.manage = 관리
.view = 보기
@@ -2034,6 +2034,15 @@ insert =
.object-s-cut-to-clipboard = __ocs_fmt_0__ 개체를 클립보드로 잘라냈습니다.
model =
+ .create = 생성
+ .boolean = 불리언 연산
+ .pyramid = 피라미드
+ .polysolid = 폴리솔리드
+ .extrude = 돌출
+ .revolve = 회전
+ .loft = 로프트
+ .sweep = 스윕
+ .presspull = 누르기/당기기
.n-specify-center-point = __ocs_arg_n__ 중심점 지정:
.n-specify-first-corner = __ocs_arg_n__ 첫 번째 구석 지정:
.n-specify-height-enter-for-default = __ocs_arg_n__ 높이 지정 :
diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl
index 876a6cbf..a2abe740 100644
--- a/locales/nl-NL/opencadstudio.ftl
+++ b/locales/nl-NL/opencadstudio.ftl
@@ -30,7 +30,7 @@ ribbon-tab =
.draw = Tekenen
.annotate = Annoteren
.insert = Invoegen
- .model = Model
+ .model = Modelleren
.layout = Indeling
.manage = Beheren
.view = Beeld
@@ -2035,6 +2035,15 @@ insert =
.object-s-cut-to-clipboard = __ocs_fmt_0__ object(en) naar het klembord geknipt.
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-first-corner = __ocs_arg_n__ Geef eerste hoek:
.n-specify-height-enter-for-default = __ocs_arg_n__ Hoogte opgeven:
diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl
index 9ac73584..9f3d339d 100644
--- a/locales/pl-PL/opencadstudio.ftl
+++ b/locales/pl-PL/opencadstudio.ftl
@@ -30,7 +30,7 @@ ribbon-tab =
.draw = Rysowanie
.annotate = Opisy
.insert = Wstaw
- .model = Model
+ .model = Modelowanie
.layout = Układ
.manage = Zarządzaj
.view = Widok
@@ -2034,6 +2034,15 @@ insert =
.object-s-cut-to-clipboard = Obiekt(y) __ocs_fmt_0__ wycięty do schowka.
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-first-corner = __ocs_arg_n__ Określ pierwszy narożnik:
.n-specify-height-enter-for-default = __ocs_arg_n__ Podaj wysokość :
diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl
index 4b70122d..0c8d9886 100644
--- a/locales/pt-BR/opencadstudio.ftl
+++ b/locales/pt-BR/opencadstudio.ftl
@@ -30,7 +30,7 @@ ribbon-tab =
.draw = Desenhar
.annotate = Anotar
.insert = Inserir
- .model = Modelo
+ .model = Modelagem
.layout = Layout
.manage = Gerenciar
.view = Exibir
@@ -2035,6 +2035,15 @@ insert =
.object-s-cut-to-clipboard = __ocs_fmt_0__ objetos recortados para a área de transferência.
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-first-corner = __ocs_arg_n__ Especifique o primeiro canto:
.n-specify-height-enter-for-default = __ocs_arg_n__ Especifique a altura :
diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl
index 957709db..37223a15 100644
--- a/locales/ru-RU/opencadstudio.ftl
+++ b/locales/ru-RU/opencadstudio.ftl
@@ -30,7 +30,7 @@ ribbon-tab =
.draw = Начертить
.annotate = Аннотации
.insert = Вставка
- .model = Модель
+ .model = Моделирование
.layout = Расположение
.manage = Управление
.view = Просмотр
@@ -2038,6 +2038,15 @@ insert =
.object-s-cut-to-clipboard = В буфер обмена вырезано объектов: __ocs_fmt_0__.
model =
+ .create = Создание
+ .boolean = Булевы операции
+ .pyramid = Пирамида
+ .polysolid = Политело
+ .extrude = Выдавливание
+ .revolve = Вращение
+ .loft = Лофт
+ .sweep = Сдвиг по траектории
+ .presspull = Нажать/вытянуть
.n-specify-center-point = __ocs_arg_n__ Укажите центральную точку:
.n-specify-first-corner = __ocs_arg_n__ Укажите первый угол:
.n-specify-height-enter-for-default = __ocs_arg_n__ Укажите высоту <Введите по умолчанию>:
diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl
index 4f2d10e7..2f44e370 100644
--- a/locales/tr-TR/opencadstudio.ftl
+++ b/locales/tr-TR/opencadstudio.ftl
@@ -30,7 +30,7 @@ ribbon-tab =
.draw = Çizim
.annotate = Açıklama
.insert = Ekle
- .model = Model
+ .model = Modelleme
.layout = Yerleşim
.manage = Yönet
.view = Görünüm
@@ -2023,6 +2023,15 @@ insert =
.object-s-cut-to-clipboard = __ocs_fmt_0__ nesne panoya kesildi.
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-first-corner = __ocs_arg_n__ İlk köşeyi belirtin:
.n-specify-height-enter-for-default = __ocs_arg_n__ Yüksekliği belirtin :
diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl
index 0fb98a5a..daec9ff0 100644
--- a/locales/zh-CN/opencadstudio.ftl
+++ b/locales/zh-CN/opencadstudio.ftl
@@ -30,7 +30,7 @@ ribbon-tab =
.draw = 绘制
.annotate = 注释
.insert = 插入
- .model = 模型
+ .model = 建模
.layout = 布局
.manage = 管理
.view = 视图
@@ -2018,6 +2018,15 @@ insert =
.object-s-cut-to-clipboard = __ocs_fmt_0__ 对象切入剪贴板。
model =
+ .create = 创建
+ .boolean = 布尔运算
+ .pyramid = 棱锥
+ .polysolid = 多段体
+ .extrude = 拉伸
+ .revolve = 旋转
+ .loft = 放样
+ .sweep = 扫掠
+ .presspull = 按拉
.n-specify-center-point = __ocs_arg_n__ 指定中心点:
.n-specify-first-corner = __ocs_arg_n__ 指定第一个角点:
.n-specify-height-enter-for-default = __ocs_arg_n__ 指定高度(按 Enter 使用默认值):
diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl
index 21f275b3..460ed3f3 100644
--- a/locales/zh-TW/opencadstudio.ftl
+++ b/locales/zh-TW/opencadstudio.ftl
@@ -30,7 +30,7 @@ ribbon-tab =
.draw = 繪製
.annotate = 註解
.insert = 插入
- .model = 模型
+ .model = 建模
.layout = 配置
.manage = 管理
.view = 視圖
@@ -2030,6 +2030,15 @@ insert =
.object-s-cut-to-clipboard = __ocs_fmt_0__ 物件切入剪貼簿。
model =
+ .create = 建立
+ .boolean = 布林運算
+ .pyramid = 角錐體
+ .polysolid = 多段實體
+ .extrude = 擠出
+ .revolve = 旋轉
+ .loft = 放樣
+ .sweep = 掃掠
+ .presspull = 按拉
.n-specify-center-point = __ocs_arg_n__ 指定中心點:
.n-specify-first-corner = __ocs_arg_n__ 指定第一個角點:
.n-specify-height-enter-for-default = __ocs_arg_n__ 指定高度(按 Enter 使用預設值):
diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs
index e84005b7..68fbe0fe 100644
--- a/src/app/command_driver.rs
+++ b/src/app/command_driver.rs
@@ -1204,15 +1204,17 @@ impl OpenCADStudio {
} => {
let label = self.history_label_from_active_cmd(i, "SOLID");
let pending = self.begin_undo(i, label, 1, true);
- self.add_solid_model(entity, *solid, history);
- self.tabs[i].dirty = true;
+ let handle = self.add_solid_model(entity, *solid, history);
+ 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].active_cmd = None;
self.tabs[i].snap_result = None;
self.restore_pre_cmd_tangent();
- if let Some(pd) = pending {
- self.commit_undo_delta(i, pd);
- }
}
CmdResult::CommitAndEditText(entity) => {
let label = self.history_label_from_active_cmd(i, "ENTITY");
@@ -3001,80 +3003,32 @@ impl OpenCADStudio {
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 ────────────────────────────────────────────────────
CmdResult::ExtrudeEntity {
handle,
height,
- color,
+ color: _,
} => {
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_model, sweep_model};
+ use crate::scene::model::sweep_model;
let entity_opt = self.tabs[i].scene.document.get_entity(handle).cloned();
if let Some(entity) = entity_opt {
- // The kernel sweeps the profile into analytic surfaces —
- // a straight run becomes a plane and an arc a cylinder —
- // 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,
- );
+ if let Some(solid) = sweep_model::extruded(&entity, height) {
+ let history = crate::scene::model::solid_history::brep_op(&solid);
let pending = self.begin_undo(i, "EXTRUDE", 1, true);
- let mut s3d = empty_solid3d();
- if let acadrust::EntityType::Solid3D(inner) = &mut s3d {
- inner.wires = solid_model::edge_wires(&solid);
- }
- let new_handle = self.tabs[i].scene.add_entity(s3d);
- self.tabs[i]
- .scene
- .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);
+ let created = self.add_solid_model(empty_solid3d(), solid, history);
+ if !created.is_null() {
+ 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 {
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();
}
+ 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 ────────────────────────────────────────────────────
CmdResult::RevolveEntity {
handle,
axis_start,
axis_end,
angle_deg,
- color,
+ color: _,
} => {
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_model, sweep_model};
+ use crate::scene::model::sweep_model;
let entity_opt = self.tabs[i].scene.document.get_entity(handle).cloned();
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(
&entity,
[
@@ -3117,31 +3125,19 @@ impl OpenCADStudio {
],
[axis_end.x as f64, axis_end.y as f64, axis_end.z as f64],
(angle_deg as f64).to_radians(),
- )
- .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::revolve_op(
- &entity,
- axis_start.to_array(),
- axis_end.to_array(),
- (angle_deg as f64).to_radians(),
- );
+ );
+ if let Some(solid) = result {
+ let history = crate::scene::model::solid_history::brep_op(&solid);
let pending = self.begin_undo(i, "REVOLVE", 1, true);
- let mut s3d = empty_solid3d();
- if let acadrust::EntityType::Solid3D(inner) = &mut s3d {
- inner.wires = solid_model::edge_wires(&solid);
- }
- let new_handle = self.tabs[i].scene.add_entity(s3d);
- self.tabs[i]
- .scene
- .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::tf!("REVOLVE: solid created ({:.0}°).", angle_deg).as_ref());
- if let Some(pd) = pending {
- self.commit_undo_delta(i, pd);
+ let created = self.add_solid_model(empty_solid3d(), solid, history);
+ if !created.is_null() {
+ 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 {
self.command_line
@@ -3160,7 +3156,7 @@ impl OpenCADStudio {
CmdResult::SweepEntity {
profile_handle,
path_handle,
- color,
+ color: _,
} => {
if self.reject_locked_edit(i, profile_handle)
|| self.reject_locked_edit(i, path_handle)
@@ -3169,7 +3165,7 @@ impl OpenCADStudio {
return Task::none();
}
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]
.scene
@@ -3177,39 +3173,21 @@ impl OpenCADStudio {
.get_entity(profile_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
.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 mut entity = empty_solid3d();
- if let acadrust::EntityType::Solid3D(solid) = &mut entity {
- let center = set.metrics.centroid;
- solid.point_of_reference = acadrust::types::Vector3::new(
- center[0], center[1], center[2],
- );
- }
- let new_handle = self.tabs[i].scene.add_entity(entity);
- 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);
+ let history = solid_history::brep_op(&solid);
+ let created = self.add_solid_model(empty_solid3d(), solid, history);
+ if !created.is_null() {
+ 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 {
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 ──────────────────────────────────────────────────────
- CmdResult::LoftEntities { handles, color } => {
+ CmdResult::LoftEntities { handles, color: _ } => {
if let Some(handle) = handles
.iter()
.find(|handle| self.tabs[i].scene.is_layer_locked(**handle))
@@ -3232,34 +3210,23 @@ impl OpenCADStudio {
return Task::none();
}
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 = handles
.iter()
.filter_map(|handle| self.tabs[i].scene.document.get_entity(*handle).cloned())
.collect();
- if let Some(mut set) = sweep_model::lofted(&profiles, color) {
- let history = crate::scene::model::solid_history::loft_op(&profiles);
+ if let Some(solid) = sweep_model::lofted(&profiles) {
let pending = self.begin_undo(i, "LOFT", 1, true);
- let mut entity = empty_solid3d();
- if let acadrust::EntityType::Solid3D(solid) = &mut entity {
- let center = set.metrics.centroid;
- solid.point_of_reference = acadrust::types::Vector3::new(
- center[0], center[1], center[2],
- );
- }
- let new_handle = self.tabs[i].scene.add_entity(entity);
- 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);
+ let history = solid_history::brep_op(&solid);
+ let created = self.add_solid_model(empty_solid3d(), solid, history);
+ if !created.is_null() {
+ 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 {
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();
}
+ 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 {
handle,
name,
diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs
index 1f86f876..efc19f56 100644
--- a/src/app/commands/dim.rs
+++ b/src/app/commands/dim.rs
@@ -1,5 +1,15 @@
use super::*;
+fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option {
+ 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 {
pub(super) fn dispatch_dim(&mut self, cmd: &str, i: usize) -> Option> {
match cmd {
@@ -950,6 +960,20 @@ impl OpenCADStudio {
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" => {
use crate::modules::draw::modify::fillet::FilletCommand;
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" => {
use crate::modules::draw::modify::fillet::ChamferCommand;
let entities: Vec<_> = self.tabs[i]
diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs
index 0b3a06e9..ccf235a1 100644
--- a/src/app/commands/display.rs
+++ b/src/app/commands/display.rs
@@ -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 ────────────────────────────────────────────────────
- // PRESSPULL on a closed boundary creates a solid by extruding it to a
- // 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" => {
+ "EXTRUDE" | "THICKEN" => {
use crate::modules::insert::solid3d_cmds::ExtrudeCommand;
// If a single entity is already selected, skip the pick step.
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);
if selected.len() == 1 {
let handle = selected[0].0;
- let mut cmd = ExtrudeCommand::new(color);
- cmd.on_entity_pick(handle, glam::DVec3::ZERO);
+ let mut cmd = ExtrudeCommand::new_named(cmd, color);
+ 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.tabs[i].active_cmd = Some(Box::new(cmd));
} else {
- let cmd = ExtrudeCommand::new(color);
+ let cmd = ExtrudeCommand::new_named(cmd, color);
self.command_line.push_info(&cmd.prompt());
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" => {
use crate::modules::insert::solid3d_cmds::RevolveCommand;
diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs
index 085cce33..1a12febf 100644
--- a/src/app/commands/draw.rs
+++ b/src/app/commands/draw.rs
@@ -925,14 +925,15 @@ impl OpenCADStudio {
}
// ── 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;
let new_cmd = PrimitiveCommand::new(cmd);
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
- // ── Design commands (solid booleans) ───────────────────────────
+ // ── Solid booleans ─────────────────────────────────────────────
"UNION" | "SUBTRACT" | "INTERSECT" => {
use crate::modules::model::boolean_cmd::BoolOp;
if let Some(op) = BoolOp::from_id(cmd) {
@@ -1033,17 +1034,7 @@ impl OpenCADStudio {
}
}
- // PYRAMID [sides] — create an n-sided pyramid mesh.
- "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));
- }
+ // PYRAMID [sides] keeps the direct numeric form.
cmd if cmd.starts_with("PYRAMID ") || cmd.starts_with("PYR ") => {
let nums: Vec = cmd
.split_whitespace()
diff --git a/src/app/model_ops.rs b/src/app/model_ops.rs
index a279e5e7..1be644f6 100644
--- a/src/app/model_ops.rs
+++ b/src/app/model_ops.rs
@@ -1,15 +1,9 @@
-// 3D solid modelling support on the App: committing Model-tab primitives,
-// 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.
+// Kernel B-rep solid modelling and exact ACIS persistence.
-use acadrust::entities::Solid3D;
-use acadrust::objects::SolidHistoryOperation;
+use acadrust::{
+ entities::Solid3D, objects::SolidHistoryOperation, EntityType, Handle,
+};
use cadkernel::brep::Body;
-use acadrust::{EntityType, Handle};
use iced::Task;
use super::Message;
@@ -18,16 +12,25 @@ use crate::scene::model::solid_history;
use crate::scene::model::solid_model::{self, Bool};
impl super::OpenCADStudio {
- /// Commit a Model-tab solid: add its acadrust entity to the document, then
- /// register the B-rep (caches it for booleans + tessellates it into the
- /// shaded mesh pipeline). Returns the new entity handle.
+ /// Add a solid and register its persistent B-rep.
pub(super) fn add_solid_model(
&mut self,
- entity: EntityType,
+ mut entity: EntityType,
solid: Body,
history: SolidHistoryOperation,
) -> Handle {
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 {
return Handle::NULL;
};
@@ -55,35 +58,183 @@ impl super::OpenCADStudio {
handles
}
- /// Run a boolean (`union` / `subtract` / `intersect`) on exactly two
- /// selected solids whose B-reps can be restored by the kernel.
+ fn replace_solid_body(&mut self, handle: Handle, result: Body, label: &str) -> bool {
+ 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 {
+ 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,
+ ) -> Task {
+ 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 {
let i = self.active_tab;
let handles = self.selected_solid_handles();
- if handles.len() != 2 {
+ if handles.len() < 2 {
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();
}
- 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 {
BoolOp::Union => Bool::Union,
BoolOp::Subtract => Bool::Subtract,
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
- .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();
- };
+ }
self.push_undo_snapshot(i, "BOOLEAN");
- // Remove the two operands (entity + mesh + cached B-rep).
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();
s3d.wires = solid_model::edge_wires(&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 =
/// 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
- /// 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 {
let i = self.active_tab;
let handles = self.selected_solid_handles();
@@ -243,63 +394,34 @@ impl super::OpenCADStudio {
Task::none()
}
- /// POLYSOLID — build a wall-like solid from a selected polyline: one box per
- /// segment (oriented along it, `width` wide, `height` tall) unioned together,
- /// reusing the box primitive + the boolean union path.
+ /// POLYSOLID — build a wall solid around an exact polyline offset.
pub(super) fn solid_polysolid(&mut self, width: f64, height: f64) -> Task {
let i = self.active_tab;
- let found: Option<(Handle, Vec<[f64; 3]>)> = self.tabs[i]
+ let found: Option<(Handle, EntityType)> = self.tabs[i]
.scene
.selected_entities()
.iter()
.filter(|(h, _)| !self.tabs[i].scene.is_layer_locked(*h))
.find_map(|(h, e)| match e {
- EntityType::LwPolyline(_) => crate::entities::curve::entity_curve(e)
- .map(|curve| (*h, crate::entities::curve::curve_points(&curve))),
+ EntityType::LwPolyline(_) => Some((*h, (*e).clone())),
_ => None,
});
- let Some((handle, pts)) = found else {
+ let Some((handle, entity)) = found else {
self.command_line
.push_error(crate::t!("POLYSOLID: select a polyline first.").as_ref());
return Task::none();
};
- let segs: Vec<([f64; 3], [f64; 3])> = pts.windows(2).map(|w| (w[0], w[1])).collect();
- let mut acc: Option = None;
- 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 {
+ let Some(result) = crate::scene::model::sweep_model::polysolid(&entity, width, height)
+ else {
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();
};
+ 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.tabs[i].scene.erase_entities(&[handle]);
let mut s3d = Solid3D::new();
@@ -310,11 +432,14 @@ impl super::OpenCADStudio {
if !h.is_null() {
self.tabs[i].scene.select_entity(h, false);
}
- self.tabs[i].dirty = true;
- self.refresh_properties();
- self.command_line.push_output(crate::tf!(
- "POLYSOLID: built a wall solid from {used} segment(s) (width {width}, height {height})."
- ).as_ref());
+ if !h.is_null() {
+ self.tabs[i].dirty = true;
+ self.refresh_properties();
+ self.command_line.push_output(
+ crate::tf!("POLYSOLID: created a wall solid (width {width}, height {height}).")
+ .as_ref(),
+ );
+ }
Task::none()
}
@@ -507,12 +632,12 @@ impl super::OpenCADStudio {
self.tabs[i].scene.deselect_all();
if !handle.is_null() {
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()
}
diff --git a/src/app/update/file.rs b/src/app/update/file.rs
index 5ea4f4e4..8615be0e 100644
--- a/src/app/update/file.rs
+++ b/src/app/update/file.rs
@@ -258,7 +258,7 @@ impl OpenCADStudio {
let body = scene.solid_models.get(&h);
let sat = needs_acis
.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();
let wires = needs_wires.then(|| {
diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs
index 2409b6a2..379eddbf 100644
--- a/src/app/update/viewport.rs
+++ b/src/app/update/viewport.rs
@@ -3454,6 +3454,38 @@ impl OpenCADStudio {
})?
});
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
// body before `on_entity_pick` can advance.
let inject_first = self.tabs[i]
@@ -3484,7 +3516,8 @@ impl OpenCADStudio {
let result = self.tabs[i].active_cmd.as_mut().map(|c| {
// Shift-swap state for TRIM/EXTEND (#336).
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.
if self.tabs[i]
@@ -4011,8 +4044,7 @@ impl OpenCADStudio {
)
})
.or_else(|| {
- // Block-internal hatch: resolve to the
- // parent Insert (AutoCAD behaviour).
+ // Block-internal hatch: resolve to the parent Insert.
scene::pick::hit_test::click_hit_insert_hatch(
p,
self.tabs[i].scene.insert_hatches_for_click().as_ref(),
diff --git a/src/command.rs b/src/command.rs
index 7cc6382e..bf5b43d3 100644
--- a/src/command.rs
+++ b/src/command.rs
@@ -1425,17 +1425,20 @@ pub enum CmdResult {
/// Translation vector applied once to every selected point.
delta: DVec3,
},
- /// Create a Solid3D placeholder entity + associated MeshModel.
- /// `mesh_fn` is called with the entity's handle string to build the mesh.
- CommitSolid3D {
- mesh_fn: Box Option + Send>,
- },
- /// Extrude the profile entity `handle` by `height` along Z.
+ /// Extrude the profile entity `handle` along its plane normal.
ExtrudeEntity {
handle: Handle,
height: f64,
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,
+ color: [f32; 4],
+ },
/// Revolve the profile entity `handle` around the given axis by `angle_deg`.
RevolveEntity {
handle: Handle,
@@ -1455,6 +1458,13 @@ pub enum CmdResult {
handles: Vec,
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.
/// 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.
@@ -1767,6 +1777,14 @@ pub trait CadCommand: Send {
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) {}
+
/// Render the entity under the cursor through the normal rollover
/// highlight while this command is waiting for an entity pick.
fn entity_pick_highlights_hover(&self) -> bool {
diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs
index a2902bd6..3a7fce6f 100644
--- a/src/locale_catalog.rs
+++ b/src/locale_catalog.rs
@@ -3,6 +3,15 @@
pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static str)> {
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")),
" (none — use: SHORTCUTS SET )" => Some(("common", "none-use-shortcuts-set-key-command")),
" Alignment:" => Some(("common", "alignment-indented-label-title-case")),
diff --git a/src/modules/insert/solid3d_cmds.rs b/src/modules/insert/solid3d_cmds.rs
index 95c9a619..db2898da 100644
--- a/src/modules/insert/solid3d_cmds.rs
+++ b/src/modules/insert/solid3d_cmds.rs
@@ -1,27 +1,19 @@
-// 2D→3D modelling commands — EXTRUDE, REVOLVE, SWEEP, LOFT.
-//
-// 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`).
+// Profile-based kernel solid commands stored as exact ACIS data.
use acadrust::{entities::Solid3D, EntityType};
use glam::DVec3;
-use crate::t;
use crate::command::{CadCommand, CmdResult};
-
-// 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.
+use crate::t;
// ── EXTRUDE command ────────────────────────────────────────────────────────
pub struct ExtrudeCommand {
+ command_name: &'static str,
step: ExtrudeStep,
pub target_handle: acadrust::Handle,
+ anchor: DVec3,
+ direction: Option,
color: [f32; 4],
}
@@ -32,10 +24,16 @@ enum ExtrudeStep {
}
impl ExtrudeCommand {
- pub fn new(color: [f32; 4]) -> Self {
+ pub fn new_named(name: &str, color: [f32; 4]) -> Self {
Self {
+ command_name: match name {
+ "THICKEN" => "THICKEN",
+ _ => "EXTRUDE",
+ },
step: ExtrudeStep::Pick,
target_handle: acadrust::Handle::NULL,
+ anchor: DVec3::ZERO,
+ direction: None,
color,
}
}
@@ -43,7 +41,7 @@ impl ExtrudeCommand {
impl CadCommand for ExtrudeCommand {
fn name(&self) -> &'static str {
- "EXTRUDE"
+ self.command_name
}
fn prompt(&self) -> String {
match self.step {
@@ -55,19 +53,33 @@ impl CadCommand for ExtrudeCommand {
fn needs_entity_pick(&self) -> bool {
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) {
+ self.direction = direction.and_then(DVec3::try_normalize);
+ }
+ fn on_entity_pick(&mut self, handle: acadrust::Handle, point: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
self.target_handle = handle;
+ self.anchor = point;
self.step = ExtrudeStep::Height;
CmdResult::NeedPoint
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
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 {
handle: self.target_handle,
- height: pt.y.abs().max(1e-4),
+ height,
color: self.color,
};
}
@@ -81,13 +93,151 @@ impl CadCommand for ExtrudeCommand {
.filter(|&h| h.abs() > 1e-6)
.map(|h| CmdResult::ExtrudeEntity {
handle: self.target_handle,
- height: h.abs(),
+ height: h,
color: self.color,
})
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
+ fn cursor_axis(&self) -> Option<(DVec3, DVec3)> {
+ (self.step == ExtrudeStep::Height).then_some((self.anchor, self.direction?))
+ }
+ fn dyn_spec(&self) -> Option {
+ (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 {
+ Some((cursor - self.anchor).dot(self.direction?))
+ }
+}
+
+// ── PRESSPULL command ─────────────────────────────────────────────────────
+
+pub struct PresspullCommand {
+ picked: Option<(acadrust::Handle, DVec3)>,
+ direction: Option,
+ 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) {
+ 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 {
+ 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 {
+ 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 {
+ let (_, anchor) = self.picked?;
+ Some((cursor - anchor).dot(self.direction?))
+ }
}
// ── REVOLVE command ────────────────────────────────────────────────────────
@@ -171,7 +321,7 @@ impl CadCommand for RevolveCommand {
.ok()
.filter(|&a| a.abs() > 1e-3)?
};
- Some(self.make_revolve(angle.abs()))
+ Some(self.make_revolve(angle))
}
fn on_enter(&mut self) -> CmdResult {
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 {
EntityType::Solid3D(Solid3D::new())
}
-
// ── Autocomplete registry ─────────────────────────────────
-inventory::submit!(crate::command::CommandRegistration { names: &["EXTRUDE"] }); // ExtrudeCommand
-inventory::submit!(crate::command::CommandRegistration { names: &["LOFT"] }); // LoftCommand
-inventory::submit!(crate::command::CommandRegistration { names: &["REVOLVE"] }); // RevolveCommand
-inventory::submit!(crate::command::CommandRegistration { names: &["SWEEP"] }); // SweepCommand
+inventory::submit!(crate::command::CommandRegistration {
+ names: &["EXTRUDE", "THICKEN", "PRESSPULL"]
+});
+inventory::submit!(crate::command::CommandRegistration { names: &["LOFT"] });
+inventory::submit!(crate::command::CommandRegistration { names: &["REVOLVE"] });
+inventory::submit!(crate::command::CommandRegistration { names: &["SWEEP"] });
diff --git a/src/modules/model/boolean_cmd.rs b/src/modules/model/boolean_cmd.rs
index 44a29a08..da1bb10b 100644
--- a/src/modules/model/boolean_cmd.rs
+++ b/src/modules/model/boolean_cmd.rs
@@ -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
// 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)]
pub enum BoolOp {
Union,
diff --git a/src/modules/model/edge_cmd.rs b/src/modules/model/edge_cmd.rs
new file mode 100644
index 00000000..65ee2d31
--- /dev/null
+++ b/src/modules/model/edge_cmd.rs
@@ -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,
+ picked: Option<(Handle, DVec3)>,
+ default_value: f64,
+}
+
+impl SolidEdgeCommand {
+ pub fn new(operation: EdgeOperation, target: Option) -> 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 {
+ if self.picked.is_none() {
+ return None;
+ }
+ let value = text.trim().parse::().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 {
+ 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 {
+ self.picked.map(|(_, pick)| cursor.distance(pick))
+ }
+}
+
+inventory::submit!(crate::command::CommandRegistration {
+ names: &["SOLIDFILLET", "SOLIDCHAMFER"]
+});
diff --git a/src/modules/model/mod.rs b/src/modules/model/mod.rs
index 08f6f6f5..5272161a 100644
--- a/src/modules/model/mod.rs
+++ b/src/modules/model/mod.rs
@@ -1,11 +1,7 @@
-// Model module — 3D solid modelling.
-//
-// 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).
+// Solid creation and kernel modelling tools.
pub mod boolean_cmd;
+pub mod edge_cmd;
pub mod primitive_cmd;
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 CONE_ICON: &[u8] = include_bytes!("../../../assets/icons/cone3d.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 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 SUBTRACT_ICON: &[u8] = include_bytes!("../../../assets/icons/subtract.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.
fn tool(id: &'static str, label: &'static str, icon: &'static [u8]) -> ToolDef {
@@ -37,7 +42,7 @@ impl CadModule for ModelModule {
"model"
}
fn title(&self) -> &'static str {
- "Model"
+ "Modelling"
}
fn ribbon_groups(&self) -> &[RibbonGroup] {
@@ -45,31 +50,46 @@ impl CadModule for ModelModule {
GROUPS.get_or_init(|| {
vec![
RibbonGroup {
- title: "Model",
+ title: "Create",
tools: vec![
- RibbonItem::LargeTool(tool("BOX", "Box", BOX_ICON)),
- RibbonItem::LargeTool(tool("CYLINDER", "Cylinder", CYLINDER_ICON)),
- RibbonItem::LargeTool(tool("CONE", "Cone", CONE_ICON)),
- RibbonItem::LargeTool(tool("SPHERE", "Sphere", SPHERE_ICON)),
- RibbonItem::Dropdown {
- id: "MODEL_MORE",
- icon: IconKind::Svg(WEDGE_ICON),
+ RibbonItem::LargeDropdown {
+ id: "MODEL_PRIMITIVES",
+ label: "Box",
+ icon: IconKind::Svg(BOX_ICON),
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)),
("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 {
- title: "Design",
+ title: "Boolean",
tools: vec![
RibbonItem::LargeTool(tool("UNION", "Union", UNION_ICON)),
RibbonItem::LargeTool(tool("SUBTRACT", "Subtract", SUBTRACT_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)),
+ ],
+ },
]
})
}
diff --git a/src/modules/model/primitive_cmd.rs b/src/modules/model/primitive_cmd.rs
index 66941b70..0d43d22b 100644
--- a/src/modules/model/primitive_cmd.rs
+++ b/src/modules/model/primitive_cmd.rs
@@ -1,22 +1,15 @@
-// 3D primitive creation — BOX / CYLINDER / CONE / SPHERE / WEDGE / PYRAMID /
-// 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.
+// Interactive kernel primitives stored as ACIS Solid3D entities.
use acadrust::entities::Solid3D;
use acadrust::objects::SolidHistoryOperation;
-use acadrust::{primitives, EntityType};
-use glam::DVec3;
-use crate::t;
+use acadrust::EntityType;
use cadkernel::brep::Body;
+use glam::DVec3;
use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::scene::model::solid_model;
use crate::scene::model::wire_model::WireModel;
+use crate::t;
/// Which primitive a `PrimitiveCommand` builds.
#[derive(Clone, Copy, PartialEq, Eq)]
@@ -26,6 +19,7 @@ pub enum Shape {
Cylinder,
Cone,
Sphere,
+ Pyramid,
Torus,
}
@@ -37,6 +31,7 @@ impl Shape {
"CYLINDER" => Shape::Cylinder,
"CONE" => Shape::Cone,
"SPHERE" => Shape::Sphere,
+ "PYRAMID" | "PYR" => Shape::Pyramid,
"TORUS" => Shape::Torus,
_ => return None,
})
@@ -48,6 +43,7 @@ impl Shape {
Shape::Cylinder => "CYLINDER",
Shape::Cone => "CONE",
Shape::Sphere => "SPHERE",
+ Shape::Pyramid => "PYRAMID",
Shape::Torus => "TORUS",
}
}
@@ -129,12 +125,11 @@ impl PrimitiveCommand {
.to_cols_array()
}
- /// Build both the acadrust `Solid3D` (ACIS, for persistence) and the
- /// kernel `Body` (rendering + booleans) from the footprint + `height`.
- fn build(&self, height: f64) -> Option<(EntityType, Body, SolidHistoryOperation)> {
+ /// Build the persistent kernel body and its history node.
+ fn build(&self, height: f64) -> Option<(Body, SolidHistoryOperation)> {
use crate::scene::model::solid_history;
- let (doc, solid, history) = match self.shape {
+ let (solid, history) = match self.shape {
Shape::Box | Shape::Wedge => {
let (a, b) = (self.pts[0], self.pts[1]);
let length = (b.x - a.x).abs();
@@ -150,7 +145,6 @@ impl PrimitiveCommand {
a.z + height / 2.0,
];
(
- primitives::build_box(center, length, width, height),
solid_model::box_solid(center, length, width, height),
solid_history::box_op(
self.history_transform(origin),
@@ -162,7 +156,6 @@ impl PrimitiveCommand {
} else {
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_history::wedge_op(
self.history_transform(DVec3::from_array(origin)),
@@ -182,7 +175,6 @@ impl PrimitiveCommand {
let center = [c.x, c.y, c.z];
if self.shape == Shape::Cylinder {
(
- primitives::build_cylinder(center, r, height),
solid_model::cylinder_solid(center, r, height),
solid_history::cylinder_op(
self.history_transform(c),
@@ -192,7 +184,6 @@ impl PrimitiveCommand {
)
} else {
(
- primitives::build_cone(center, r, height),
solid_model::cone_solid(center, r, height),
solid_history::cone_op(
self.history_transform(c),
@@ -210,11 +201,21 @@ impl PrimitiveCommand {
}
let center = [c.x, c.y, c.z];
(
- primitives::build_sphere(center, r),
solid_model::sphere_solid(center, 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 => {
let c = self.pts[0];
let first = (self.pts[1] - c).length();
@@ -228,7 +229,6 @@ impl PrimitiveCommand {
let minor = (outer - inner) * 0.5;
let center = [c.x, c.y, c.z];
(
- primitives::build_torus(center, major, minor),
solid_model::torus_solid(center, major, minor),
solid_history::torus_op(
self.history_transform(c),
@@ -239,14 +239,12 @@ impl PrimitiveCommand {
}
};
let solid = solid?;
- let mut s3d = Solid3D::new();
- s3d.set_sat_document(&doc);
- Some((EntityType::Solid3D(s3d), solid, history))
+ Some((solid, history))
}
fn commit(&self, height: f64) -> CmdResult {
match self.build(height) {
- Some((entity, solid, history)) => {
+ Some((solid, history)) => {
// Built upright in its own frame, then put on the working
// plane — the same move `place_entity` makes for the ACIS
// copy, so the two stay on top of each other.
@@ -262,11 +260,21 @@ impl PrimitiveCommand {
],
);
match placed {
- Some(placed) => CmdResult::CommitSolid {
- entity: self.plane.place_entity(entity),
- solid: Box::new(placed),
- history,
- },
+ Some(placed) => {
+ let Some(document) =
+ crate::scene::convert::acis_export::solid_to_sat(&placed)
+ 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,
}
}
@@ -390,7 +398,15 @@ impl CadCommand for PrimitiveCommand {
fn footprint_wire(shape: Shape, pts: &[DVec3]) -> WireModel {
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 r = (pts[1] - c).length();
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]);
}
}
- Shape::Cylinder | Shape::Cone => {
+ Shape::Cylinder | Shape::Cone | Shape::Pyramid => {
let center = pts[0];
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);
if shape == Shape::Cylinder {
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 ─────────────────────────────────
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 {
@@ -522,9 +550,9 @@ fn wire(name: &str, points: Vec<[f32; 3]>) -> WireModel {
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
- dash_from_start: false,
- dash_align_end: None,
- text_verts: Vec::new(),
+ dash_from_start: false,
+ dash_align_end: None,
+ text_verts: Vec::new(),
name: name.into(),
points,
points_low: Vec::new(),
diff --git a/src/scene/convert/acis_export.rs b/src/scene/convert/acis_export.rs
index 0f522478..48e3730c 100644
--- a/src/scene/convert/acis_export.rs
+++ b/src/scene/convert/acis_export.rs
@@ -3,12 +3,21 @@
//! Analytic surfaces remain analytic instead of becoming facets.
use cadkernel::acis::append;
-use acadrust::entities::acis::SatDocument;
+use acadrust::entities::acis::{SabReader, SabWriter, SatDocument};
use cadkernel::brep::Body;
/// Returns `None` when the body contains an unsupported record form.
-pub fn planar_solid_to_sat(body: &Body) -> Option {
+pub fn solid_to_sat(body: &Body) -> Option {
let mut document = SatDocument::new();
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)
}
diff --git a/src/scene/mod.rs b/src/scene/mod.rs
index d887e77e..b22dde38 100644
--- a/src/scene/mod.rs
+++ b/src/scene/mod.rs
@@ -518,10 +518,8 @@ pub(crate) fn wire_gpu_patch_enabled() -> bool {
/// Resolve a viewport's paper-to-model scale ratio from its two
/// DXF-derived sources.
///
-/// `view_height` (model-space view extent) is the canonical source — it
-/// is what AutoCAD actually uses to draw, and what we keep in sync on
-/// every write. `custom_scale` is consulted only when `view_height` is
-/// missing or zero (some third-party exporters omit it).
+/// `view_height` is canonical. `custom_scale` is the fallback when it is
+/// missing or zero.
#[inline]
pub fn vp_effective_scale(custom_scale: f64, view_height: f64, vp_height: f64) -> f64 {
if view_height.abs() > 1e-9 {
@@ -2945,8 +2943,8 @@ impl Scene {
}
/// Guarantee that a paper layout has its full-screen overall (`id == 1`)
- /// sheet viewport. AutoCAD always writes one, and `add_layout` creates it,
- /// but this is a safety net for layouts that arrive without it. The sheet
+ /// sheet viewport. `add_layout` creates it; this is a safety net for layouts
+ /// that arrive without it. The sheet
/// viewport is the authoritative paper-space view and the canvas every
/// floating viewport overlays.
pub fn ensure_sheet_viewport(&mut self, layout_name: &str) {
@@ -3016,9 +3014,8 @@ impl Scene {
let mut vp = acadrust::entities::Viewport::new();
vp.id = 1;
vp.status = acadrust::entities::ViewportStatusFlags::default_on();
- // Paper-space center is a 2D (x, y) point with z = 0 — the same
- // convention MVIEW uses for floating viewports. AutoCAD/TrueView read
- // the viewport center as (x, y); putting the paper-height midpoint in z
+ // Paper-space center is a 2D (x, y) point with z = 0. Putting the
+ // paper-height midpoint in z
// (with y = 0) left the sheet view centered at y = 0, shifting the whole
// layout half a page down. See issue #156.
vp.center = acadrust::types::Vector3::new(
@@ -3242,8 +3239,7 @@ impl Scene {
}
/// Dashed rectangle marking the printable area — the paper inset by the
- /// layout's plot margins. AutoCAD draws this guide on every layout; with the
- /// margins now preserved we can reflect it too. `None` in model space, when
+ /// layout's plot margins. `None` in model space, when
/// the layout has no margins, or when the inset would be degenerate.
pub(super) fn printable_area_wire(&self) -> Option {
if self.current_layout == "Model" {
@@ -3311,8 +3307,7 @@ impl Scene {
/// The effective plot settings for the current layout: a standalone
/// PlotSettings page setup if one exists, otherwise the settings embedded in
/// the LAYOUT object (paper size, margins, origin, rotation, scale). Loaded
- /// AutoCAD files keep their settings embedded, so without this fallback the
- /// plot/PDF path would ignore the file's rotation, origin and scale.
+ /// The fallback preserves rotation, origin and scale from loaded files.
pub fn effective_plot_settings(&self) -> Option {
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 {
+ 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 {
+ 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(
&self,
bounds: iced::Rectangle,
@@ -9374,9 +9407,7 @@ impl Scene {
if any {
return Some((min, max));
}
- // Last-resort: the header's saved EXTMIN/EXTMAX. AutoCAD writes these
- // on save so opening a file gives ZOOM EXTENTS a useful answer before
- // the wire cache is built.
+ // Last resort: saved EXTMIN/EXTMAX before the wire cache is built.
const SANE_EXTENT: f64 = 1.0e16;
let h = &self.document.header;
let hmin = h.model_space_extents_min;
diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs
index bd143587..9879bd2d 100644
--- a/src/scene/model/solid_history.rs
+++ b/src/scene/model/solid_history.rs
@@ -1,11 +1,9 @@
-use acadrust::entities::{EmbeddedEntity, Solid3D};
+use acadrust::entities::Solid3D;
use acadrust::objects::{
- SolidHistoryBox, SolidHistoryBrep, SolidHistoryCylinder, SolidHistoryLoft,
+ SolidHistoryBox, SolidHistoryBrep, SolidHistoryCylinder,
SolidHistoryNodeBase, SolidHistoryOperation, SolidHistoryPyramid,
- SolidHistoryRevolve, SolidHistorySphere, SolidHistorySweep, SolidHistoryTorus,
+ SolidHistorySphere, SolidHistoryTorus,
};
-use acadrust::types::{Vector2, Vector3};
-use acadrust::EntityType;
use cadkernel::brep::Body;
use crate::command::EntityTransform;
@@ -474,21 +472,6 @@ fn base(transform: [f64; 16]) -> SolidHistoryNodeBase {
base
}
-fn embedded(entity: &EntityType) -> Option {
- 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(
transform: [f64; 16],
length: f64,
@@ -593,7 +576,7 @@ pub fn pyramid_op(
}
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| {
let mut solid = Solid3D::new();
solid.set_sat_document(&document);
@@ -607,56 +590,3 @@ pub fn brep_op(body: &Body) -> SolidHistoryOperation {
..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()
- })
-}
diff --git a/src/scene/model/solid_model.rs b/src/scene/model/solid_model.rs
index 15d2fa11..b229981c 100644
--- a/src/scene/model/solid_model.rs
+++ b/src/scene/model/solid_model.rs
@@ -1,19 +1,6 @@
-// B-rep construction for the Model tab's primitives, plus tessellation into
-// 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.
+// Kernel B-rep construction and display tessellation.
-use cadkernel::brep::{self, Body};
+use cadkernel::brep::{self, Body, Curve3, EdgeKey, FaceKey, Surface};
use crate::scene::model::mesh_model::{MeshLodSet, MeshModel};
@@ -223,6 +210,65 @@ pub fn edge_wires(body: &Body) -> Vec {
.collect()
}
+/// B-rep edge nearest a world-space surface pick.
+pub fn nearest_edge(body: &Body, pick: [f64; 3]) -> Option {
+ 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 {
+ 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 ──────────────────────────────────────────────────────
/// 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 {
Bool::Subtract => brep::Operation::Difference,
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 ────────────────────────────────────────────────────────────
-/// 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 {
- mesh_from_tessellation(tessellation(body), color)
-}
-
fn mesh_from_tessellation(
tessellation: brep::mesh::BodyMesh,
color: [f32; 4],
diff --git a/src/scene/model/sweep_model.rs b/src/scene/model/sweep_model.rs
index f79e048c..124e53fd 100644
--- a/src/scene/model/sweep_model.rs
+++ b/src/scene/model/sweep_model.rs
@@ -1,20 +1,8 @@
-// EXTRUDE / REVOLVE: turning a drawn profile into a solid.
-//
-// 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.
+// Exact profile sweeps stored as kernel B-reps and ACIS.
use cadkernel::brep::{self, Body};
-use cadkernel::geom2d::Curve;
-use cadkernel::space::{PlanarCurve, Plane};
+use cadkernel::geom2d::{offset_polyline, Arc, Curve, EllipseArc, Line, Polyline};
+use cadkernel::space::{PlanarCurve, Plane, Vec3};
use acadrust::EntityType;
use crate::entities::curve::entity_curve;
@@ -40,13 +28,17 @@ pub fn profile_of(entity: &EntityType) -> Option {
return None;
}
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(),
- // 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::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::