diff --git a/Cargo.lock b/Cargo.lock index 6aef22802..661be0bc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4205,7 +4205,7 @@ dependencies = [ [[package]] name = "opencadcodec" version = "0.6.0" -source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=ab2eecd#ab2eecdbffc31120b5ad6d899f6fc67cf21ede39" +source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=c9ffcac#c9ffcac5ecdc7a504cc4a7e3ecb52ac9d587c8bf" dependencies = [ "anyhow", "bitflags 2.13.1", @@ -4228,7 +4228,7 @@ dependencies = [ [[package]] name = "opencadgraph" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/opencadgraph.git?rev=194e619#194e6192c9eca5c916371e7434b9fde18911aa6a" +source = "git+https://github.com/HakanSeven12/opencadgraph.git?rev=7f6aae7#7f6aae7aff6b7de87cc1dcdb38a06c3e64ae1922" dependencies = [ "opencadkernel", "serde_json", @@ -4237,7 +4237,7 @@ dependencies = [ [[package]] name = "opencadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=bf44e68#bf44e6856f2582eb5df5038650b6a6910eb9dac5" +source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=99b6b48#99b6b4890704d300596ba502b5a1c8ec1eefa954" dependencies = [ "cavalier_contours", "opencadcodec", @@ -4248,7 +4248,7 @@ dependencies = [ [[package]] name = "opencadkernel-constraints" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=bf44e68#bf44e6856f2582eb5df5038650b6a6910eb9dac5" +source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=99b6b48#99b6b4890704d300596ba502b5a1c8ec1eefa954" dependencies = [ "nalgebra", ] diff --git a/Cargo.toml b/Cargo.toml index 64187a804..c9480bb41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,10 +34,10 @@ rfd = "0.17" clap = { version = "4", features = ["derive", "string"] } env_logger = "0.11" log = "0.4" -codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "ab2eecd", features = ["serde"] } -kernel = { package = "opencadkernel", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "bf44e68", features = ["acis", "offset"] } -kernel-constraints = { package = "opencadkernel-constraints", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "bf44e68" } -graph = { package = "opencadgraph", git = "https://github.com/HakanSeven12/opencadgraph.git", rev = "194e619" } +codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "c9ffcac", features = ["serde"] } +kernel = { package = "opencadkernel", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "99b6b48", features = ["acis", "offset"] } +kernel-constraints = { package = "opencadkernel-constraints", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "99b6b48" } +graph = { package = "opencadgraph", git = "https://github.com/HakanSeven12/opencadgraph.git", rev = "7f6aae7" } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } diff --git a/crates/ocs_plugin_api/Cargo.toml b/crates/ocs_plugin_api/Cargo.toml index 85f91b710..2ff186185 100644 --- a/crates/ocs_plugin_api/Cargo.toml +++ b/crates/ocs_plugin_api/Cargo.toml @@ -15,7 +15,7 @@ serde_json = { version = "1", optional = true } # Pulled in only by the `host` feature, which adds the `opencadcodec`-typed # `HostApi` runtime surface. The default crate stays dependency-free so engine # crates and external tooling can depend on the manifest/ribbon contract cheaply. -codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "ab2eecd", optional = true, features = ["serde"] } +codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "c9ffcac", optional = true, features = ["serde"] } # Runtime IPC and serialization (host feature only). interprocess = { version = "2", optional = true } @@ -39,7 +39,7 @@ serde_json = "1" serde = { version = "1", features = ["derive"] } cargo-lock = "11" # opencadcodec is scanned at build time to generate the embedded type registry. -codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "ab2eecd", features = ["serde"] } +codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "c9ffcac", features = ["serde"] } [dev-dependencies] serde_json = "1" diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml index 9a406230b..00e7bbd79 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] -codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "ab2eecd", features = ["serde"] } +codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "c9ffcac", 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 9e7adf385..3fa33ea45 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -1451,6 +1451,7 @@ common = .zoom = التكبير .reverse-mouse-wheel-zoom-zoomwheel = عكس التكبير بعجلة الفأرة (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = لوحة اللمس: التمرير يحرك العرض، Ctrl+تمرير أو القرص يكبّر + .interface-scale = مقياس الواجهة .zoom-factor = معامل التكبير .how-far-one-wheel-notch-zooms-zoomfactor = مقدار التكبير لكل درجة من العجلة (ZOOMFACTOR). .text-and-dimensions = النص والأبعاد diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index d4ca54bf4..d186cba66 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -1438,6 +1438,7 @@ common = .zoom = Мащабиране .reverse-mouse-wheel-zoom-zoomwheel = Обръщане на мащабирането с колелцето (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Тъчпад: превъртането мести изгледа, Ctrl+превъртане или щипване мащабира + .interface-scale = Мащаб на интерфейса .zoom-factor = Коефициент на мащабиране .how-far-one-wheel-notch-zooms-zoomfactor = Колко мащабира едно щракване на колелцето (ZOOMFACTOR). .text-and-dimensions = Текст и размери diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 1d3710279..07669d6b8 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -1438,6 +1438,7 @@ common = .zoom = Zoom .reverse-mouse-wheel-zoom-zoomwheel = Obrátit zoom kolečkem myši (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Touchpad: posun posouvá pohled, Ctrl+posun nebo sevření zvětšuje + .interface-scale = Měřítko rozhraní .zoom-factor = Faktor zoomu .how-far-one-wheel-notch-zooms-zoomfactor = O kolik přiblíží jeden zub kolečka (ZOOMFACTOR). .text-and-dimensions = Text a kóty diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 68af4c46f..ea5b01467 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -1439,6 +1439,7 @@ common = .zoom = Zoom .reverse-mouse-wheel-zoom-zoomwheel = Mausradzoom umkehren (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Touchpad: Scrollen verschiebt, Strg+Scrollen oder Zusammenziehen zoomt + .interface-scale = Oberflächenskalierung .zoom-factor = Zoomfaktor .how-far-one-wheel-notch-zooms-zoomfactor = Wie weit eine Radrastung zoomt (ZOOMFACTOR). .text-and-dimensions = Text und Bemaßung diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index c3e4f2c83..743e2bb7c 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -1440,6 +1440,7 @@ common = .zoom = Ζουμ .reverse-mouse-wheel-zoom-zoomwheel = Αντιστροφή ζουμ με τον τροχό (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Επιφάνεια αφής: η κύλιση μετακινεί, Ctrl+κύλιση ή τσίμπημα κάνει ζουμ + .interface-scale = Κλίμακα διεπαφής .zoom-factor = Συντελεστής ζουμ .how-far-one-wheel-notch-zooms-zoomfactor = Πόσο ζουμάρει ένα σκαλί του τροχού (ZOOMFACTOR). .text-and-dimensions = Κείμενο και διαστάσεις diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 39581bfde..7c22d658b 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -1440,6 +1440,7 @@ common = .zoom = Zoom .reverse-mouse-wheel-zoom-zoomwheel = Reverse mouse wheel zoom (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Touchpad: scroll pans, Ctrl+scroll or pinch zooms + .interface-scale = Interface scale .zoom-factor = Zoom factor .how-far-one-wheel-notch-zooms-zoomfactor = How far one wheel notch zooms (ZOOMFACTOR). .text-and-dimensions = Text and Dimensions diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 3e23e63e2..66d5e31d7 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -1439,6 +1439,7 @@ common = .zoom = Zoom .reverse-mouse-wheel-zoom-zoomwheel = Invertir el zoom de la rueda del ratón (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Panel táctil: el desplazamiento encuadra, Ctrl+desplazamiento o pellizco hace zoom + .interface-scale = Escala de la interfaz .zoom-factor = Factor de zoom .how-far-one-wheel-notch-zooms-zoomfactor = Cuánto amplía un paso de la rueda (ZOOMFACTOR). .text-and-dimensions = Texto y acotación diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index f5332f224..fb4ce630b 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -1438,6 +1438,7 @@ common = .zoom = Zoomaus .reverse-mouse-wheel-zoom-zoomwheel = Käännä hiiren rullan zoomaus (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Kosketuslevy: vieritys panoroi, Ctrl+vieritys tai nipistys zoomaa + .interface-scale = Käyttöliittymän skaalaus .zoom-factor = Zoomauskerroin .how-far-one-wheel-notch-zooms-zoomfactor = Kuinka paljon yksi rullan pykälä zoomaa (ZOOMFACTOR). .text-and-dimensions = Teksti ja mitoitus diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 4b1c9c4f6..85bf5e706 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -1439,6 +1439,7 @@ common = .zoom = Zoom .reverse-mouse-wheel-zoom-zoomwheel = Inverser le zoom à la molette (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Pavé tactile : le défilement déplace, Ctrl+défilement ou pincement zoome + .interface-scale = Échelle de l'interface .zoom-factor = Facteur de zoom .how-far-one-wheel-notch-zooms-zoomfactor = De combien zoome un cran de molette (ZOOMFACTOR). .text-and-dimensions = Texte et cotation diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 1374059b8..46e2c5f75 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -1427,6 +1427,7 @@ common = .zoom = ज़ूम .reverse-mouse-wheel-zoom-zoomwheel = माउस व्हील ज़ूम उलटें (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = टचपैड: स्क्रॉल पैन करता है, Ctrl+स्क्रॉल या पिंच ज़ूम करता है + .interface-scale = इंटरफ़ेस स्केल .zoom-factor = ज़ूम गुणक .how-far-one-wheel-notch-zooms-zoomfactor = व्हील का एक पायदान कितना ज़ूम करता है (ZOOMFACTOR)। .text-and-dimensions = पाठ और आयाम diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index e61e6db3f..59b432544 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -1438,6 +1438,7 @@ common = .zoom = Nagyítás .reverse-mouse-wheel-zoom-zoomwheel = Egérgörgős nagyítás megfordítása (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Érintőpad: a görgetés mozgat, a Ctrl+görgetés vagy csippentés nagyít + .interface-scale = Felület mérete .zoom-factor = Nagyítási tényező .how-far-one-wheel-notch-zooms-zoomfactor = Mennyit nagyít egy görgőkattanás (ZOOMFACTOR). .text-and-dimensions = Szöveg és méretezés diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index db0ca1b45..d2bfc1ae1 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -1438,6 +1438,7 @@ common = .zoom = Zoom .reverse-mouse-wheel-zoom-zoomwheel = Inverti lo zoom con la rotellina (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Touchpad: lo scorrimento sposta la vista, Ctrl+scorrimento o pizzico esegue lo zoom + .interface-scale = Scala dell'interfaccia .zoom-factor = Fattore di zoom .how-far-one-wheel-notch-zooms-zoomfactor = Di quanto ingrandisce uno scatto della rotellina (ZOOMFACTOR). .text-and-dimensions = Testo e quote diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index b3da26db1..72c9b13e0 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -1429,6 +1429,7 @@ common = .zoom = ズーム .reverse-mouse-wheel-zoom-zoomwheel = マウス ホイールのズーム方向を反転 (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = タッチパッド: スクロールで画面移動、Ctrl+スクロールまたはピンチでズーム + .interface-scale = インターフェースの拡大率 .zoom-factor = ズーム倍率 .how-far-one-wheel-notch-zooms-zoomfactor = ホイール 1 ノッチのズーム量 (ZOOMFACTOR)。 .text-and-dimensions = 文字と寸法 diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 07b8733d4..403135aa8 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -1438,6 +1438,7 @@ common = .zoom = 줌 .reverse-mouse-wheel-zoom-zoomwheel = 마우스 휠 줌 방향 반전 (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = 터치패드: 스크롤은 화면 이동, Ctrl+스크롤 또는 핀치는 확대/축소 + .interface-scale = 인터페이스 배율 .zoom-factor = 줌 배율 .how-far-one-wheel-notch-zooms-zoomfactor = 휠 한 칸이 확대하는 정도 (ZOOMFACTOR). .text-and-dimensions = 문자 및 치수 diff --git a/locales/lo-LA/opencadstudio.ftl b/locales/lo-LA/opencadstudio.ftl index 0c633a84e..8b75a109a 100644 --- a/locales/lo-LA/opencadstudio.ftl +++ b/locales/lo-LA/opencadstudio.ftl @@ -1440,6 +1440,7 @@ common = .zoom = ຊູມ .reverse-mouse-wheel-zoom-zoomwheel = ສະຫຼັບທິດທາງການໝຸນລໍ້ເມົາສ໌ຊູມ (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = ທັດແພດ: ເລື່ອນເພື່ອຍ້າຍມຸມມອງ, Ctrl+ເລື່ອນ ຫຼື ບີບເພື່ອຊູມ + .interface-scale = ຂະໜາດສ່ວນຕິດຕໍ່ .zoom-factor = ຕົວຄູນການຊູມ .how-far-one-wheel-notch-zooms-zoomfactor = ໄລຍະຊູມຕໍ່ການໝຸນລໍ້ເມົາສ໌ໜຶ່ງຄລິກ (ZOOMFACTOR). .text-and-dimensions = ຂໍ້ຄວາມ ແລະ ຂະໜາດ diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index b80c45c52..112069a59 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -1437,6 +1437,7 @@ common = .zoom = Zoomen .reverse-mouse-wheel-zoom-zoomwheel = Muiswielzoom omkeren (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Touchpad: scrollen verschuift, Ctrl+scrollen of knijpen zoomt + .interface-scale = Interfaceschaal .zoom-factor = Zoomfactor .how-far-one-wheel-notch-zooms-zoomfactor = Hoeveel één wielklik zoomt (ZOOMFACTOR). .text-and-dimensions = Tekst en maatvoering diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 4bda1bd8b..16cec2da3 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -1438,6 +1438,7 @@ common = .zoom = Powiększenie .reverse-mouse-wheel-zoom-zoomwheel = Odwróć powiększanie kółkiem myszy (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Touchpad: przewijanie przesuwa widok, Ctrl+przewijanie lub szczypanie powiększa + .interface-scale = Skala interfejsu .zoom-factor = Współczynnik powiększenia .how-far-one-wheel-notch-zooms-zoomfactor = O ile powiększa jedno kliknięcie kółka (ZOOMFACTOR). .text-and-dimensions = Tekst i wymiary diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index f16e194c4..e32d1d614 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -1439,6 +1439,7 @@ common = .zoom = Zoom .reverse-mouse-wheel-zoom-zoomwheel = Inverter o zoom da roda do mouse (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Touchpad: rolar move a vista, Ctrl+rolar ou pinçar aplica zoom + .interface-scale = Escala da interface .zoom-factor = Fator de zoom .how-far-one-wheel-notch-zooms-zoomfactor = Quanto um passo da roda amplia (ZOOMFACTOR). .text-and-dimensions = Texto e cotas diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index adb69ae0f..5734d26e5 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -1442,6 +1442,7 @@ common = .zoom = Зумирование .reverse-mouse-wheel-zoom-zoomwheel = Обратить зумирование колесом мыши (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Тачпад: прокрутка панорамирует, Ctrl+прокрутка или щипок масштабирует + .interface-scale = Масштаб интерфейса .zoom-factor = Коэффициент зумирования .how-far-one-wheel-notch-zooms-zoomfactor = Насколько приближает один щелчок колеса (ZOOMFACTOR). .text-and-dimensions = Текст и размеры diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index fb9d7010f..87e53c2af 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -1425,6 +1425,7 @@ common = .zoom = Yakınlaştırma .reverse-mouse-wheel-zoom-zoomwheel = Fare tekerleği yakınlaştırmasını ters çevir (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = Dokunmatik yüzey: kaydırma kaydırır, Ctrl+kaydırma veya sıkıştırma yakınlaştırır + .interface-scale = Arayüz ölçeği .zoom-factor = Yakınlaştırma çarpanı .how-far-one-wheel-notch-zooms-zoomfactor = Bir tekerlek adımının ne kadar yakınlaştıracağı (ZOOMFACTOR). .text-and-dimensions = Metin ve ölçülendirme diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index da267f304..ac1e10a16 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -1426,6 +1426,7 @@ common = .zoom = 缩放 .reverse-mouse-wheel-zoom-zoomwheel = 反转鼠标滚轮缩放方向 (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = 触控板:滚动平移,Ctrl+滚动或捏合缩放 + .interface-scale = 界面缩放 .zoom-factor = 缩放系数 .how-far-one-wheel-notch-zooms-zoomfactor = 滚轮每格的缩放幅度 (ZOOMFACTOR)。 .text-and-dimensions = 文字和标注 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 07312b40e..122cf89e5 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -1438,6 +1438,7 @@ common = .zoom = 縮放 .reverse-mouse-wheel-zoom-zoomwheel = 反轉滑鼠滾輪縮放方向 (ZOOMWHEEL) .touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms = 觸控板:捲動平移,Ctrl+捲動或捏合縮放 + .interface-scale = 介面縮放 .zoom-factor = 縮放係數 .how-far-one-wheel-notch-zooms-zoomfactor = 滾輪每一格的縮放幅度 (ZOOMFACTOR)。 .text-and-dimensions = 文字與標註 diff --git a/plugins/opencad-python/Cargo.lock b/plugins/opencad-python/Cargo.lock index 76d4cfd36..308d68303 100644 --- a/plugins/opencad-python/Cargo.lock +++ b/plugins/opencad-python/Cargo.lock @@ -1264,7 +1264,7 @@ dependencies = [ [[package]] name = "opencadcodec" version = "0.6.0" -source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=ab2eecd#ab2eecdbffc31120b5ad6d899f6fc67cf21ede39" +source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=c9ffcac#c9ffcac5ecdc7a504cc4a7e3ecb52ac9d587c8bf" dependencies = [ "anyhow", "bitflags", diff --git a/src/app/automation.rs b/src/app/automation.rs index 9d230d30c..088493ab7 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -1801,7 +1801,7 @@ mod tests { viewport.id = 2; viewport.width = 100.0; viewport.height = 50.0; - viewport.status.is_on = true; + viewport.turn_on(); scene.add_entity(codec::EntityType::Viewport(viewport)); for entity in scene.document.entities_mut() { if let codec::EntityType::Viewport(viewport) = entity { diff --git a/src/app/command_driver/solid3d.rs b/src/app/command_driver/solid3d.rs index d9a78dc21..5042fb036 100644 --- a/src/app/command_driver/solid3d.rs +++ b/src/app/command_driver/solid3d.rs @@ -489,6 +489,8 @@ impl OpenCADStudio { .collect::>(); // Group 294: a base point the user picked. let picked_base = options.base_point.is_some(); + let several = profiles.len() > 1; + let picked = options; let options = sweep_model::sweep_selection_options(&selection, options); let pending = if profiles.is_empty() { None @@ -532,8 +534,10 @@ impl OpenCADStudio { continue; } }; + // An associative surface keeps the profile and path it reads. + let associative = surface && sweep_model::surface_associativity(); let deletes_path = - crate::app::delobj_deletes_auxiliary(delete_objects, surface); + !associative && crate::app::delobj_deletes_auxiliary(delete_objects, surface); if deletes_path && self.tabs[i].scene.is_layer_locked(path_handle) { failed += 1; continue; @@ -546,6 +550,14 @@ impl OpenCADStudio { created, &expressions, ); + if associative { + // A picked base point, or the one several profiles share. + let linked = crate::command::SweepOptions { + base_point: if picked_base || several { options.and_then(|o| o.base_point) } else { None }, + ..picked + }; + sweep_model::link_sweep_sources(&mut self.tabs[i].scene.document, created, handle, path_handle, &linked); + } created } else { self.add_solid_model( @@ -558,7 +570,7 @@ impl OpenCADStudio { failed += 1; } else { created_handles.push(created); - if delete_profiles { + if delete_profiles && !associative { consumed.push(handle); } if deletes_path { diff --git a/src/app/command_driver/tests.rs b/src/app/command_driver/tests.rs index c5a250660..85e6e3c8b 100644 --- a/src/app/command_driver/tests.rs +++ b/src/app/command_driver/tests.rs @@ -935,14 +935,17 @@ mod delobj_tests { expected, ); } - assert_eq!( - sweep_source_presence(2, crate::command::ExtrudeMode::Surface), - (false, false), - ); - assert_eq!( - sweep_source_presence(3, crate::command::ExtrudeMode::Surface), - (false, true), - ); + // A swept surface is associative by default (SURFACEASSOCIATIVITY 1) + // and keeps the profile and path it follows, whatever DELOBJ says; + // DELOBJ reaches surfaces only with associativity off, a profile-wide + // switch this parallel test leaves alone. + assert!(crate::scene::model::sweep_model::surface_associativity()); + for value in [2, 3] { + assert_eq!( + sweep_source_presence(value, crate::command::ExtrudeMode::Surface), + (true, true), + ); + } } } diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index d1b05da03..72c664de6 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -697,6 +697,7 @@ inventory::submit!(crate::command::CommandRegistration { "PDFOSNAP", "UOSNAP", "FIELDDISPLAY", + "SURFACEASSOCIATIVITY", "FIELDEVAL", "PDFIMPORTMODE", "PDFIMPORTFILTER", diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index e38b4adfa..d8faa16c4 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -279,7 +279,7 @@ impl OpenCADStudio { if name.is_empty() { None } else { - Some(name.clone()) + Some(name.to_uppercase()) } }) .collect(); @@ -310,6 +310,17 @@ impl OpenCADStudio { .map(|ds| ds.dimtxsty.clone()), ) .filter(|s| !s.is_empty()) + .map(|s| s.to_uppercase()) + .collect(); + // Complex linetypes reach their text and shape styles by handle. + let linetype_styles: rustc_hash::FxHashSet = self.tabs[i] + .scene + .document + .line_types + .iter() + .flat_map(|lt| lt.elements.iter()) + .filter_map(|element| element.complex.as_ref()) + .map(|complex| complex.style_handle) .collect(); // Linetypes: entity references plus every layer's own // linetype — a ByLayer entity reaches it through the layer. @@ -334,6 +345,7 @@ impl OpenCADStudio { .map(|l| l.line_type.clone()), ) .filter(|s| !s.is_empty()) + .map(|s| s.to_uppercase()) .collect(); // Live blocks are reachable from layouts and style roots @@ -403,7 +415,9 @@ impl OpenCADStudio { .document .layers .iter() - .filter(|l| l.name != "0" && !used_layers.contains(&l.name)) + .filter(|l| { + l.name != "0" && !used_layers.contains(&l.name.to_uppercase()) + }) .map(|l| l.name.clone()) .collect() } else { @@ -415,8 +429,14 @@ impl OpenCADStudio { .document .text_styles .iter() + // Unnamed shape-file records serve complex linetypes + // and are never listed for purging. .filter(|s| { - s.name != "Standard" && !used_text_styles.contains(&s.name) + !s.name.is_empty() + && !s.is_shape_file + && !s.name.eq_ignore_ascii_case("Standard") + && !used_text_styles.contains(&s.name.to_uppercase()) + && !linetype_styles.contains(&s.handle) }) .map(|s| s.name.clone()) .collect() @@ -432,7 +452,7 @@ impl OpenCADStudio { .iter() .filter(|lt| { !standard.iter().any(|s| s.eq_ignore_ascii_case(<.name)) - && !used_linetypes.contains(<.name) + && !used_linetypes.contains(<.name.to_uppercase()) }) .map(|lt| lt.name.clone()) .collect() @@ -479,16 +499,17 @@ impl OpenCADStudio { snapshot_pushed = true; } - // Apply removals (mutable) - for name in &layer_remove { - self.tabs[i].scene.document.layers.remove(name); - } - for name in &style_remove { - self.tabs[i].scene.document.text_styles.remove(name); - } - for name in <_remove { - self.tabs[i].scene.document.line_types.remove(name); - } + // Apply removals (mutable). Count what actually went: an + // entry the table cannot remove by name would otherwise be + // listed again on every pass and never let the loop end. + let doc = &mut self.tabs[i].scene.document; + let layers_gone = + layer_remove.iter().filter(|n| doc.layers.remove(n).is_some()).count(); + let styles_gone = + style_remove.iter().filter(|n| doc.text_styles.remove(n).is_some()).count(); + let lts_gone = + lt_remove.iter().filter(|n| doc.line_types.remove(n).is_some()).count(); + let mut blocks_gone = 0usize; for name in &block_remove { // Drop the block definition's member entities (and the // BLOCK/ENDBLK delimiters) before the record so no orphaned @@ -506,12 +527,17 @@ impl OpenCADStudio { for h in handles { self.tabs[i].scene.document.remove_entity(h); } - self.tabs[i].scene.document.block_records.remove(name); + if self.tabs[i].scene.document.block_records.remove(name).is_some() { + blocks_gone += 1; + } + } + n_layers += layers_gone; + n_styles += styles_gone; + n_lts += lts_gone; + n_blocks += blocks_gone; + if layers_gone + styles_gone + lts_gone + blocks_gone == 0 { + break; } - n_layers += layer_remove.len(); - n_styles += style_remove.len(); - n_lts += lt_remove.len(); - n_blocks += block_remove.len(); } // Remove draw-order tables whose owning blocks are gone. @@ -1093,6 +1119,7 @@ impl OpenCADStudio { | "DGNOSNAP" | "UOSNAP" | "FIELDDISPLAY" + | "SURFACEASSOCIATIVITY" | "PDFIMPORTMODE" | "PDFIMPORTFILTER" | "PDFIMPORTLAYERS" @@ -1301,6 +1328,28 @@ impl OpenCADStudio { } return Some(self.finish_dispatch(cmd)); } + if name == "SURFACEASSOCIATIVITY" { + use crate::scene::model::sweep_model; + let current = i16::from(sweep_model::surface_associativity()); + match value.as_deref().map(|v| v.trim().parse::().ok().filter(|v| (0..=1).contains(v))) { + Some(Some(mode)) => { + if current != mode { + sweep_model::set_surface_associativity(mode == 1); + self.save_config(); + } + } + Some(None) => { + self.command_line.push_error(crate::t!("Requires 0 or 1 only.").as_ref()); + self.command_line.push_output(&format!("Enter new value for SURFACEASSOCIATIVITY <{current}>:")); + self.pending_setvar = Some(name.clone()); + } + None => { + self.command_line.push_output(&format!("Enter new value for SURFACEASSOCIATIVITY <{current}>:")); + self.pending_setvar = Some(name.clone()); + } + } + return Some(self.finish_dispatch(cmd)); + } if name == "FIELDDISPLAY" { let current = i16::from(crate::entities::field::display()); match value.as_deref().map(|v| v.trim().parse::().ok().filter(|v| (0..=1).contains(v))) { diff --git a/src/app/commands/view.rs b/src/app/commands/view.rs index 3c6bc31dd..1436e1450 100644 --- a/src/app/commands/view.rs +++ b/src/app/commands/view.rs @@ -719,7 +719,7 @@ impl OpenCADStudio { vp.view_height, vp.height, ), - vp.status.is_on, + vp.is_on(), vp.status.locked, )) } else { diff --git a/src/app/layers.rs b/src/app/layers.rs index 7ec119470..474792cde 100644 --- a/src/app/layers.rs +++ b/src/app/layers.rs @@ -62,10 +62,25 @@ impl OpenCADStudio { let i = self.active_tab; let doc_layers = self.tabs[i].scene.document.layers.clone(); let vp_info = self.tabs[i].scene.viewport_list(); + let names_before: Vec = + self.tabs[i].layers.layers.iter().map(|l| l.name.clone()).collect(); self.tabs[i] .layers .sync_with_viewports(&doc_layers, vp_info); self.sync_ribbon_layers(); + // The Properties palette lists layer names it read when it was built; + // a layer added, removed or renamed must reach that list too (#1664). + // Only then: the rebuild is costly for a large selection. + let names_changed = self.tabs[i].layers.layers.len() != names_before.len() + || self.tabs[i] + .layers + .layers + .iter() + .zip(&names_before) + .any(|(layer, before)| layer.name != *before); + if names_changed { + self.refresh_properties(); + } } /// Pick up layers an entity edit registered on the fly (`ensure_layer`). diff --git a/src/app/mod.rs b/src/app/mod.rs index 2532acacb..50dc08597 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -561,6 +561,8 @@ pub(super) struct OpenCADStudio { zoom_wheel_reversed: bool, /// Scroll pans and Ctrl+scroll zooms (touchpads); see `scroll_intent`. touchpad_pan: bool, + /// Interface scale in percent, applied through the window scale factor. + ui_scale_percent: u16, /// Mouse-wheel zoom sensitivity (ZOOMFACTOR), held inside /// `settings::ZOOM_FACTOR_MIN..=settings::ZOOM_FACTOR_MAX`. `SETVAR` /// only sets the range the system variable itself has; the Options @@ -2378,6 +2380,8 @@ pub enum Message { /// Toggle reversing the mouse-wheel zoom direction (ZOOMWHEEL). ZoomWheelReversedChanged(bool), TouchpadPanChanged(bool), + /// Interface scale in percent (Options > Display). + UiScaleChanged(u16), /// Change how far one wheel notch zooms, from the Options slider /// (ZOOMFACTOR). ZoomFactorChanged(i32), @@ -4161,6 +4165,7 @@ impl OpenCADStudio { polar_increment_deg: 45.0, zoom_wheel_reversed: false, touchpad_pan: false, + ui_scale_percent: 100, zoom_factor: 60, zoom_factor_input: 60.to_string(), cursor_size: 5, @@ -4904,6 +4909,7 @@ pub fn run() -> iced::Result { ..iced::Settings::default() }) .subscription(OpenCADStudio::subscription) + .scale_factor(|state: &OpenCADStudio, _| f32::from(state.ui_scale_percent) / 100.0) .title(|state: &OpenCADStudio, window_id: window::Id| { let _ = window_id; // all dialogs are in-canvas modals now if let Some(tab) = state.tabs.get(state.active_tab) { diff --git a/src/app/settings.rs b/src/app/settings.rs index aa6b60548..87ded73a8 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -183,6 +183,11 @@ pub const ZOOM_FACTOR_MAX: i32 = 500; /// Hold a ZOOMFACTOR inside the range the application can store, wherever it /// arrives from — the slider, the Options field, or a configuration file /// written by hand or by an older build. +/// Interface scale in percent, kept to the range the Options slider offers. +pub fn clamp_ui_scale(percent: u16) -> u16 { + percent.clamp(75, 200) +} + pub fn clamp_zoom_factor(v: i32) -> i32 { v.clamp(ZOOM_FACTOR_MIN, ZOOM_FACTOR_MAX) } @@ -391,6 +396,8 @@ pub struct UserSettings { /// Scrolling pans and Ctrl+scroll (a touchpad pinch) zooms, for a /// touchpad that reports two-finger drags as wheel scrolls. pub touchpad_pan: bool, + /// Interface scale in percent, on top of the display's own scale. + pub ui_scale_percent: u16, pub zoom_factor: i32, /// CURSORSIZE: normalized crosshair reach; 5 retains the original 60 px arms. pub cursor_size: i32, @@ -415,6 +422,8 @@ pub struct UserSettings { pub attdef_on_screen: bool, /// FIELDDISPLAY: fields show on a gray background. pub field_display: bool, + /// SURFACEASSOCIATIVITY: new surfaces stay associative to their sources. + pub surface_associativity: bool, /// Nested-copy symbol handling: false inserts, true binds. pub ncopy_bind: bool, /// Last Options page; unknown saved names fall back without rejecting the config. @@ -811,6 +820,7 @@ impl Default for UserSettings { polar_increment_deg: 45.0, zoom_wheel_reversed: false, touchpad_pan: false, + ui_scale_percent: 100, zoom_factor: 60, cursor_size: 5, pick_box: 3, @@ -827,6 +837,7 @@ impl Default for UserSettings { image_frame: 1, attdef_on_screen: true, field_display: true, + surface_associativity: true, ncopy_bind: false, cursor_type: CursorType::Crosshair, crosshair_color: None, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index b9c7cc58c..6e0c8f294 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -663,6 +663,7 @@ impl OpenCADStudio { polar_increment_deg: self.polar_increment_deg, zoom_wheel_reversed: self.zoom_wheel_reversed, touchpad_pan: self.touchpad_pan, + ui_scale_percent: self.ui_scale_percent, zoom_factor: self.zoom_factor, cursor_size: self.cursor_size, pick_box: self.pick_box, @@ -679,6 +680,7 @@ impl OpenCADStudio { image_frame: crate::scene::frame::profile_image_mode(), attdef_on_screen: crate::modules::draw::draw::attdef::session().on_screen, field_display: crate::entities::field::display(), + surface_associativity: crate::scene::model::sweep_model::surface_associativity(), ncopy_bind: self.ncopy_bind, cursor_type: self.cursor_type, crosshair_color: self.crosshair_color, @@ -764,6 +766,7 @@ impl OpenCADStudio { self.polar_increment_deg = s.polar_increment_deg; self.zoom_wheel_reversed = s.zoom_wheel_reversed; self.touchpad_pan = s.touchpad_pan; + self.ui_scale_percent = crate::app::settings::clamp_ui_scale(s.ui_scale_percent); self.zoom_factor = crate::app::settings::clamp_zoom_factor(s.zoom_factor); // Like the drafting-rotation field below, the Options field edits a // buffer rather than the value, so it is reseeded whenever the value @@ -789,6 +792,7 @@ impl OpenCADStudio { crate::scene::frame::set_profile_image_mode(s.image_frame); crate::modules::draw::draw::attdef::session().on_screen = s.attdef_on_screen; crate::entities::field::set_display(s.field_display); + crate::scene::model::sweep_model::set_surface_associativity(s.surface_associativity); self.ncopy_bind = s.ncopy_bind; self.cursor_type = s.cursor_type; self.crosshair_color = s.crosshair_color; diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 61a3dffaf..b4bac5d8b 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -8391,6 +8391,12 @@ impl OpenCADStudio { Task::none() } + Message::UiScaleChanged(percent) => { + self.ui_scale_percent = crate::app::settings::clamp_ui_scale(percent); + self.persist_settings_if_changed(); + Task::none() + } + Message::ZoomFactorChanged(factor) => { self.zoom_factor = crate::app::settings::clamp_zoom_factor(factor); // The field beside the slider reads the same value, so the diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index d12abbd5a..8f177d0dd 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -927,11 +927,14 @@ bg={bg_ms:.1}ms n={view_count}" snap_angle_deg: self.snap_angle_deg, pick_pending: self.pending_pick_label().is_some(), // An infinite-line preview runs along the arms; drop them - // so the preview stays visible (#1437). - hide_arms: tab - .active_cmd - .as_ref() - .is_some_and(|c| matches!(c.name(), "XLINE" | "RAY")), + // so the preview stays visible (#1437). A command asking + // for objects shows the pickbox alone, so picking reads + // apart from both idle and point input (#1661). + hide_arms: tab.active_cmd.as_ref().is_some_and(|c| { + matches!(c.name(), "XLINE" | "RAY") + || c.needs_entity_pick() + || c.is_selection_gathering() + }), snap_color: self.snap_marker_color, }, crate::ui::overlay::SelectionVisualOptions { diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 510b5edc2..83a6d3bfb 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -317,6 +317,7 @@ impl OpenCADStudio { commandline_fade_ms: self.commandline_fade_ms, zoom_wheel_reversed: self.zoom_wheel_reversed, touchpad_pan: self.touchpad_pan, + ui_scale_percent: self.ui_scale_percent, zoom_factor: self.zoom_factor, texteditmode: self.texteditmode, dimension_continue_mode: self.dimension_continue_mode, diff --git a/src/app/viewport_dimension_tests.rs b/src/app/viewport_dimension_tests.rs index 04e09759f..2628f1355 100644 --- a/src/app/viewport_dimension_tests.rs +++ b/src/app/viewport_dimension_tests.rs @@ -27,7 +27,7 @@ fn fixture() -> (OpenCADStudio, Handle, ViewportFrame) { viewport.view_height = 1000.0; viewport.custom_scale = 0.1; viewport.view_direction = Vector3::UNIT_Z; - viewport.status.is_on = true; + viewport.turn_on(); let viewport = scene.add_entity(EntityType::Viewport(viewport)); let frame = scene.viewport_frame(viewport).unwrap(); scene.document.header.dimension_associativity = 2; @@ -484,7 +484,7 @@ fn viewport_dimension_eligibility_is_shared_and_uses_clipping() { first.viewport ); if let Some(EntityType::Viewport(vp)) = scene.document.get_entity_mut(first.viewport) { - vp.status.is_on = false; + vp.turn_off(); } assert!(scene.viewport_frames_at_paper_point(paper).is_empty()); assert!(scene.dimension_pick_through_viewport(paper, 0.2).is_none()); diff --git a/src/entities/viewport.rs b/src/entities/viewport.rs index 3e65cca64..37798bef2 100644 --- a/src/entities/viewport.rs +++ b/src/entities/viewport.rs @@ -113,7 +113,7 @@ fn properties(vp: &Viewport) -> Vec { field: "vp_on", value: PropValue::BoolToggle { field: "vp_on", - value: vp.status.is_on, + value: vp.is_on(), }, }, ro( @@ -193,11 +193,14 @@ fn apply_geom_prop(vp: &mut Viewport, field: &str, value: &str) { return; } "vp_on" => { - vp.status.is_on = if value == "toggle" { - !vp.status.is_on + // turn_on/turn_off keep the explicit-off bit in step, so a + // viewport the file marked off can be turned back on. + let on = if value == "toggle" { !vp.is_on() } else { value == "true" }; + if on { + vp.turn_on(); } else { - value == "true" - }; + vp.turn_off(); + } return; } "vp_perspective" => { diff --git a/src/io/mod.rs b/src/io/mod.rs index 0ad3565bc..7ba12b2c0 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -2539,7 +2539,7 @@ fn fix_viewport_status_flags(doc: &mut CadDocument) { // If bit 0 is not set but bit 15 is, this is an external-format viewport: // treat bit 15 as "on" and bit 14 as "locked". if (bits & 0x0001) == 0 && (bits & 0x8000) != 0 { - vp.status.is_on = true; + vp.turn_on(); vp.status.locked = (bits & 0x4000) != 0; } } diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index b38ab91df..19b4ed554 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -4798,6 +4798,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Zoom" => Some(("common", "zoom")), "Reverse mouse wheel zoom (ZOOMWHEEL)" => Some(("common", "reverse-mouse-wheel-zoom-zoomwheel")), "Touchpad: scroll pans, Ctrl+scroll or pinch zooms" => Some(("common", "touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms")), + "Interface scale" => Some(("common", "interface-scale")), "Zoom factor" => Some(("common", "zoom-factor")), "How far one wheel notch zooms (ZOOMFACTOR)." => Some(("common", "how-far-one-wheel-notch-zooms-zoomfactor")), "Text and Dimensions" => Some(("common", "text-and-dimensions")), diff --git a/src/scene/dwg_native_constraints.rs b/src/scene/dwg_native_constraints.rs index ce2327f55..f2779fa26 100644 --- a/src/scene/dwg_native_constraints.rs +++ b/src/scene/dwg_native_constraints.rs @@ -2482,6 +2482,9 @@ fn detach_preserved_scope( Some(ObjectType::Associative(object)) => match &object.data { AssociativeData::ConstraintGroup(_) | AssociativeData::Network(_) => false, AssociativeData::Variable(variable) => variable.name.trim().is_empty(), + // A surface action whose surface was erased goes with it. + AssociativeData::Action(_) => action_surface(document, *handle) + .is_none_or(|surface| document.get_entity(surface).is_some()), _ => true, }, _ => false, @@ -2489,6 +2492,35 @@ fn detach_preserved_scope( .collect(), _ => Vec::new(), }; + // Unnamed variables stay only while a kept action reads them. + let read_variables: Vec = foreign_actions + .iter() + .filter_map(|handle| match document.objects.get(handle) { + Some(ObjectType::Associative(AssociativeObject { + data: AssociativeData::Action(action), + .. + })) => Some(action), + _ => None, + }) + .flat_map(|action| action.values.iter().flat_map(|value| value.variables.iter().map(|variable| variable.handle))) + .filter_map(|handle| match document.objects.get(&handle) { + Some(ObjectType::Associative(AssociativeObject { + data: AssociativeData::ValueDependency(value), + .. + })) => Some(value.dependency.dependent_on), + _ => None, + }) + .collect(); + let foreign_actions: Vec = foreign_actions + .into_iter() + .filter(|handle| match document.objects.get(handle) { + Some(ObjectType::Associative(AssociativeObject { + data: AssociativeData::Variable(_), + .. + })) => read_variables.contains(handle), + _ => true, + }) + .collect(); let preserved_groups: Vec<_> = native_group_handles(document, owner) .into_iter() .filter(|handle| { @@ -2574,6 +2606,265 @@ fn detach_preserved_scope( (variables, preserved_groups, foreign_actions) } +/// The surface an associative surface action writes. +fn action_surface(document: &CadDocument, action: Handle) -> Option { + let Some(ObjectType::Associative(AssociativeObject { + data: AssociativeData::Action(action), + .. + })) = document.objects.get(&action) + else { + return None; + }; + let Some(ObjectType::Associative(AssociativeObject { + data: AssociativeData::SurfaceActionBody(body), + .. + })) = document.objects.get(&action.action_body) + else { + return None; + }; + match document.objects.get(&body.surface_body.dependency) { + Some(ObjectType::Associative(AssociativeObject { + data: AssociativeData::Dependency(dependency), + .. + })) => Some(dependency.dependent_on), + _ => None, + } +} + +/// Associative swept surfaces made here that the drawing's network does +/// not carry yet: (surface, profile, path). +fn pending_swept_surfaces(document: &CadDocument) -> Vec<(Handle, Handle, Handle)> { + use crate::scene::model::sweep_model; + let carried: Vec = document + .objects + .keys() + .filter_map(|handle| action_surface(document, *handle)) + .collect(); + let mut pending = document + .entities() + .filter(|entity| matches!(entity, codec::EntityType::Surface(surface) + if matches!(surface.surface_data, codec::entities::SurfaceData::Swept { .. }))) + .filter_map(|entity| { + let (profile, path) = sweep_model::sweep_sources(entity)?; + let surface = entity.common().handle; + (!carried.contains(&surface) + && document.get_entity(profile).is_some_and(|e| sweep_model::assoc_edge_curve(e).is_some()) + && document.get_entity(path).is_some_and(|e| sweep_model::assoc_edge_curve(e).is_some())) + .then_some((surface, profile, path)) + }) + .collect::>(); + pending.sort_by_key(|(surface, _, _)| surface.value()); + pending +} + +/// The names an expression reads. +fn expression_names(expression: &str) -> Vec { + let mut names = Vec::new(); + let mut current = String::new(); + for character in expression.chars().chain(std::iter::once(' ')) { + if character.is_alphanumeric() || character == '_' { + current.push(character); + } else if !current.is_empty() { + if current.chars().next().is_some_and(|c| c.is_alphabetic()) && !names.contains(¤t) { + names.push(current.clone()); + } + current.clear(); + } + } + names +} + +/// The reference's associative network for each pending swept surface: an +/// action with a swept-surface body that writes the surface and reads its +/// profile and path, with the rotation, scale and twist as value +/// parameters; a scale or twist expression is an unnamed variable reading +/// the named ones. Returns the network's new actions in order. +fn materialize_swept_surface_actions( + allocator: &mut Allocator, + network: Handle, + parameters: &ParameterTable, + pending: &[(Handle, Handle, Handle)], + first_index: i32, +) -> Vec { + use crate::scene::model::sweep_model; + use codec::objects::*; + let mut added = Vec::new(); + for (surface, profile, path) in pending.iter().copied() { + let document = &*allocator.document; + let Some(surface_entity) = document.get_entity(surface) else { continue }; + let codec::EntityType::Surface(surface_value) = surface_entity else { continue }; + let codec::entities::SurfaceData::Swept { options, .. } = &surface_value.surface_data else { continue }; + let (rotation, scale, twist) = (options.align_angle, options.scale_factor, options.twist_angle); + let expressions = sweep_model::sweep_expressions(surface_entity).unwrap_or([None, None]); + let (Some(profile_curve), Some(path_curve)) = ( + document.get_entity(profile).and_then(sweep_model::assoc_edge_curve), + document.get_entity(path).and_then(sweep_model::assoc_edge_curve), + ) else { continue }; + let evaluate = |expression: &str| { + let mut table = parameters.clone(); + table.set("sweepExpression", expression).ok()?; + table.resolve("sweepExpression").ok().filter(|value| value.is_finite()) + }; + let document = &mut *allocator.document; + let mut next = || document.allocate_handle(); + let (action, body, written, profile_param, profile_edge, profile_read, path_param, path_edge, path_read) = + (next(), next(), next(), next(), next(), next(), next(), next(), next()); + let dependency = |dependent_on: Handle, read: bool, id: i32, order: i32| AssocDependency { + class_version: 2, + status: 0, + is_read_dependency: read, + is_write_dependency: !read, + is_attached_to_object: true, + is_delegating_to_owning_action: true, + order, + dependent_on, + name: None, + read_dependency: Handle::NULL, + node: Handle::NULL, + dependency_body: Handle::NULL, + dependency_body_id: id, + }; + let mut objects: Vec<(Handle, Handle, &str, &str, AssociativeData)> = Vec::new(); + let mut reactors: Vec<(Handle, Handle)> = vec![(surface, written), (profile, profile_read), (path, path_read)]; + let mut dependencies = vec![written, profile_read, path_read]; + let mut value_handles = [Handle::NULL, Handle::NULL]; + let mut network_actions = vec![action]; + // Scale (slot 0) and twist (slot 1) expressions. + for (slot, expression) in expressions.iter().enumerate() { + let Some(expression) = expression else { continue }; + let Some(value) = evaluate(expression) else { continue }; + let (variable, read_by_action) = (document.allocate_handle(), document.allocate_handle()); + let mut variable_dependencies = Vec::new(); + for (index, name) in expression_names(expression).iter().enumerate() { + let Some(named) = allocator.variables.get(name).copied() else { continue }; + let reads = document.allocate_handle(); + let named_value = parameters.resolve(name).unwrap_or(0.0); + objects.push((reads, variable, "ACDBASSOCVALUEDEPENDENCY", "AcDbAssocValueDependency", + AssociativeData::ValueDependency(AssocValueDependency { + dependency: dependency(named, true, index as i32 + 1, 0), + class_version: 0, + name: String::new(), + value: Allocator::numeric_eval(named_value), + }))); + reactors.push((named, reads)); + variable_dependencies.push(AssocVariableDependency { dependency: reads, flags: 0 }); + } + objects.push((variable, network, "ACDBASSOCVARIABLE", "AcDbAssocVariable", AssociativeData::Variable(AssocVariable { + action: AssocAction { + class_version: 2, + owning_network: network, + max_dependency_index: variable_dependencies.len() as i32, + ..Default::default() + }, + class_version: 2, + name: String::new(), + expression: expression.clone(), + evaluator: "AcDbCalc:1.0".to_string(), + description: String::new(), + value: Allocator::numeric_eval(value), + has_cached_value: false, + cached_value: String::new(), + flag: false, + dependencies: variable_dependencies, + }))); + objects.push((read_by_action, action, "ACDBASSOCVALUEDEPENDENCY", "AcDbAssocValueDependency", + AssociativeData::ValueDependency(AssocValueDependency { + dependency: dependency(variable, true, dependencies.len() as i32 + 1, 0), + class_version: 0, + name: String::new(), + value: Allocator::numeric_eval(value), + }))); + reactors.push((variable, read_by_action)); + dependencies.push(read_by_action); + value_handles[slot] = read_by_action; + network_actions.push(variable); + } + let real = |value: f64| AssocEvalVariant { code: 40, value: AssocEvalValue::Real(value) }; + let value = |name: &str, unit_type: i32, amount: f64, handle: Handle| AssocValueParam { + class_version: 0, + name: name.to_string(), + unit_type, + variables: vec![AssocValueParamVariable { value: real(amount), handle }], + controlled_object_dependency: written, + }; + objects.push((action, network, "ACDBASSOCACTION", "AcDbAssocAction", AssociativeData::Action(AssocAction { + class_version: 2, + geometry_status: 0, + owning_network: network, + action_body: body, + action_index: 0, + max_dependency_index: dependencies.len() as i32, + dependencies: dependencies.iter().map(|handle| AssocActionDependency { is_owned: true, dependency: *handle }).collect(), + owned_parameters: vec![profile_param, path_param], + values: vec![ + value("RotationAngle", 2, rotation, Handle::NULL), + value("ScaleFactor", 0, scale, value_handles[0]), + value("TwistAngle", 2, twist, value_handles[1]), + ], + }))); + objects.push((body, action, "ACDBASSOCSWEPTSURFACEACTIONBODY", "AcDbAssocSweptSurfaceActionBody", + AssociativeData::SurfaceActionBody(AssocSurfaceActionBody { + kind: AssocSurfaceActionKind::Swept, + action_body: AssocActionBody { version: 2 }, + surface_body: AssocSurfaceBody { version: 0, dependency: written, is_semi_associative: false, marker: 1, is_semi_override: false, grip_status: 0 }, + ..Default::default() + }))); + objects.push((written, action, "ACDBASSOCDEPENDENCY", "AcDbAssocDependency", AssociativeData::Dependency(dependency(surface, false, 1, i32::MIN)))); + let edge_param = |read: Handle, (kind, subcurve, curve): (i32, AssocSubcurveKind, Vec)| AssociativeData::EdgeActionParam(AssocEdgeActionParam { + single_dependency: AssocSingleDependencyActionParam { + action_param: AssocActionParam { is_r2013: 1, version: 0, name: String::new() }, + dependency_class_version: 0, + dependency: read, + class_version: 0, + }, + parameter: Handle::NULL, + has_action: true, + action_type: kind, + subcurve_kind: subcurve, + curve, + }); + let path_parameter = |name: &str, child: Handle| AssociativeData::PathActionParam(AssocPathActionParam { + compound: AssocCompoundActionParam { + action_param: AssocActionParam { is_r2013: 1, version: 0, name: name.to_string() }, + class_version: 0, + status: 0, + parameters: vec![child], + child_parameter: None, + }, + version: 0, + }); + objects.push((profile_param, action, "ACDBASSOCPATHACTIONPARAM", "AcDbAssocPathActionParam", path_parameter("SweepProfile", profile_edge))); + objects.push((profile_edge, profile_param, "ACDBASSOCEDGEACTIONPARAM", "AcDbAssocEdgeActionParam", edge_param(profile_read, profile_curve))); + objects.push((profile_read, action, "ACDBASSOCDEPENDENCY", "AcDbAssocDependency", AssociativeData::Dependency(dependency(profile, true, 2, 0)))); + objects.push((path_param, action, "ACDBASSOCPATHACTIONPARAM", "AcDbAssocPathActionParam", path_parameter("SweepPath", path_edge))); + objects.push((path_edge, path_param, "ACDBASSOCEDGEACTIONPARAM", "AcDbAssocEdgeActionParam", edge_param(path_read, path_curve))); + objects.push((path_read, action, "ACDBASSOCDEPENDENCY", "AcDbAssocDependency", AssociativeData::Dependency(dependency(path, true, 3, 0)))); + for (handle, owner, dxf, class, data) in objects { + allocator.insert_associative_at(handle, owner, dxf, class, data); + } + for (target, reactor) in reactors { + let document = &mut *allocator.document; + if let Some(entity) = document.get_entity_mut(target) { + entity.common_mut().reactors.push(reactor); + } else if let Some(ObjectType::Associative(object)) = document.objects.get_mut(&target) { + object.reactors.push(reactor); + } + } + added.extend(network_actions); + } + for (offset, handle) in added.iter().copied().enumerate() { + if let Some(ObjectType::Associative(object)) = allocator.document.objects.get_mut(&handle) { + let action = match &mut object.data { + AssociativeData::Action(action) => action, + AssociativeData::Variable(variable) => &mut variable.action, + _ => continue, + }; + action.action_index = first_index + offset as i32 + 1; + } + } + added +} + /// Re-evaluates the unnamed variables of kept actions (a swept surface's /// scale or twist expression) and the values their actions cache, so the /// file carries the values the named parameters give. @@ -2714,6 +3005,7 @@ fn materialize_scope( && existing_variables.is_empty() && preserved_groups.is_empty() && foreign_actions.is_empty() + && !(materialize_all_parameters && !pending_swept_surfaces(document).is_empty()) { return None; } @@ -2898,6 +3190,9 @@ fn materialize_scope( } } refresh_foreign_values(allocator.document, &foreign_actions, parameters); + let pending = if materialize_all_parameters { pending_swept_surfaces(allocator.document) } else { Vec::new() }; + let surface_actions = materialize_swept_surface_actions( + &mut allocator, network_handle, parameters, &pending, first_foreign + foreign_actions.len() as i32); let network = AssocNetwork { action: AssocAction { @@ -2907,7 +3202,7 @@ fn materialize_scope( ..Default::default() }, network_version: 0, - network_action_index: first_foreign + foreign_actions.len() as i32, + network_action_index: first_foreign + (foreign_actions.len() + surface_actions.len()) as i32, actions: allocator .variable_actions .iter() @@ -2915,6 +3210,7 @@ fn materialize_scope( .chain(group_handle) .chain(preserved_groups) .chain(foreign_actions) + .chain(surface_actions) .map(|dependency| AssocActionDependency { is_owned: true, dependency, @@ -3055,7 +3351,8 @@ impl Scene { }; if set.is_none() && !existing - && (owner != model_owner || self.named_parameters.is_empty()) + && (owner != model_owner + || (self.named_parameters.is_empty() && pending_swept_surfaces(&self.document).is_empty())) { continue; } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 025b31d6c..bb92874c9 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -3696,6 +3696,11 @@ impl Scene { changes.push(change); } } + for change in self.refresh_associative_sweeps(&changes) { + if !changes.iter().any(|(handle, _)| *handle == change.0) { + changes.push(change); + } + } if solve_parametric && !self.parametric_constraints.is_empty() { for change in self.refresh_parametric_constraints_with_initial_policy( &changes, @@ -7499,7 +7504,7 @@ impl Scene { let Some(EntityType::Viewport(viewport)) = self.document.get_entity(handle) else { continue; }; - if viewport.common.owner_handle != paper_block || !viewport.status.is_on { + if viewport.common.owner_handle != paper_block || !viewport.is_on() { continue; } let mode = render_mode_override.unwrap_or(viewport.render_mode); diff --git a/src/scene/model/sweep_command_model.rs b/src/scene/model/sweep_command_model.rs index 7913e22a6..5d68a3994 100644 --- a/src/scene/model/sweep_command_model.rs +++ b/src/scene/model/sweep_command_model.rs @@ -167,7 +167,11 @@ pub fn sweep_selection_options(profiles: &[EntityType], mut options: SweepOption } // Spatial profiles keep their own anchor. let profiles = profiles.iter().filter(|profile| spatial_profile(profile).is_none()).collect::>(); - if profiles.is_empty() { + // A single profile takes its base from the path it is swept along + // (`sweep_record`). + // ponytail: several profiles share the group anchor even when the path + // starts inside one of them; the reference rule for that case is unmeasured. + if profiles.len() < 2 { return Some(options); } let geometry = profiles.iter().map(|profile| { @@ -184,23 +188,7 @@ pub fn sweep_selection_options(profiles: &[EntityType], mut options: SweepOption /// The path traversed the other way when it was picked nearer its end /// (measured in plan, as the reference measures the pick), else `None`. fn reversed_toward_pick(path: &EntityType, pick: glam::DVec3) -> Option { - let (start, end) = match path { - EntityType::Polyline3D(value) if !value.is_closed() => { - let p = |v: &codec::entities::Vertex3DPolyline| glam::DVec3::new(v.position.x, v.position.y, v.position.z); - (p(value.vertices.first()?), p(value.vertices.last()?)) - } - EntityType::Spline(value) if !value.flags.closed => { - let points = if value.control_points.is_empty() { &value.fit_points } else { &value.control_points }; - let p = |v: &codec::types::Vector3| glam::DVec3::new(v.x, v.y, v.z); - (p(points.first()?), p(points.last()?)) - } - _ => { - let planar = crate::entities::curve::entity_curve(path)?; - if planar.curve.is_closed() { return None; } - let at = |t: f64| glam::DVec3::from_array(planar.plane.point_at(planar.curve.point_at(t))); - (at(0.0), at(1.0)) - } - }; + let (start, end) = path_ends(path)?; let flat = |p: glam::DVec3| p.truncate().distance(pick.truncate()); if flat(end) >= flat(start) { return None; } match path { @@ -220,6 +208,27 @@ fn reversed_toward_pick(path: &EntityType, pick: glam::DVec3) -> Option Option<(glam::DVec3, glam::DVec3)> { + Some(match path { + EntityType::Polyline3D(value) if !value.is_closed() => { + let p = |v: &codec::entities::Vertex3DPolyline| glam::DVec3::new(v.position.x, v.position.y, v.position.z); + (p(value.vertices.first()?), p(value.vertices.last()?)) + } + EntityType::Spline(value) if !value.flags.closed => { + let points = if value.control_points.is_empty() { &value.fit_points } else { &value.control_points }; + let p = |v: &codec::types::Vector3| glam::DVec3::new(v.x, v.y, v.z); + (p(points.first()?), p(points.last()?)) + } + _ => { + let planar = crate::entities::curve::entity_curve(path)?; + if planar.curve.is_closed() { return None; } + let at = |t: f64| glam::DVec3::from_array(planar.plane.point_at(planar.curve.point_at(t))); + (at(0.0), at(1.0)) + } + }) +} + pub fn sweep_record(profile: &EntityType, path: &EntityType, options: SweepOptions) -> Option { // Placed from the picked end; the record keeps the original path, and // the placed frame tells which end the sweep starts from. @@ -229,10 +238,10 @@ pub fn sweep_record(profile: &EntityType, path: &EntityType, options: SweepOptio return Some(record); } let (sweep_entity, sweep_entity_transform) = embedded_sweep_profile(profile)?; - let (plane, wires, _) = kernel::acis::sweep_profile_geometry(&sweep_entity, sweep_entity_transform).ok()?; + kernel::acis::sweep_profile_geometry(&sweep_entity, sweep_entity_transform).ok()?; let base_point = match options.base_point { Some(point) => point.to_array(), - None => kernel::brep::sweep_profile_base(plane, &wires)?, + None => kernel::acis::sweep_default_base(&sweep_entity, sweep_entity_transform, &embedded_sweep_path(path)?).ok()?, }; let mut base = SolidHistoryNodeBase::new(1); base.transform = glam::DMat4::IDENTITY.to_cols_array(); @@ -337,6 +346,195 @@ pub fn link_sweep_expressions( crate::scene::view::dispatch::set_entity_xdata(document, handle, SWEEP_EXPRESSION_APP, Some(values)); } +/// SURFACEASSOCIATIVITY: whether new surfaces stay associative to the +/// objects they were made from. A profile setting, so one value for every +/// drawing. +static SURFACE_ASSOCIATIVITY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true); + +pub fn surface_associativity() -> bool { + SURFACE_ASSOCIATIVITY.load(std::sync::atomic::Ordering::Relaxed) +} + +pub fn set_surface_associativity(on: bool) { + SURFACE_ASSOCIATIVITY.store(on, std::sync::atomic::Ordering::Relaxed); +} + +/// The application name under which an associative swept surface keeps +/// the profile and path it was swept from. +pub const SWEEP_SOURCES_APP: &str = "OCS_SWEEP_SOURCES"; + +/// Marks a swept surface associative to the profile and path it was swept +/// from, with the options it is swept again with when they change; the +/// drawing's associative network for it is written on save. +pub fn link_sweep_sources( + document: &mut codec::CadDocument, + surface: codec::Handle, + profile: codec::Handle, + path: codec::Handle, + options: &SweepOptions, +) { + use codec::xdata::XDataValue; + if surface.is_null() { return; } + let point = |p: glam::DVec3| XDataValue::Point3D(Vector3::new(p.x, p.y, p.z)); + // The path keeps the direction it was swept in when it is edited. + let reversed = options.path_pick + .zip(document.get_entity(path)) + .is_some_and(|(pick, path)| reversed_toward_pick(path, pick).is_some()); + let mut values = vec![ + XDataValue::Handle(profile), + XDataValue::Handle(path), + XDataValue::Integer16(i16::from(options.align) | (i16::from(options.bank) << 1) | (i16::from(reversed) << 2)), + ]; + if let Some(base) = options.base_point { + values.push(XDataValue::String("BASE".to_string())); + values.push(point(base)); + } + crate::scene::view::dispatch::set_entity_xdata(document, surface, SWEEP_SOURCES_APP, Some(values)); +} + +/// The profile and path an associative swept surface was swept from. +pub fn sweep_sources(entity: &EntityType) -> Option<(codec::Handle, codec::Handle)> { + let record = entity.common().extended_data.records().iter() + .find(|record| record.application_name == SWEEP_SOURCES_APP)?; + let mut handles = record.values.iter().filter_map(|value| match value { + codec::xdata::XDataValue::Handle(handle) => Some(*handle), + _ => None, + }); + Some((handles.next()?, handles.next()?)) +} + +/// The surface swept again from its current profile and path, with the +/// options it was made with: (surface record, body). +pub fn resweep_from_sources( + surface: &EntityType, + profile: &EntityType, + path: &EntityType, +) -> Option<(SolidHistorySweep, Body)> { + use codec::xdata::XDataValue; + let record = surface.common().extended_data.records().iter() + .find(|record| record.application_name == SWEEP_SOURCES_APP)?; + let EntityType::Surface(value) = surface else { return None }; + let SurfaceData::Swept { options: stored, .. } = &value.surface_data else { return None }; + let mut options = SweepOptions { + scale: stored.scale_factor, + twist_angle: stored.twist_angle, + ..SweepOptions::default() + }; + let mut values = record.values.iter(); + while let Some(value) = values.next() { + match value { + XDataValue::Integer16(flags) => { + options.align = flags & 1 != 0; + options.bank = flags & 2 != 0; + // Swept from the end: picked there. + if flags & 4 != 0 { + options.path_pick = path_ends(path).map(|(_, end)| end); + } + } + XDataValue::String(key) if key == "BASE" => { + if let Some(XDataValue::Point3D(p)) = values.next() { + options.base_point = Some(glam::DVec3::new(p.x, p.y, p.z)); + } + } + _ => {} + } + } + let record = if spatial_profile(profile).is_some() { + spatial_sweep_record(profile, path, options).ok()? + } else { + sweep_record(profile, path, options)? + }; + let body = kernel::acis::rebuild_sweep_with_mode(&record, true).ok()?; + Some((record, body)) +} + +/// An entity's curve as the reference stores it in an edge action +/// parameter: line segment (23: start, vector to the end), arc (11: +/// centre, normal, reference axis, radius, start and end angle, 0) or a +/// composite of those (47: count, then each part's type and values). +pub fn assoc_edge_curve(entity: &EntityType) -> Option<(i32, codec::objects::AssocSubcurveKind, Vec)> { + use codec::objects::{AssocCurveValue as V, AssocSubcurveKind as K}; + use codec::types::Vector3 as P; + let point = |p: glam::DVec3| V::Point(P::new(p.x, p.y, p.z)); + let segment = |a: glam::DVec3, b: glam::DVec3| vec![point(a), point(b - a)]; + // An arc about `normal` from `start` through `sweep` (signed, about the + // normal) on a circle of `radius`; measured from the reference axis. + let arc = |centre: glam::DVec3, normal: glam::DVec3, axis: glam::DVec3, radius: f64, start: f64, end: f64| vec![ + point(centre), point(normal), point(axis), V::Real(radius), V::Real(start), V::Real(end), V::Real(0.0), + ]; + match entity { + EntityType::Line(line) => { + let (a, b) = (glam::DVec3::new(line.start.x, line.start.y, line.start.z), glam::DVec3::new(line.end.x, line.end.y, line.end.z)); + Some((23, K::LineSegment3d, segment(a, b))) + } + EntityType::Circle(circle) => { + let normal = glam::DVec3::new(circle.normal.x, circle.normal.y, circle.normal.z).try_normalize()?; + let axis = arbitrary_x(normal); + Some((11, K::Arc, arc(glam::DVec3::new(circle.center.x, circle.center.y, circle.center.z), normal, axis, circle.radius, 0.0, std::f64::consts::TAU))) + } + EntityType::Arc(value) => { + let normal = glam::DVec3::new(value.normal.x, value.normal.y, value.normal.z).try_normalize()?; + let axis = arbitrary_x(normal); + // The end past the start by the arc's sweep; an equal pair is a + // full turn. (A loop adding TAU never ends on a huge angle.) + let sweep = (value.end_angle - value.start_angle).rem_euclid(std::f64::consts::TAU); + let end = value.start_angle + if sweep > 0.0 { sweep } else { std::f64::consts::TAU }; + Some((11, K::Arc, arc(glam::DVec3::new(value.center.x, value.center.y, value.center.z), normal, axis, value.radius, value.start_angle, end))) + } + EntityType::LwPolyline(polyline) => { + let normal = glam::DVec3::new(polyline.normal.x, polyline.normal.y, polyline.normal.z).try_normalize()?; + let axis_x = arbitrary_x(normal); + let axis_y = normal.cross(axis_x); + let at = |v: &codec::entities::LwVertex| axis_x * v.location.x + axis_y * v.location.y + normal * polyline.elevation; + let count = polyline.vertices.len(); + let spans = if polyline.is_closed { count } else { count.saturating_sub(1) }; + if spans == 0 { return None; } + let mut values = vec![V::Int(spans as i32)]; + for index in 0..spans { + let (from, to) = (&polyline.vertices[index], &polyline.vertices[(index + 1) % count]); + let (a, b) = (at(from), at(to)); + if from.bulge.abs() <= 1e-12 { + values.push(V::Int(23)); + values.extend(segment(a, b)); + } else { + // The arc in the polyline's plane; a clockwise one turns about + // the opposite normal, from the same reference axis. + let bulge = from.bulge; + let chord = b - a; + let centre = a + chord * 0.5 + normal.cross(chord) * ((1.0 - bulge * bulge) / (4.0 * bulge)); + let radius = chord.length() * (1.0 + bulge * bulge) / (4.0 * bulge.abs()); + let turn = normal * bulge.signum(); + let across = turn.cross(axis_x); + let angle = |p: glam::DVec3| (p - centre).dot(across).atan2((p - centre).dot(axis_x)); + let start = angle(a); + let end = start + 4.0 * bulge.atan().abs(); + values.push(V::Int(11)); + values.extend(arc(centre, turn, axis_x, radius, start, end)); + } + } + Some((47, K::None, values)) + } + EntityType::Polyline3D(polyline) => { + let points = polyline.vertices.iter().map(|v| glam::DVec3::new(v.position.x, v.position.y, v.position.z)).collect::>(); + let spans = if polyline.is_closed() { points.len() } else { points.len().saturating_sub(1) }; + if spans == 0 { return None; } + let mut values = vec![V::Int(spans as i32)]; + for index in 0..spans { + values.push(V::Int(23)); + values.extend(segment(points[index], points[(index + 1) % points.len()])); + } + Some((47, K::None, values)) + } + _ => None, + } +} + +/// The arbitrary-axis X direction of an entity normal. +fn arbitrary_x(normal: glam::DVec3) -> glam::DVec3 { + let world = if normal.x.abs() < 1.0 / 64.0 && normal.y.abs() < 1.0 / 64.0 { glam::DVec3::Y } else { glam::DVec3::Z }; + world.cross(normal).normalize() +} + /// The scale and twist expressions a swept surface is linked to. pub fn sweep_expressions(entity: &EntityType) -> Option<[Option; 2]> { let record = entity.common().extended_data.records().iter() diff --git a/src/scene/model/sweep_model.rs b/src/scene/model/sweep_model.rs index 3c12024f7..0d247adb4 100644 --- a/src/scene/model/sweep_model.rs +++ b/src/scene/model/sweep_model.rs @@ -13,7 +13,7 @@ use codec::EntityType; use crate::entities::curve::entity_curve; pub use super::sweep_command_model::{ - is_sweep_path, is_sweep_profile, scaled_sweep_path_refusal, spatial_profile, spatial_sweep_record, surface_sweep_record, sweep_expressions, link_sweep_expressions, sweep_record, sweep_refusal_code, sweep_selection_options, swept_surface_entity, swept_with_options, + is_sweep_path, is_sweep_profile, scaled_sweep_path_refusal, spatial_profile, spatial_sweep_record, surface_sweep_record, sweep_expressions, link_sweep_expressions, link_sweep_sources, sweep_sources, SWEEP_SOURCES_APP, resweep_from_sources, surface_associativity, set_surface_associativity, assoc_edge_curve, sweep_record, sweep_refusal_code, sweep_selection_options, swept_surface_entity, swept_with_options, }; /// A drawn profile, as the kernel wants it: the plane it lies in and the diff --git a/src/scene/modify.rs b/src/scene/modify.rs index ac069dcaf..dbde57ece 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -1130,6 +1130,54 @@ impl Scene { rebuilt } + /// Associative swept surfaces follow edits of their profile or path: + /// swept again from them with the options they were made with. A + /// surface edited along with its sources keeps its own edit. Returns the + /// surfaces rebuilt. + pub(crate) fn refresh_associative_sweeps(&mut self, changes: &[(Handle, crate::scene::ChangeKind)]) -> Vec<(Handle, crate::scene::ChangeKind)> { + use crate::scene::model::sweep_model; + let changed = changes.iter() + .filter(|(_, kind)| !matches!(kind, crate::scene::ChangeKind::Removed)) + .map(|(handle, _)| *handle) + .collect::>(); + // Every edit passes through here: a drawing with no associative + // swept surface (no application registered for them) skips the scan. + if changed.is_empty() || !self.document.app_ids.contains(sweep_model::SWEEP_SOURCES_APP) { + return Vec::new(); + } + let linked = self.document.entities() + .filter(|entity| matches!(entity, EntityType::Surface(_))) + .filter_map(|entity| { + let handle = entity.common().handle; + let (profile, path) = sweep_model::sweep_sources(entity)?; + (!changed.contains(&handle) && (changed.contains(&profile) || changed.contains(&path))) + .then_some((handle, profile, path)) + }) + .collect::>(); + let mut rebuilt = Vec::new(); + for (handle, profile, path) in linked { + let (Some(surface), Some(profile), Some(path)) = ( + self.document.get_entity(handle), + self.document.get_entity(profile), + self.document.get_entity(path), + ) else { continue }; + let Some((record, body)) = sweep_model::resweep_from_sources(surface, profile, path) else { continue }; + let EntityType::Surface(fresh) = sweep_model::swept_surface_entity(&record) else { continue }; + let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&body) else { continue }; + let Some(display) = self.prepare_solid_model_display(handle, &body) else { continue }; + let before = self.document.get_entity(handle).cloned().map(std::sync::Arc::new); + self.record_undo_before(handle, before); + if let Some(EntityType::Surface(surface)) = self.document.get_entity_mut(handle) { + surface.acis_data = codec::entities::AcisData::from_sat(&document.to_sat_string()); + surface.surface_data = fresh.surface_data; + surface.point_of_reference = fresh.point_of_reference; + } + self.register_prepared_solid_model(handle, body, display); + rebuilt.push((handle, crate::scene::ChangeKind::Modified)); + } + rebuilt + } + /// Swept surfaces the reference keeps associative to a scale or twist /// expression (an associative swept surface action whose value parameter /// depends on an expression variable) are linked the same way here. @@ -1140,6 +1188,7 @@ impl Scene { _ => None, }; let mut links = Vec::new(); + let mut sources = Vec::new(); for item in self.document.objects.values() { let ObjectType::Associative(action) = item else { continue }; let AssociativeData::Action(action) = &action.data else { continue }; @@ -1160,6 +1209,32 @@ impl Scene { if expressions.iter().any(Option::is_some) { links.push((written.dependent_on, expressions)); } + // The profile and path the action reads. + let source = |name: &str| { + action.owned_parameters.iter().find_map(|parameter| { + let Some(AssociativeData::PathActionParam(path)) = object(*parameter) else { return None }; + if path.compound.action_param.name != name { return None; } + let Some(AssociativeData::EdgeActionParam(edge)) = object(*path.compound.parameters.first()?) else { return None }; + match object(edge.single_dependency.dependency) { + Some(AssociativeData::Dependency(dependency)) => Some(dependency.dependent_on), + _ => None, + } + }) + }; + if let (Some(profile), Some(path)) = (source("SweepProfile"), source("SweepPath")) { + sources.push((written.dependent_on, profile, path)); + } + } + for (surface, profile, path) in sources { + let Some(entity @ EntityType::Surface(value)) = self.document.get_entity(surface) else { continue }; + if crate::scene::model::sweep_model::sweep_sources(entity).is_some() { continue; } + let codec::entities::SurfaceData::Swept { options, .. } = &value.surface_data else { continue }; + let linked = crate::command::SweepOptions { + align: options.sweep_alignment_flags == 1, + bank: options.bank, + ..crate::command::SweepOptions::default() + }; + crate::scene::model::sweep_model::link_sweep_sources(&mut self.document, surface, profile, path, &linked); } for (handle, expressions) in links { let Some(entity) = self.document.get_entity(handle) else { continue }; diff --git a/src/scene/mspace.rs b/src/scene/mspace.rs index 2d4a0b305..de7e10b5a 100644 --- a/src/scene/mspace.rs +++ b/src/scene/mspace.rs @@ -801,7 +801,7 @@ impl Scene { let Some(EntityType::Viewport(vp)) = self.document.get_entity(*handle) else { return None; }; - if !vp.status.is_on { + if !vp.is_on() { return None; } let rect = self.viewport_screen_rect(vp.common.handle, canvas)?; @@ -838,7 +838,7 @@ impl Scene { pub fn viewport_displays_content(&self, vp_handle: Handle) -> bool { matches!( self.document.get_entity(vp_handle), - Some(EntityType::Viewport(vp)) if vp.status.is_on + Some(EntityType::Viewport(vp)) if vp.is_on() ) } @@ -850,7 +850,7 @@ impl Scene { let Some(EntityType::Viewport(vp)) = self.document.get_entity(*handle) else { return None; }; - if vp.status.is_on { + if vp.is_on() { Some(vp.common.handle) } else { None diff --git a/src/scene/paper.rs b/src/scene/paper.rs index 11aa7b34d..669d6f74c 100644 --- a/src/scene/paper.rs +++ b/src/scene/paper.rs @@ -194,7 +194,7 @@ impl Scene { let Some(EntityType::Viewport(vp)) = self.document.get_entity(handle) else { continue; }; - if !vp.status.is_on + if !vp.is_on() || self.entity_temporarily_hidden(handle) { continue; diff --git a/src/scene/project.rs b/src/scene/project.rs index 765c6169a..0cf433927 100644 --- a/src/scene/project.rs +++ b/src/scene/project.rs @@ -21,7 +21,7 @@ impl Scene { }) .filter(|vp| { vp.common.owner_handle == paper_block - && vp.status.is_on + && vp.is_on() && only_vp.map_or(true, |h| vp.common.handle == h) && exclude_vp.map_or(true, |h| vp.common.handle != h) }) @@ -439,7 +439,7 @@ impl Scene { .iter() .filter_map(|handle| match self.document.get_entity(*handle) { Some(EntityType::Viewport(viewport)) - if viewport.common.owner_handle == paper_block && viewport.status.is_on => + if viewport.common.owner_handle == paper_block && viewport.is_on() => { Some(viewport) } diff --git a/src/ui/window/options.rs b/src/ui/window/options.rs index a8ebaa668..427cbc742 100644 --- a/src/ui/window/options.rs +++ b/src/ui/window/options.rs @@ -61,6 +61,8 @@ pub struct AppPrefs { pub zoom_wheel_reversed: bool, /// Scroll pans and Ctrl+scroll zooms (touchpads). pub touchpad_pan: bool, + /// Interface scale in percent. + pub ui_scale_percent: u16, /// ZOOMFACTOR. The slider sets the range the system variable has; the /// field beside it reaches `settings::ZOOM_FACTOR_MAX`. pub zoom_factor: i32, @@ -679,6 +681,23 @@ pub fn view_window<'a>( .width(sizing.width), Space::new().height(12), color_controls, + Space::new().height(16), + // A list, not a slider: the dialog rescales under the cursor as the + // value changes, which would drag a slider along with it. + row![ + text(crate::t!("Interface scale")).size(12).width(150), + iced::widget::pick_list( + Some(format!("{}%", prefs.ui_scale_percent)), + [75u16, 100, 125, 150, 175, 200].map(|p| format!("{p}%")).to_vec(), + |label: &String| label.clone(), + ) + .on_select(|label: String| { + Message::UiScaleChanged(label.trim_end_matches('%').parse().unwrap_or(100)) + }) + .width(sizing.width), + ] + .spacing(12) + .align_y(iced::Center), Space::new().height(24), row![ text(crate::t!("Model Space Appearance")).size(15),