forked from 1b-agent/opencadstudio-upstream
feat(gradient): align editing and rendering
This commit is contained in:
commit
2a852f4479
37 files changed
+539
-371
No files matched your search
Generated
+2
-2
@@ -72,7 +72,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "acadrust"
|
name = "acadrust"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=0975677#0975677029f2b472759db00e9692421a5831ad00"
|
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=3742f38#3742f382e6f359178ec7bed6b7719c7ac85f3538"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cadkernel"
|
name = "cadkernel"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=3d5de36#3d5de36cb5b391f17da924c5f80e34a0cdc1d948"
|
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=927f888#927f88839bbffe024579e14f2acd4cc87ed4a05a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acadrust",
|
"acadrust",
|
||||||
"cavalier_contours",
|
"cavalier_contours",
|
||||||
|
|||||||
+2
-2
@@ -27,8 +27,8 @@ glam = { version = "0.33", features = ["bytemuck"] }
|
|||||||
rfd = "0.17"
|
rfd = "0.17"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "0975677", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "3742f38", features = ["serde"] }
|
||||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "3d5de36", features = ["acis", "offset"] }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "927f888", features = ["acis", "offset"] }
|
||||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
|
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
|
||||||
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
||||||
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
|
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "0975677", optional = true, features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "3742f38", optional = true, features = ["serde"] }
|
||||||
|
|
||||||
# Runtime IPC and serialization (host feature only).
|
# Runtime IPC and serialization (host feature only).
|
||||||
interprocess = { version = "2", optional = true }
|
interprocess = { version = "2", optional = true }
|
||||||
@@ -37,7 +37,7 @@ serde_json = "1"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
cargo-lock = "11"
|
cargo-lock = "11"
|
||||||
# acadrust is scanned at build time to generate the embedded type registry.
|
# acadrust is scanned at build time to generate the embedded type registry.
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "0975677", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "3742f38", features = ["serde"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
@@ -8,7 +8,7 @@ publish = false
|
|||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "0975677", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "3742f38", features = ["serde"] }
|
||||||
bincode = "1.3"
|
bincode = "1.3"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
|
|||||||
@@ -2025,6 +2025,11 @@ model =
|
|||||||
.cv-hull = غلاف نقاط التحكم
|
.cv-hull = غلاف نقاط التحكم
|
||||||
.class-entity = الفئة / الكيان
|
.class-entity = الفئة / الكيان
|
||||||
.curved = منحني
|
.curved = منحني
|
||||||
|
.cylindrical = أسطواني
|
||||||
|
.inverted-curved = منحني معكوس
|
||||||
|
.inverted-cylindrical = أسطواني معكوس
|
||||||
|
.inverted-hemispherical = نصف كروي معكوس
|
||||||
|
.inverted-spherical = كروي معكوس
|
||||||
.data-size = حجم البيانات
|
.data-size = حجم البيانات
|
||||||
.database-curves = منحنيات قاعدة البيانات
|
.database-curves = منحنيات قاعدة البيانات
|
||||||
.design = التصميم
|
.design = التصميم
|
||||||
|
|||||||
@@ -2012,6 +2012,11 @@ model =
|
|||||||
.cv-hull = CV обвивка
|
.cv-hull = CV обвивка
|
||||||
.class-entity = Клас / Обект
|
.class-entity = Клас / Обект
|
||||||
.curved = Извит
|
.curved = Извит
|
||||||
|
.cylindrical = Цилиндричен
|
||||||
|
.inverted-curved = Обърнат извит
|
||||||
|
.inverted-cylindrical = Обърнат цилиндричен
|
||||||
|
.inverted-hemispherical = Обърнат полусферичен
|
||||||
|
.inverted-spherical = Обърнат сферичен
|
||||||
.data-size = Размер на данни
|
.data-size = Размер на данни
|
||||||
.database-curves = Криви в базата данни
|
.database-curves = Криви в базата данни
|
||||||
.design = Дизайн
|
.design = Дизайн
|
||||||
|
|||||||
@@ -2012,6 +2012,11 @@ model =
|
|||||||
.cv-hull = CV Trup
|
.cv-hull = CV Trup
|
||||||
.class-entity =Třída / Entita
|
.class-entity =Třída / Entita
|
||||||
.curved = Zakřivené
|
.curved = Zakřivené
|
||||||
|
.cylindrical = Válcové
|
||||||
|
.inverted-curved = Obrácené zakřivené
|
||||||
|
.inverted-cylindrical = Obrácené válcové
|
||||||
|
.inverted-hemispherical = Obrácené polokulovité
|
||||||
|
.inverted-spherical = Obrácené sférické
|
||||||
.data-size = Velikost dat
|
.data-size = Velikost dat
|
||||||
.database-curves = Databázové křivky
|
.database-curves = Databázové křivky
|
||||||
.design = Design
|
.design = Design
|
||||||
|
|||||||
@@ -2011,6 +2011,11 @@ model =
|
|||||||
.cv-hull = Kontrollpolygon
|
.cv-hull = Kontrollpolygon
|
||||||
.class-entity = Klasse / Objekt
|
.class-entity = Klasse / Objekt
|
||||||
.curved = gebogen
|
.curved = gebogen
|
||||||
|
.cylindrical = Zylindrisch
|
||||||
|
.inverted-curved = Invertiert gebogen
|
||||||
|
.inverted-cylindrical = Invertiert zylindrisch
|
||||||
|
.inverted-hemispherical = Invertiert hemisphärisch
|
||||||
|
.inverted-spherical = Invertiert kugelförmig
|
||||||
.data-size = Datengröße
|
.data-size = Datengröße
|
||||||
.database-curves = Datenbankkurven
|
.database-curves = Datenbankkurven
|
||||||
.design = Design
|
.design = Design
|
||||||
|
|||||||
@@ -2014,6 +2014,11 @@ model =
|
|||||||
.cv-hull = CV Hull
|
.cv-hull = CV Hull
|
||||||
.class-entity = Class / Entity
|
.class-entity = Class / Entity
|
||||||
.curved = Curved
|
.curved = Curved
|
||||||
|
.cylindrical = Cylindrical
|
||||||
|
.inverted-curved = Inverted curved
|
||||||
|
.inverted-cylindrical = Inverted cylindrical
|
||||||
|
.inverted-hemispherical = Inverted hemispherical
|
||||||
|
.inverted-spherical = Inverted spherical
|
||||||
.data-size = Data Size
|
.data-size = Data Size
|
||||||
.database-curves = Database Curves
|
.database-curves = Database Curves
|
||||||
.design = Design
|
.design = Design
|
||||||
|
|||||||
@@ -2013,6 +2013,11 @@ model =
|
|||||||
.cv-hull = Envolvente de vértices de control
|
.cv-hull = Envolvente de vértices de control
|
||||||
.class-entity = Clase/Entidad
|
.class-entity = Clase/Entidad
|
||||||
.curved = Curvado
|
.curved = Curvado
|
||||||
|
.cylindrical = Cilíndrico
|
||||||
|
.inverted-curved = Curvado invertido
|
||||||
|
.inverted-cylindrical = Cilíndrico invertido
|
||||||
|
.inverted-hemispherical = Hemisférico invertido
|
||||||
|
.inverted-spherical = Esférico invertido
|
||||||
.data-size = Tamaño de los datos
|
.data-size = Tamaño de los datos
|
||||||
.database-curves = Curvas de base de datos
|
.database-curves = Curvas de base de datos
|
||||||
.design = Diseño
|
.design = Diseño
|
||||||
|
|||||||
@@ -2012,6 +2012,11 @@ model =
|
|||||||
.cv-hull = CV Runko
|
.cv-hull = CV Runko
|
||||||
.class-entity =Luokka / Entiteetti
|
.class-entity =Luokka / Entiteetti
|
||||||
.curved = Kaareva
|
.curved = Kaareva
|
||||||
|
.cylindrical = Sylinterimäinen
|
||||||
|
.inverted-curved = Käänteinen kaareva
|
||||||
|
.inverted-cylindrical = Käänteinen sylinterimäinen
|
||||||
|
.inverted-hemispherical = Käänteinen puolipallomainen
|
||||||
|
.inverted-spherical = Käänteinen pallomainen
|
||||||
.data-size = Tietojen koko
|
.data-size = Tietojen koko
|
||||||
.database-curves = Tietokantakäyrät
|
.database-curves = Tietokantakäyrät
|
||||||
.design = Suunnittelu
|
.design = Suunnittelu
|
||||||
|
|||||||
@@ -2013,6 +2013,11 @@ model =
|
|||||||
.cv-hull = CV Houille
|
.cv-hull = CV Houille
|
||||||
.class-entity = Catégorie/entité
|
.class-entity = Catégorie/entité
|
||||||
.curved = Courbé
|
.curved = Courbé
|
||||||
|
.cylindrical = Cylindrique
|
||||||
|
.inverted-curved = Courbé inversé
|
||||||
|
.inverted-cylindrical = Cylindrique inversé
|
||||||
|
.inverted-hemispherical = Hémisphérique inversé
|
||||||
|
.inverted-spherical = Sphérique inversé
|
||||||
.data-size = Taille des données
|
.data-size = Taille des données
|
||||||
.database-curves = Courbes de base de données
|
.database-curves = Courbes de base de données
|
||||||
.design = Conception
|
.design = Conception
|
||||||
|
|||||||
@@ -2001,6 +2001,11 @@ model =
|
|||||||
.cv-hull = CV हल
|
.cv-hull = CV हल
|
||||||
.class-entity = क्लास / ऑब्जेक्ट
|
.class-entity = क्लास / ऑब्जेक्ट
|
||||||
.curved = घुमावदार
|
.curved = घुमावदार
|
||||||
|
.cylindrical = बेलनाकार
|
||||||
|
.inverted-curved = उलटा घुमावदार
|
||||||
|
.inverted-cylindrical = उलटा बेलनाकार
|
||||||
|
.inverted-hemispherical = उलटा अर्धगोलाकार
|
||||||
|
.inverted-spherical = उलटा गोलाकार
|
||||||
.data-size = डेटा आकार
|
.data-size = डेटा आकार
|
||||||
.database-curves = डेटाबेस वक्र
|
.database-curves = डेटाबेस वक्र
|
||||||
.design = डिजाइन
|
.design = डिजाइन
|
||||||
|
|||||||
@@ -2012,6 +2012,11 @@ model =
|
|||||||
.cv-hull = CV Ház
|
.cv-hull = CV Ház
|
||||||
.class-entity =Osztály / Entitás
|
.class-entity =Osztály / Entitás
|
||||||
.curved = Ívelt
|
.curved = Ívelt
|
||||||
|
.cylindrical = Hengeres
|
||||||
|
.inverted-curved = Fordított ívelt
|
||||||
|
.inverted-cylindrical = Fordított hengeres
|
||||||
|
.inverted-hemispherical = Fordított félgömb alakú
|
||||||
|
.inverted-spherical = Fordított gömb alakú
|
||||||
.data-size = Adatméret
|
.data-size = Adatméret
|
||||||
.database-curves = Adatbázis görbék
|
.database-curves = Adatbázis görbék
|
||||||
.design = Tervezés
|
.design = Tervezés
|
||||||
|
|||||||
@@ -2012,6 +2012,11 @@ model =
|
|||||||
.cv-hull = CV Scafo
|
.cv-hull = CV Scafo
|
||||||
.class-entity =Classe/Entità
|
.class-entity =Classe/Entità
|
||||||
.curved = Curvo
|
.curved = Curvo
|
||||||
|
.cylindrical = Cilindrico
|
||||||
|
.inverted-curved = Curvo invertito
|
||||||
|
.inverted-cylindrical = Cilindrico invertito
|
||||||
|
.inverted-hemispherical = Emisferico invertito
|
||||||
|
.inverted-spherical = Sferico invertito
|
||||||
.data-size = Dimensione dei dati
|
.data-size = Dimensione dei dati
|
||||||
.database-curves = Curve di database
|
.database-curves = Curve di database
|
||||||
.design = Progettazione
|
.design = Progettazione
|
||||||
|
|||||||
@@ -2001,6 +2001,11 @@ model =
|
|||||||
.cv-hull = 制御点ハル
|
.cv-hull = 制御点ハル
|
||||||
.class-entity = クラス / 図形
|
.class-entity = クラス / 図形
|
||||||
.curved = 曲線
|
.curved = 曲線
|
||||||
|
.cylindrical = 円筒状
|
||||||
|
.inverted-curved = 反転曲線
|
||||||
|
.inverted-cylindrical = 反転円筒状
|
||||||
|
.inverted-hemispherical = 反転半球状
|
||||||
|
.inverted-spherical = 反転球状
|
||||||
.data-size = データサイズ
|
.data-size = データサイズ
|
||||||
.database-curves = データベース曲線
|
.database-curves = データベース曲線
|
||||||
.design = 設計
|
.design = 設計
|
||||||
|
|||||||
@@ -2012,6 +2012,11 @@ model =
|
|||||||
.cv-hull = CV 선체
|
.cv-hull = CV 선체
|
||||||
.class-entity =클래스/엔티티
|
.class-entity =클래스/엔티티
|
||||||
.curved = 곡선
|
.curved = 곡선
|
||||||
|
.cylindrical = 원통형
|
||||||
|
.inverted-curved = 반전 곡선형
|
||||||
|
.inverted-cylindrical = 반전 원통형
|
||||||
|
.inverted-hemispherical = 반전 반구형
|
||||||
|
.inverted-spherical = 반전 구형
|
||||||
.data-size = 데이터 크기
|
.data-size = 데이터 크기
|
||||||
.database-curves = 데이터베이스 곡선
|
.database-curves = 데이터베이스 곡선
|
||||||
.design = 디자인
|
.design = 디자인
|
||||||
|
|||||||
@@ -2013,6 +2013,11 @@ model =
|
|||||||
.cv-hull = CV Romp
|
.cv-hull = CV Romp
|
||||||
.class-entity = Klasse/object
|
.class-entity = Klasse/object
|
||||||
.curved = gebogen
|
.curved = gebogen
|
||||||
|
.cylindrical = Cilindrisch
|
||||||
|
.inverted-curved = Omgekeerd gebogen
|
||||||
|
.inverted-cylindrical = Omgekeerd cilindrisch
|
||||||
|
.inverted-hemispherical = Omgekeerd hemisferisch
|
||||||
|
.inverted-spherical = Omgekeerd sferisch
|
||||||
.data-size = Gegevensgrootte
|
.data-size = Gegevensgrootte
|
||||||
.database-curves = Databasecurves
|
.database-curves = Databasecurves
|
||||||
.design = Ontwerp
|
.design = Ontwerp
|
||||||
|
|||||||
@@ -2012,6 +2012,11 @@ model =
|
|||||||
.cv-hull = Kadłub CV
|
.cv-hull = Kadłub CV
|
||||||
.class-entity =Klasa/jednostka
|
.class-entity =Klasa/jednostka
|
||||||
.curved = Zakrzywiony
|
.curved = Zakrzywiony
|
||||||
|
.cylindrical = Cylindryczny
|
||||||
|
.inverted-curved = Odwrócony zakrzywiony
|
||||||
|
.inverted-cylindrical = Odwrócony cylindryczny
|
||||||
|
.inverted-hemispherical = Odwrócony półkulisty
|
||||||
|
.inverted-spherical = Odwrócony kulisty
|
||||||
.data-size = Rozmiar danych
|
.data-size = Rozmiar danych
|
||||||
.database-curves = Krzywe bazy danych
|
.database-curves = Krzywe bazy danych
|
||||||
.design = Projekt
|
.design = Projekt
|
||||||
|
|||||||
@@ -2013,6 +2013,11 @@ model =
|
|||||||
.cv-hull = Casco de VC
|
.cv-hull = Casco de VC
|
||||||
.class-entity = Classe / Entidade
|
.class-entity = Classe / Entidade
|
||||||
.curved = Curvo
|
.curved = Curvo
|
||||||
|
.cylindrical = Cilíndrico
|
||||||
|
.inverted-curved = Curvo invertido
|
||||||
|
.inverted-cylindrical = Cilíndrico invertido
|
||||||
|
.inverted-hemispherical = Hemisférico invertido
|
||||||
|
.inverted-spherical = Esférico invertido
|
||||||
.data-size = Tamanho dos dados
|
.data-size = Tamanho dos dados
|
||||||
.database-curves = Curvas do banco de dados
|
.database-curves = Curvas do banco de dados
|
||||||
.design = Projeto
|
.design = Projeto
|
||||||
|
|||||||
@@ -2016,6 +2016,11 @@ model =
|
|||||||
.cv-hull = CV Корпус
|
.cv-hull = CV Корпус
|
||||||
.class-entity = Класс / Объект
|
.class-entity = Класс / Объект
|
||||||
.curved = искривленный
|
.curved = искривленный
|
||||||
|
.cylindrical = цилиндрический
|
||||||
|
.inverted-curved = инвертированный искривленный
|
||||||
|
.inverted-cylindrical = инвертированный цилиндрический
|
||||||
|
.inverted-hemispherical = инвертированный полусферический
|
||||||
|
.inverted-spherical = инвертированный сферический
|
||||||
.data-size = Размер данных
|
.data-size = Размер данных
|
||||||
.database-curves = Кривые базы данных
|
.database-curves = Кривые базы данных
|
||||||
.design = Дизайн
|
.design = Дизайн
|
||||||
|
|||||||
@@ -1974,6 +1974,11 @@ model =
|
|||||||
.cv-hull = Kontrol Noktası Kabuğu
|
.cv-hull = Kontrol Noktası Kabuğu
|
||||||
.class-entity = Sınıf / Nesne
|
.class-entity = Sınıf / Nesne
|
||||||
.curved = Eğri
|
.curved = Eğri
|
||||||
|
.cylindrical = Silindirik
|
||||||
|
.inverted-curved = Ters eğri
|
||||||
|
.inverted-cylindrical = Ters silindirik
|
||||||
|
.inverted-hemispherical = Ters yarım küresel
|
||||||
|
.inverted-spherical = Ters küresel
|
||||||
.data-size = Veri Boyutu
|
.data-size = Veri Boyutu
|
||||||
.database-curves = Veritabanı Eğrileri
|
.database-curves = Veritabanı Eğrileri
|
||||||
.design = Tasarım
|
.design = Tasarım
|
||||||
|
|||||||
@@ -1996,6 +1996,11 @@ model =
|
|||||||
.cv-hull = CV 外壳
|
.cv-hull = CV 外壳
|
||||||
.class-entity = 类/实体
|
.class-entity = 类/实体
|
||||||
.curved = 曲线
|
.curved = 曲线
|
||||||
|
.cylindrical = 圆柱形
|
||||||
|
.inverted-curved = 反向曲线形
|
||||||
|
.inverted-cylindrical = 反向圆柱形
|
||||||
|
.inverted-hemispherical = 反向半球形
|
||||||
|
.inverted-spherical = 反向球形
|
||||||
.data-size = 数据大小
|
.data-size = 数据大小
|
||||||
.database-curves = 数据库曲线
|
.database-curves = 数据库曲线
|
||||||
.design = 设计
|
.design = 设计
|
||||||
|
|||||||
@@ -2008,6 +2008,11 @@ model =
|
|||||||
.cv-hull = CV 外殼
|
.cv-hull = CV 外殼
|
||||||
.class-entity = 類/實體
|
.class-entity = 類/實體
|
||||||
.curved = 曲線
|
.curved = 曲線
|
||||||
|
.cylindrical = 圓柱形
|
||||||
|
.inverted-curved = 反向曲線形
|
||||||
|
.inverted-cylindrical = 反向圓柱形
|
||||||
|
.inverted-hemispherical = 反向半球形
|
||||||
|
.inverted-spherical = 反向球形
|
||||||
.data-size = 資料大小
|
.data-size = 資料大小
|
||||||
.database-curves = 資料庫曲線
|
.database-curves = 資料庫曲線
|
||||||
.design = 設計
|
.design = 設計
|
||||||
|
|||||||
@@ -676,8 +676,9 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
"GRADIENT" => {
|
"GRADIENT" => {
|
||||||
use crate::modules::draw::draw::hatch::GradientCommand;
|
use crate::modules::draw::draw::hatch::GradientCommand;
|
||||||
let outlines = self.tabs[i].scene.closed_outlines();
|
let outlines = self.tabs[i].scene.hatch_boundary_outlines();
|
||||||
let new_cmd = GradientCommand::new(outlines);
|
let boundary_sources = self.tabs[i].scene.hatch_boundary_sources();
|
||||||
|
let new_cmd = GradientCommand::new(outlines, boundary_sources);
|
||||||
self.command_line.push_info(&new_cmd.prompt());
|
self.command_line.push_info(&new_cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1224,7 +1224,8 @@ impl OpenCADStudio {
|
|||||||
acadrust::EntityType::Text(_)
|
acadrust::EntityType::Text(_)
|
||||||
| acadrust::EntityType::MText(_)
|
| acadrust::EntityType::MText(_)
|
||||||
| acadrust::EntityType::Insert(_)
|
| acadrust::EntityType::Insert(_)
|
||||||
| acadrust::EntityType::Leader(_) => Some(("annotative", None)),
|
| acadrust::EntityType::Leader(_)
|
||||||
|
| acadrust::EntityType::Hatch(_) => Some(("annotative", None)),
|
||||||
acadrust::EntityType::MultiLeader(_) => {
|
acadrust::EntityType::MultiLeader(_) => {
|
||||||
Some(("enable_annotation_scale", None))
|
Some(("enable_annotation_scale", None))
|
||||||
}
|
}
|
||||||
@@ -1267,7 +1268,8 @@ impl OpenCADStudio {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
acadrust::EntityType::Text(_)
|
acadrust::EntityType::Text(_)
|
||||||
| acadrust::EntityType::Insert(_) => set_row_value(
|
| acadrust::EntityType::Insert(_)
|
||||||
|
| acadrust::EntityType::Hatch(_) => set_row_value(
|
||||||
&mut sections,
|
&mut sections,
|
||||||
"annotative",
|
"annotative",
|
||||||
crate::scene::model::object::PropValue::BoolToggle {
|
crate::scene::model::object::PropValue::BoolToggle {
|
||||||
|
|||||||
+76
-25
@@ -1749,33 +1749,84 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
let handles = self.property_target_handles(i);
|
let handles = self.property_target_handles(i);
|
||||||
|
|
||||||
if !handles.is_empty() {
|
if !handles.is_empty() {
|
||||||
if matches!(field, "spline_method" | "knot_param" | "cv_frame") {
|
if matches!(
|
||||||
|
field,
|
||||||
|
"spline_method"
|
||||||
|
| "knot_param"
|
||||||
|
| "cv_frame"
|
||||||
|
| "fill_type"
|
||||||
|
| "gradient_type"
|
||||||
|
| "style"
|
||||||
|
| "pattern_type_label"
|
||||||
|
) {
|
||||||
let unchanged = handles.iter().all(|handle| {
|
let unchanged = handles.iter().all(|handle| {
|
||||||
let Some(acadrust::EntityType::Spline(spline)) =
|
match self.tabs[i].scene.document.get_entity(*handle) {
|
||||||
self.tabs[i].scene.document.get_entity(*handle)
|
Some(acadrust::EntityType::Spline(spline)) => match field {
|
||||||
else {
|
"spline_method" => {
|
||||||
return false;
|
value
|
||||||
};
|
== if crate::entities::spline::shows_fit_points(spline) {
|
||||||
match field {
|
"Fit"
|
||||||
"spline_method" => {
|
} else {
|
||||||
value == if crate::entities::spline::shows_fit_points(spline) {
|
"Control Vertices"
|
||||||
"Fit"
|
}
|
||||||
} else {
|
|
||||||
"Control Vertices"
|
|
||||||
}
|
}
|
||||||
}
|
"knot_param" => {
|
||||||
"knot_param" => {
|
let current = match spline.knot_parameterization {
|
||||||
let current = match spline.knot_parameterization {
|
0 => "Chord",
|
||||||
0 => "Chord",
|
1 => "Square Root",
|
||||||
1 => "Square Root",
|
2 => "Uniform",
|
||||||
2 => "Uniform",
|
_ => "Custom",
|
||||||
_ => "Custom",
|
};
|
||||||
};
|
value == current
|
||||||
value == current
|
}
|
||||||
}
|
"cv_frame" => {
|
||||||
"cv_frame" => {
|
value
|
||||||
value == if spline.cv_frame_visible { "Show" } else { "Hide" }
|
== if spline.cv_frame_visible {
|
||||||
}
|
"Show"
|
||||||
|
} else {
|
||||||
|
"Hide"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
Some(acadrust::EntityType::Hatch(hatch)) => match field {
|
||||||
|
"fill_type" => {
|
||||||
|
value
|
||||||
|
== if hatch.gradient_color.is_single_color {
|
||||||
|
"One color"
|
||||||
|
} else {
|
||||||
|
"Two color"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"gradient_type" => {
|
||||||
|
let (kind, inverted) =
|
||||||
|
crate::scene::model::hatch_model::GradientKind::from_name(
|
||||||
|
&hatch.gradient_color.name,
|
||||||
|
);
|
||||||
|
value == kind.choice_label(inverted)
|
||||||
|
}
|
||||||
|
"style" => {
|
||||||
|
value
|
||||||
|
== match hatch.style {
|
||||||
|
acadrust::entities::HatchStyleType::Normal => "Normal",
|
||||||
|
acadrust::entities::HatchStyleType::Outer => "Outer",
|
||||||
|
acadrust::entities::HatchStyleType::Ignore => "Ignore",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"pattern_type_label" => {
|
||||||
|
value
|
||||||
|
== match hatch.pattern_type {
|
||||||
|
acadrust::entities::HatchPatternType::Predefined => {
|
||||||
|
"Predefined"
|
||||||
|
}
|
||||||
|
acadrust::entities::HatchPatternType::UserDefined => {
|
||||||
|
"User Defined"
|
||||||
|
}
|
||||||
|
acadrust::entities::HatchPatternType::Custom => "Custom",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+119
-96
@@ -323,6 +323,33 @@ pub fn clear_background_color(h: &mut Hatch) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn associative_property(h: &Hatch) -> Property {
|
||||||
|
if h
|
||||||
|
.paths
|
||||||
|
.iter()
|
||||||
|
.any(|path| !path.boundary_handles.is_empty())
|
||||||
|
{
|
||||||
|
Property {
|
||||||
|
label: t!("Associative").into_owned(),
|
||||||
|
field: "associative",
|
||||||
|
value: PropValue::BoolToggle {
|
||||||
|
field: "associative",
|
||||||
|
value: h.is_associative,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ro(
|
||||||
|
t!("Associative").as_ref(),
|
||||||
|
"associative",
|
||||||
|
if h.is_associative {
|
||||||
|
t!("Yes").into_owned()
|
||||||
|
} else {
|
||||||
|
t!("No").into_owned()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn properties(h: &Hatch) -> Vec<PropSection> {
|
fn properties(h: &Hatch) -> Vec<PropSection> {
|
||||||
let pattern_type = match h.pattern_type {
|
let pattern_type = match h.pattern_type {
|
||||||
acadrust::entities::HatchPatternType::Predefined => "Predefined",
|
acadrust::entities::HatchPatternType::Predefined => "Predefined",
|
||||||
@@ -345,59 +372,66 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
|
|||||||
if g.enabled {
|
if g.enabled {
|
||||||
// ── Gradient fill ──────────────────────────────────────────────────
|
// ── Gradient fill ──────────────────────────────────────────────────
|
||||||
let grad_type = if g.is_single_color { "One color" } else { "Two color" };
|
let grad_type = if g.is_single_color { "One color" } else { "Two color" };
|
||||||
let centered = if g.shift.abs() < 1e-9 { "Yes" } else { "No" };
|
let (kind, inverted) =
|
||||||
let mut sections = vec![
|
crate::scene::model::hatch_model::GradientKind::from_name(&g.name);
|
||||||
|
let mut pattern_props = vec![
|
||||||
|
ro(t!("Type").as_ref(), "fill_kind", t!("Gradient").into_owned()),
|
||||||
|
Property {
|
||||||
|
label: t!("Color mode").into_owned(),
|
||||||
|
field: "fill_type",
|
||||||
|
value: PropValue::Choice {
|
||||||
|
selected: grad_type.to_string(),
|
||||||
|
options: vec!["One color".into(), "Two color".into()],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
label: t!("Gradient type").into_owned(),
|
||||||
|
field: "gradient_type",
|
||||||
|
value: PropValue::Choice {
|
||||||
|
selected: kind.choice_label(inverted).to_string(),
|
||||||
|
options: crate::scene::model::hatch_model::GradientKind::CHOICES
|
||||||
|
.iter()
|
||||||
|
.map(|(kind, inverted)| kind.choice_label(*inverted).to_string())
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
label: t!("Color 1").into_owned(),
|
||||||
|
field: "gradient_color_1",
|
||||||
|
value: PropValue::ColorChoice(grad_c1),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if g.is_single_color {
|
||||||
|
pattern_props.push(edit(
|
||||||
|
t!("Tint/Shade").as_ref(),
|
||||||
|
"gradient_tint",
|
||||||
|
g.color_tint.clamp(0.0, 1.0),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
pattern_props.push(Property {
|
||||||
|
label: t!("Color 2").into_owned(),
|
||||||
|
field: "gradient_color_2",
|
||||||
|
value: PropValue::ColorChoice(grad_c2),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
pattern_props.push(edit_angle(
|
||||||
|
t!("Angle").as_ref(),
|
||||||
|
"pattern_angle",
|
||||||
|
g.angle.to_degrees(),
|
||||||
|
));
|
||||||
|
pattern_props.push(Property {
|
||||||
|
label: t!("Centered").into_owned(),
|
||||||
|
field: "gradient_centered",
|
||||||
|
value: PropValue::BoolToggle {
|
||||||
|
field: "gradient_centered",
|
||||||
|
value: g.shift < 0.5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return vec![
|
||||||
PropSection {
|
PropSection {
|
||||||
title: t!("Pattern").into_owned(),
|
title: t!("Pattern").into_owned(),
|
||||||
props: vec![
|
props: pattern_props,
|
||||||
Property {
|
|
||||||
label: t!("Type").into_owned(),
|
|
||||||
field: "fill_type",
|
|
||||||
value: PropValue::Choice {
|
|
||||||
selected: grad_type.to_string(),
|
|
||||||
options: vec!["Two color".into(), "One color".into()],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
let (kind, invert) =
|
|
||||||
crate::scene::model::hatch_model::GradientKind::from_name(&g.name);
|
|
||||||
let _ = invert;
|
|
||||||
Property {
|
|
||||||
label: t!("Gradient type").into_owned(),
|
|
||||||
field: "gradient_type",
|
|
||||||
value: PropValue::Choice {
|
|
||||||
selected: kind.label().to_string(),
|
|
||||||
options: crate::scene::model::hatch_model::GradientKind::ALL
|
|
||||||
.iter()
|
|
||||||
.map(|k| k.label().to_string())
|
|
||||||
.collect(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Property {
|
|
||||||
label: t!("Invert").into_owned(),
|
|
||||||
field: "gradient_invert",
|
|
||||||
value: PropValue::BoolToggle {
|
|
||||||
field: "gradient_invert",
|
|
||||||
value: crate::scene::model::hatch_model::GradientKind::from_name(
|
|
||||||
&g.name,
|
|
||||||
)
|
|
||||||
.1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Property {
|
|
||||||
label: t!("Color 1").into_owned(),
|
|
||||||
field: "gradient_color_1",
|
|
||||||
value: PropValue::ColorChoice(grad_c1),
|
|
||||||
},
|
|
||||||
Property {
|
|
||||||
label: t!("Color 2").into_owned(),
|
|
||||||
field: "gradient_color_2",
|
|
||||||
value: PropValue::ColorChoice(grad_c2),
|
|
||||||
},
|
|
||||||
edit_angle(t!("Angle").as_ref(), "pattern_angle", g.angle.to_degrees()),
|
|
||||||
ro(t!("Centered").as_ref(), "gradient_centered", centered),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
PropSection {
|
PropSection {
|
||||||
title: t!("Geometry").into_owned(),
|
title: t!("Geometry").into_owned(),
|
||||||
@@ -410,33 +444,19 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
|
|||||||
PropSection {
|
PropSection {
|
||||||
title: t!("Misc").into_owned(),
|
title: t!("Misc").into_owned(),
|
||||||
props: vec![
|
props: vec![
|
||||||
ro(t!("Associative").as_ref(),
|
associative_property(h),
|
||||||
"associative",
|
|
||||||
if h.is_associative { "Yes" } else { "No" },
|
|
||||||
),
|
|
||||||
ro(t!("Annotative").as_ref(), "annotative", String::new()),
|
ro(t!("Annotative").as_ref(), "annotative", String::new()),
|
||||||
ro(t!("Island detection style").as_ref(), "style", style),
|
|
||||||
Property {
|
Property {
|
||||||
label: t!("Background").into_owned(),
|
label: t!("Island detection style").into_owned(),
|
||||||
field: "bg_enabled",
|
field: "style",
|
||||||
value: PropValue::BoolToggle {
|
value: PropValue::Choice {
|
||||||
field: "bg_enabled",
|
selected: style.to_string(),
|
||||||
value: bg_on,
|
options: vec!["Normal".into(), "Outer".into(), "Ignore".into()],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
if bg_on {
|
|
||||||
if let Some(sec) = sections.last_mut() {
|
|
||||||
sec.props.push(Property {
|
|
||||||
label: t!("Background color").into_owned(),
|
|
||||||
field: "background_color",
|
|
||||||
value: PropValue::ColorChoice(bg_col),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sections;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -455,14 +475,7 @@ fn properties(h: &Hatch) -> Vec<PropSection> {
|
|||||||
field: "pattern_name",
|
field: "pattern_name",
|
||||||
value: PropValue::HatchPatternChoice(h.pattern.name.clone()),
|
value: PropValue::HatchPatternChoice(h.pattern.name.clone()),
|
||||||
};
|
};
|
||||||
let associative_row = Property {
|
let associative_row = associative_property(h);
|
||||||
label: t!("Associative").into_owned(),
|
|
||||||
field: "associative",
|
|
||||||
value: PropValue::BoolToggle {
|
|
||||||
field: "associative",
|
|
||||||
value: h.is_associative,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let double_row = Property {
|
let double_row = Property {
|
||||||
label: t!("Double").into_owned(),
|
label: t!("Double").into_owned(),
|
||||||
field: "double",
|
field: "double",
|
||||||
@@ -567,11 +580,22 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
"associative" => {
|
"associative" => {
|
||||||
h.is_associative = if value == "toggle" {
|
let requested = if value == "toggle" {
|
||||||
!h.is_associative
|
!h.is_associative
|
||||||
} else {
|
} else {
|
||||||
value == "true"
|
value == "true"
|
||||||
};
|
};
|
||||||
|
// An associative flag without source handles is inert and cannot
|
||||||
|
// update from boundary edits. Only enable it when a real
|
||||||
|
// relationship is available; disabling retains the handles so the
|
||||||
|
// user can turn it back on later.
|
||||||
|
if !requested
|
||||||
|
|| h.paths
|
||||||
|
.iter()
|
||||||
|
.any(|path| !path.boundary_handles.is_empty())
|
||||||
|
{
|
||||||
|
h.is_associative = requested;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
"pattern_type_label" => {
|
"pattern_type_label" => {
|
||||||
@@ -593,31 +617,27 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
"fill_type" => {
|
"fill_type" => {
|
||||||
h.gradient_color.is_single_color = value == "One color";
|
let one_color = value == "One color";
|
||||||
|
if one_color && !h.gradient_color.is_single_color {
|
||||||
|
h.gradient_color.color_tint = 1.0;
|
||||||
|
}
|
||||||
|
h.gradient_color.is_single_color = one_color;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Gradient shape selection + stop inversion (#415). Both re-derive the
|
|
||||||
// standard DXF gradient name; Linear has no INV name, so inverting a
|
|
||||||
// linear swaps the colour stops instead.
|
|
||||||
"gradient_type" => {
|
"gradient_type" => {
|
||||||
use crate::scene::model::hatch_model::GradientKind;
|
use crate::scene::model::hatch_model::GradientKind;
|
||||||
let (_, invert) = GradientKind::from_name(&h.gradient_color.name);
|
if let Some((kind, invert)) = GradientKind::from_choice_label(value) {
|
||||||
if let Some(kind) = GradientKind::from_label(value) {
|
|
||||||
h.gradient_color.name = kind.dxf_name(invert).to_string();
|
h.gradient_color.name = kind.dxf_name(invert).to_string();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
"gradient_invert" => {
|
"gradient_centered" => {
|
||||||
use crate::scene::model::hatch_model::GradientKind;
|
let centered = if value == "toggle" {
|
||||||
let (kind, invert) = GradientKind::from_name(&h.gradient_color.name);
|
h.gradient_color.shift >= 0.5
|
||||||
let invert = !invert;
|
|
||||||
if matches!(kind, GradientKind::Linear) {
|
|
||||||
if h.gradient_color.colors.len() >= 2 {
|
|
||||||
h.gradient_color.colors.swap(0, 1);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
h.gradient_color.name = kind.dxf_name(invert).to_string();
|
value == "true"
|
||||||
}
|
};
|
||||||
|
h.gradient_color.shift = if centered { 0.0 } else { 1.0 };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -630,6 +650,9 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) {
|
|||||||
h.gradient_color.angle = v.to_radians();
|
h.gradient_color.angle = v.to_radians();
|
||||||
h.pattern_angle = v.to_radians();
|
h.pattern_angle = v.to_radians();
|
||||||
}
|
}
|
||||||
|
"gradient_tint" if h.gradient_color.enabled => {
|
||||||
|
h.gradient_color.color_tint = v.clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
"pattern_angle" => {
|
"pattern_angle" => {
|
||||||
let angle = v.to_radians();
|
let angle = v.to_radians();
|
||||||
let delta = angle - h.pattern_angle;
|
let delta = angle - h.pattern_angle;
|
||||||
|
|||||||
@@ -701,6 +701,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||||||
"Cutting edges" => Some(("modify", "cutting-edges")),
|
"Cutting edges" => Some(("modify", "cutting-edges")),
|
||||||
"Cyan" => Some(("common", "cyan")),
|
"Cyan" => Some(("common", "cyan")),
|
||||||
"Cylinder" => Some(("common", "cylinder")),
|
"Cylinder" => Some(("common", "cylinder")),
|
||||||
|
"Cylindrical" => Some(("model", "cylindrical")),
|
||||||
"DATAEXTRACTION {rows} rows → \"{fname}\"" => Some(("common", "dataextraction-rows-rows-fname")),
|
"DATAEXTRACTION {rows} rows → \"{fname}\"" => Some(("common", "dataextraction-rows-rows-fname")),
|
||||||
"DATAEXTRACTION: write failed: {e}" => Some(("common", "dataextraction-write-failed-e")),
|
"DATAEXTRACTION: write failed: {e}" => Some(("common", "dataextraction-write-failed-e")),
|
||||||
"DATALINK path to the .csv file:" => Some(("view", "datalink-path-to-the-csv-file")),
|
"DATALINK path to the .csv file:" => Some(("view", "datalink-path-to-the-csv-file")),
|
||||||
@@ -1234,6 +1235,10 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||||||
"Inverse Linear" => Some(("properties", "inverse-linear")),
|
"Inverse Linear" => Some(("properties", "inverse-linear")),
|
||||||
"Inverse Square" => Some(("properties", "inverse-square")),
|
"Inverse Square" => Some(("properties", "inverse-square")),
|
||||||
"Invert" => Some(("properties", "invert")),
|
"Invert" => Some(("properties", "invert")),
|
||||||
|
"Inverted curved" => Some(("model", "inverted-curved")),
|
||||||
|
"Inverted cylindrical" => Some(("model", "inverted-cylindrical")),
|
||||||
|
"Inverted hemispherical" => Some(("model", "inverted-hemispherical")),
|
||||||
|
"Inverted spherical" => Some(("model", "inverted-spherical")),
|
||||||
"Invert Selection" => Some(("ui", "invert-selection")),
|
"Invert Selection" => Some(("ui", "invert-selection")),
|
||||||
"Invert Selection: {} object(s) selected." => Some(("common", "invert-selection-object-s-selected")),
|
"Invert Selection: {} object(s) selected." => Some(("common", "invert-selection-object-s-selected")),
|
||||||
"Invert: off" => Some(("draw", "invert-off")),
|
"Invert: off" => Some(("draw", "invert-off")),
|
||||||
|
|||||||
@@ -306,7 +306,7 @@ impl HatchCommand {
|
|||||||
let (rel, origin, wcs) = pack_rings(&rings);
|
let (rel, origin, wcs) = pack_rings(&rings);
|
||||||
let exterior = cadkernel::geom2d::ring_nesting_depths(&rings)
|
let exterior = cadkernel::geom2d::ring_nesting_depths(&rings)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|depth| depth % 2 == 0)
|
.map(|depth| depth == 0)
|
||||||
.collect();
|
.collect();
|
||||||
let boundary_sources = rings
|
let boundary_sources = rings
|
||||||
.iter()
|
.iter()
|
||||||
@@ -645,6 +645,7 @@ impl CadCommand for HatchCommand {
|
|||||||
|
|
||||||
pub struct GradientCommand {
|
pub struct GradientCommand {
|
||||||
outlines: Vec<Vec<[f64; 2]>>,
|
outlines: Vec<Vec<[f64; 2]>>,
|
||||||
|
boundary_sources: rustc_hash::FxHashMap<Handle, Vec<Line>>,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
manual_pts: Vec<DVec3>,
|
manual_pts: Vec<DVec3>,
|
||||||
missed: bool,
|
missed: bool,
|
||||||
@@ -655,9 +656,13 @@ pub struct GradientCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl GradientCommand {
|
impl GradientCommand {
|
||||||
pub fn new(outlines: Vec<Vec<[f64; 2]>>) -> Self {
|
pub fn new(
|
||||||
|
outlines: Vec<Vec<[f64; 2]>>,
|
||||||
|
boundary_sources: rustc_hash::FxHashMap<Handle, Vec<Line>>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
outlines,
|
outlines,
|
||||||
|
boundary_sources,
|
||||||
mode: Mode::PickInside,
|
mode: Mode::PickInside,
|
||||||
manual_pts: vec![],
|
manual_pts: vec![],
|
||||||
missed: false,
|
missed: false,
|
||||||
@@ -668,6 +673,14 @@ impl GradientCommand {
|
|||||||
|
|
||||||
fn make_hatch(&self, rings: Vec<Vec<[f64; 2]>>) -> HatchModel {
|
fn make_hatch(&self, rings: Vec<Vec<[f64; 2]>>) -> HatchModel {
|
||||||
let (rel, origin, wcs) = pack_rings(&rings);
|
let (rel, origin, wcs) = pack_rings(&rings);
|
||||||
|
let exterior = cadkernel::geom2d::ring_nesting_depths(&rings)
|
||||||
|
.into_iter()
|
||||||
|
.map(|depth| depth == 0)
|
||||||
|
.collect();
|
||||||
|
let boundary_sources = rings
|
||||||
|
.iter()
|
||||||
|
.map(|ring| crate::scene::ring_source_handles(ring, &self.boundary_sources))
|
||||||
|
.collect();
|
||||||
HatchModel {
|
HatchModel {
|
||||||
render_instance: None,
|
render_instance: None,
|
||||||
boundary: std::sync::Arc::new(rel),
|
boundary: std::sync::Arc::new(rel),
|
||||||
@@ -676,6 +689,7 @@ impl GradientCommand {
|
|||||||
color2: [0.18, 0.18, 0.18, 0.0],
|
color2: [0.18, 0.18, 0.18, 0.0],
|
||||||
kind: self.kind,
|
kind: self.kind,
|
||||||
invert: self.invert,
|
invert: self.invert,
|
||||||
|
shift: 0.0,
|
||||||
},
|
},
|
||||||
name: self.kind.dxf_name(self.invert).into(),
|
name: self.kind.dxf_name(self.invert).into(),
|
||||||
color: [0.30, 0.60, 0.95, 0.80],
|
color: [0.30, 0.60, 0.95, 0.80],
|
||||||
@@ -685,8 +699,8 @@ impl GradientCommand {
|
|||||||
scale: 1.0,
|
scale: 1.0,
|
||||||
world_origin: origin,
|
world_origin: origin,
|
||||||
boundary_wcs: Some(std::sync::Arc::new(wcs)),
|
boundary_wcs: Some(std::sync::Arc::new(wcs)),
|
||||||
boundary_exterior: None,
|
boundary_exterior: Some(std::sync::Arc::new(exterior)),
|
||||||
boundary_sources: None,
|
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
|
||||||
draw_depth: 0.0,
|
draw_depth: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -705,15 +719,10 @@ impl CadCommand for GradientCommand {
|
|||||||
} else {
|
} else {
|
||||||
std::borrow::Cow::Borrowed("")
|
std::borrow::Cow::Borrowed("")
|
||||||
};
|
};
|
||||||
let invert = if self.invert {
|
|
||||||
t!(", inverted")
|
|
||||||
} else {
|
|
||||||
std::borrow::Cow::Borrowed("")
|
|
||||||
};
|
|
||||||
t!(
|
t!(
|
||||||
"GRADIENT (%{kind}%{invert}) Pick internal point:%{miss}",
|
"GRADIENT (%{kind}%{invert}) Pick internal point:%{miss}",
|
||||||
kind = t!(self.kind.label()),
|
kind = t!(self.kind.choice_label(self.invert)),
|
||||||
invert = invert,
|
invert = std::borrow::Cow::Borrowed(""),
|
||||||
miss = miss
|
miss = miss
|
||||||
)
|
)
|
||||||
.into_owned()
|
.into_owned()
|
||||||
@@ -733,15 +742,14 @@ impl CadCommand for GradientCommand {
|
|||||||
match &self.mode {
|
match &self.mode {
|
||||||
Mode::PickInside => {
|
Mode::PickInside => {
|
||||||
let mut opts = vec![CmdOption::new("Draw manually", "S")];
|
let mut opts = vec![CmdOption::new("Draw manually", "S")];
|
||||||
for k in crate::scene::model::hatch_model::GradientKind::ALL {
|
for (kind, inverted) in
|
||||||
if k != self.kind {
|
crate::scene::model::hatch_model::GradientKind::CHOICES
|
||||||
opts.push(CmdOption::new(k.label(), k.label()));
|
{
|
||||||
|
if kind != self.kind || inverted != self.invert {
|
||||||
|
let label = kind.choice_label(inverted);
|
||||||
|
opts.push(CmdOption::new(label, label));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
opts.push(CmdOption::new(
|
|
||||||
if self.invert { "Invert: on" } else { "Invert: off" },
|
|
||||||
"I",
|
|
||||||
));
|
|
||||||
opts
|
opts
|
||||||
}
|
}
|
||||||
Mode::Manual => {
|
Mode::Manual => {
|
||||||
@@ -806,17 +814,11 @@ impl CadCommand for GradientCommand {
|
|||||||
self.missed = false;
|
self.missed = false;
|
||||||
return Some(CmdResult::NeedPoint);
|
return Some(CmdResult::NeedPoint);
|
||||||
}
|
}
|
||||||
// Gradient type keywords / buttons + the invert toggle (#415).
|
if let Some((kind, inverted)) =
|
||||||
if t.eq_ignore_ascii_case("i") || t.eq_ignore_ascii_case("invert") {
|
crate::scene::model::hatch_model::GradientKind::from_choice_label(t)
|
||||||
self.invert = !self.invert;
|
|
||||||
return Some(CmdResult::NeedPoint);
|
|
||||||
}
|
|
||||||
if let Some(k) = crate::scene::model::hatch_model::GradientKind::ALL
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.find(|k| k.label().eq_ignore_ascii_case(t))
|
|
||||||
{
|
{
|
||||||
self.kind = k;
|
self.kind = kind;
|
||||||
|
self.invert = inverted;
|
||||||
return Some(CmdResult::NeedPoint);
|
return Some(CmdResult::NeedPoint);
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -545,8 +545,8 @@ impl Scene {
|
|||||||
|
|
||||||
/// Build closed planar regions from the visible wire geometry.
|
/// Build closed planar regions from the visible wire geometry.
|
||||||
///
|
///
|
||||||
/// Unlike `closed_outlines()`, the source entities do not need to be closed
|
/// Source entities do not need to be closed individually. Intersections are
|
||||||
/// individually. Intersections are inserted as temporary graph vertices and
|
/// inserted as temporary graph vertices and
|
||||||
/// the bounded faces of that planar graph are returned as hatch candidates.
|
/// the bounded faces of that planar graph are returned as hatch candidates.
|
||||||
///
|
///
|
||||||
/// Curved entities participate through their already-tessellated WireModel
|
/// Curved entities participate through their already-tessellated WireModel
|
||||||
|
|||||||
+98
-52
@@ -90,6 +90,36 @@ fn family_from_stored_line(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Preserve the selected hue while moving its HSL lightness towards the
|
||||||
|
/// persisted one-colour tint/shade target (0 = black, 1 = white).
|
||||||
|
fn gradient_tint_color(base: [f32; 4], target: f32) -> [f32; 4] {
|
||||||
|
let max = base[0].max(base[1]).max(base[2]);
|
||||||
|
let min = base[0].min(base[1]).min(base[2]);
|
||||||
|
let lightness = (max + min) * 0.5;
|
||||||
|
let target = target.clamp(0.0, 1.0);
|
||||||
|
let mut result = base;
|
||||||
|
if target <= lightness {
|
||||||
|
let factor = if lightness > 1.0e-6 {
|
||||||
|
target / lightness
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
for channel in &mut result[..3] {
|
||||||
|
*channel *= factor;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let factor = if lightness < 1.0 - 1.0e-6 {
|
||||||
|
(target - lightness) / (1.0 - lightness)
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
for channel in &mut result[..3] {
|
||||||
|
*channel += (1.0 - *channel) * factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
impl Scene {
|
impl Scene {
|
||||||
// ── Entity management ─────────────────────────────────────────────────
|
// ── Entity management ─────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -1041,8 +1071,13 @@ impl Scene {
|
|||||||
m.angle_offset = dxf.pattern_angle as f32;
|
m.angle_offset = dxf.pattern_angle as f32;
|
||||||
m.scale = dxf.pattern_scale as f32;
|
m.scale = dxf.pattern_scale as f32;
|
||||||
}
|
}
|
||||||
model::hatch_model::HatchPattern::Gradient { angle_deg, .. } => {
|
model::hatch_model::HatchPattern::Gradient {
|
||||||
*angle_deg = dxf.pattern_angle.to_degrees() as f32;
|
angle_deg,
|
||||||
|
shift,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
*angle_deg = dxf.gradient_color.angle.to_degrees() as f32;
|
||||||
|
*shift = dxf.gradient_color.shift as f32;
|
||||||
}
|
}
|
||||||
model::hatch_model::HatchPattern::Pattern(_)
|
model::hatch_model::HatchPattern::Pattern(_)
|
||||||
| model::hatch_model::HatchPattern::Solid => {}
|
| model::hatch_model::HatchPattern::Solid => {}
|
||||||
@@ -1520,8 +1555,8 @@ impl Scene {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut boundary: Vec<[f64; 2]> = Vec::new();
|
let mut rings = Vec::new();
|
||||||
let mut boundary_exterior = Vec::new();
|
let mut ring_sources = Vec::new();
|
||||||
|
|
||||||
for path in &dxf.paths {
|
for path in &dxf.paths {
|
||||||
// Skip TEXTBOX boundary paths (flag bit 3). These are text
|
// Skip TEXTBOX boundary paths (flag bit 3). These are text
|
||||||
@@ -1531,12 +1566,6 @@ impl Scene {
|
|||||||
if path.flags.bits() & 8 != 0 {
|
if path.flags.bits() & 8 != 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let before_path = boundary.len();
|
|
||||||
if !boundary.is_empty() {
|
|
||||||
boundary.push([f64::NAN, f64::NAN]);
|
|
||||||
}
|
|
||||||
let path_start = boundary.len();
|
|
||||||
|
|
||||||
let mut edge_polys: Vec<Vec<[f64; 2]>> = Vec::new();
|
let mut edge_polys: Vec<Vec<[f64; 2]>> = Vec::new();
|
||||||
for edge in &path.edges {
|
for edge in &path.edges {
|
||||||
if let Some(curve) = crate::entities::hatch::edge_curve(edge) {
|
if let Some(curve) = crate::entities::hatch::edge_curve(edge) {
|
||||||
@@ -1549,29 +1578,44 @@ impl Scene {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
boundary.extend(chain_path_edges(edge_polys));
|
let mut ring = chain_path_edges(edge_polys);
|
||||||
|
if ring.is_empty() {
|
||||||
if boundary.len() == path_start {
|
|
||||||
boundary.truncate(before_path);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
boundary_exterior.push(path.flags.is_external() || path.flags.is_outermost());
|
if ring.len() >= 3 {
|
||||||
if boundary.len() >= path_start + 3 {
|
let first = ring[0];
|
||||||
let first = boundary[path_start];
|
let last = *ring.last().unwrap();
|
||||||
let last = *boundary.last().unwrap();
|
|
||||||
if (first[0] - last[0]).abs() > 1e-5 || (first[1] - last[1]).abs() > 1e-5 {
|
if (first[0] - last[0]).abs() > 1e-5 || (first[1] - last[1]).abs() > 1e-5 {
|
||||||
boundary.push(first);
|
ring.push(first);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
rings.push(ring);
|
||||||
|
ring_sources.push(path.boundary_handles.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
if boundary.is_empty() {
|
if rings.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if !boundary_exterior.iter().any(|role| *role) {
|
|
||||||
if let Some(first) = boundary_exterior.first_mut() {
|
let depths = cadkernel::geom2d::ring_nesting_depths(&rings);
|
||||||
*first = true;
|
let mut boundary = Vec::new();
|
||||||
|
let mut boundary_exterior = Vec::new();
|
||||||
|
let mut boundary_sources = Vec::new();
|
||||||
|
for ((ring, sources), depth) in rings.into_iter().zip(ring_sources).zip(depths) {
|
||||||
|
let keep = match dxf.style {
|
||||||
|
acadrust::entities::HatchStyleType::Normal => true,
|
||||||
|
acadrust::entities::HatchStyleType::Outer => depth <= 1,
|
||||||
|
acadrust::entities::HatchStyleType::Ignore => depth == 0,
|
||||||
|
};
|
||||||
|
if !keep {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
if !boundary.is_empty() {
|
||||||
|
boundary.push([f64::NAN, f64::NAN]);
|
||||||
|
}
|
||||||
|
boundary.extend(ring);
|
||||||
|
boundary_exterior.push(depth == 0);
|
||||||
|
boundary_sources.push(sources);
|
||||||
}
|
}
|
||||||
// The batched hatch renderer keeps boundaries in a GPU storage
|
// The batched hatch renderer keeps boundaries in a GPU storage
|
||||||
// buffer (no fixed length), so a hatch with many island loops must
|
// buffer (no fixed length), so a hatch with many island loops must
|
||||||
@@ -1618,8 +1662,13 @@ impl Scene {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
gradient_color1 = stop(0);
|
gradient_color1 = stop(0);
|
||||||
let color2 = stop(1).unwrap_or(color);
|
let color1 = gradient_color1.unwrap_or(color);
|
||||||
let angle_deg = dxf.pattern_angle.to_degrees() as f32;
|
let color2 = if dxf.gradient_color.is_single_color {
|
||||||
|
gradient_tint_color(color1, dxf.gradient_color.color_tint as f32)
|
||||||
|
} else {
|
||||||
|
stop(1).unwrap_or(color)
|
||||||
|
};
|
||||||
|
let angle_deg = dxf.gradient_color.angle.to_degrees() as f32;
|
||||||
let (kind, invert) =
|
let (kind, invert) =
|
||||||
model::hatch_model::GradientKind::from_name(&dxf.gradient_color.name);
|
model::hatch_model::GradientKind::from_name(&dxf.gradient_color.name);
|
||||||
model::hatch_model::HatchPattern::Gradient {
|
model::hatch_model::HatchPattern::Gradient {
|
||||||
@@ -1627,6 +1676,7 @@ impl Scene {
|
|||||||
color2,
|
color2,
|
||||||
kind,
|
kind,
|
||||||
invert,
|
invert,
|
||||||
|
shift: dxf.gradient_color.shift as f32,
|
||||||
}
|
}
|
||||||
} else if dxf.is_solid {
|
} else if dxf.is_solid {
|
||||||
model::hatch_model::HatchPattern::Solid
|
model::hatch_model::HatchPattern::Solid
|
||||||
@@ -1768,13 +1818,7 @@ impl Scene {
|
|||||||
boundary: std::sync::Arc::new(boundary_f32),
|
boundary: std::sync::Arc::new(boundary_f32),
|
||||||
boundary_wcs: None,
|
boundary_wcs: None,
|
||||||
boundary_exterior: Some(std::sync::Arc::new(boundary_exterior)),
|
boundary_exterior: Some(std::sync::Arc::new(boundary_exterior)),
|
||||||
boundary_sources: Some(std::sync::Arc::new(
|
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
|
||||||
dxf.paths
|
|
||||||
.iter()
|
|
||||||
.filter(|path| path.flags.bits() & 8 == 0)
|
|
||||||
.map(|path| path.boundary_handles.clone())
|
|
||||||
.collect(),
|
|
||||||
)),
|
|
||||||
pattern,
|
pattern,
|
||||||
name,
|
name,
|
||||||
// A gradient starts from its first stop; other fills use the
|
// A gradient starts from its first stop; other fills use the
|
||||||
@@ -2144,27 +2188,28 @@ impl Scene {
|
|||||||
let mut push_ring = |r: &mut Vec<Vector2>, is_outer: bool, index: usize| {
|
let mut push_ring = |r: &mut Vec<Vector2>, is_outer: bool, index: usize| {
|
||||||
if !r.is_empty() {
|
if !r.is_empty() {
|
||||||
let edge = PolylineEdge::new(std::mem::take(r), true);
|
let edge = PolylineEdge::new(std::mem::take(r), true);
|
||||||
let mut path = if is_outer {
|
let handles: Vec<_> = model
|
||||||
let mut p = BoundaryPath::external();
|
|
||||||
p.flags = acadrust::entities::hatch::BoundaryPathFlags::from_bits(
|
|
||||||
p.flags.bits()
|
|
||||||
| acadrust::entities::hatch::BoundaryPathFlags::OUTERMOST.bits(),
|
|
||||||
);
|
|
||||||
p
|
|
||||||
} else {
|
|
||||||
BoundaryPath::new()
|
|
||||||
};
|
|
||||||
path.add_edge(BoundaryEdge::Polyline(edge));
|
|
||||||
if let Some(handles) = model
|
|
||||||
.boundary_sources
|
.boundary_sources
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(|sources| sources.get(index))
|
.and_then(|sources| sources.get(index))
|
||||||
{
|
.into_iter()
|
||||||
for &handle in handles {
|
.flatten()
|
||||||
if handle.is_valid() {
|
.copied()
|
||||||
path.add_boundary_handle(handle);
|
.filter(|handle| handle.is_valid())
|
||||||
}
|
.collect();
|
||||||
}
|
let mut bits = 0;
|
||||||
|
if is_outer {
|
||||||
|
bits |= acadrust::entities::hatch::BoundaryPathFlags::OUTERMOST.bits();
|
||||||
|
}
|
||||||
|
if !handles.is_empty() {
|
||||||
|
bits |= acadrust::entities::hatch::BoundaryPathFlags::EXTERNAL.bits();
|
||||||
|
}
|
||||||
|
let mut path = BoundaryPath::with_flags(
|
||||||
|
acadrust::entities::hatch::BoundaryPathFlags::from_bits(bits),
|
||||||
|
);
|
||||||
|
path.add_edge(BoundaryEdge::Polyline(edge));
|
||||||
|
for handle in handles {
|
||||||
|
path.add_boundary_handle(handle);
|
||||||
}
|
}
|
||||||
dxf.paths.push(path);
|
dxf.paths.push(path);
|
||||||
}
|
}
|
||||||
@@ -2254,6 +2299,7 @@ impl Scene {
|
|||||||
color2,
|
color2,
|
||||||
kind,
|
kind,
|
||||||
invert,
|
invert,
|
||||||
|
shift,
|
||||||
} = &model.pattern
|
} = &model.pattern
|
||||||
{
|
{
|
||||||
let to_color = |c: [f32; 4]| acadrust::types::Color::Rgb {
|
let to_color = |c: [f32; 4]| acadrust::types::Color::Rgb {
|
||||||
@@ -2264,10 +2310,10 @@ impl Scene {
|
|||||||
dxf.is_solid = true;
|
dxf.is_solid = true;
|
||||||
dxf.gradient_color.enabled = true;
|
dxf.gradient_color.enabled = true;
|
||||||
dxf.gradient_color.name = kind.dxf_name(*invert).to_string();
|
dxf.gradient_color.name = kind.dxf_name(*invert).to_string();
|
||||||
// The render model reads the gradient angle from pattern_angle
|
// Keep both persisted angle fields aligned.
|
||||||
// (radians); the gradient record keeps its own copy for the file.
|
|
||||||
dxf.pattern_angle = (*angle_deg as f64).to_radians();
|
dxf.pattern_angle = (*angle_deg as f64).to_radians();
|
||||||
dxf.gradient_color.angle = (*angle_deg as f64).to_radians();
|
dxf.gradient_color.angle = (*angle_deg as f64).to_radians();
|
||||||
|
dxf.gradient_color.shift = *shift as f64;
|
||||||
dxf.gradient_color.is_single_color = false;
|
dxf.gradient_color.is_single_color = false;
|
||||||
// Linear has no INV name in the standard set: persist an inverted
|
// Linear has no INV name in the standard set: persist an inverted
|
||||||
// linear by swapping the colour stops instead.
|
// linear by swapping the colour stops instead.
|
||||||
|
|||||||
@@ -4652,71 +4652,6 @@ impl Scene {
|
|||||||
names
|
names
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Collect closed polygon outlines (world XY) from the current layout.
|
|
||||||
pub fn closed_outlines(&self) -> Vec<Vec<[f64; 2]>> {
|
|
||||||
self.entity_wires()
|
|
||||||
.iter()
|
|
||||||
.filter_map(|wire| {
|
|
||||||
let pts: Vec<[f64; 2]> = wire
|
|
||||||
.points
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.enumerate()
|
|
||||||
.filter_map(|(index, high)| {
|
|
||||||
if !high[0].is_finite() || !high[1].is_finite() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]);
|
|
||||||
let point = [
|
|
||||||
high[0] as f64 + low[0] as f64,
|
|
||||||
high[1] as f64 + low[1] as f64,
|
|
||||||
];
|
|
||||||
point.iter().all(|value| value.is_finite()).then_some(point)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
if pts.len() < 4 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let f = pts.first()?;
|
|
||||||
let l = pts.last()?;
|
|
||||||
let dx = f[0] - l[0];
|
|
||||||
let dy = f[1] - l[1];
|
|
||||||
if (dx * dx + dy * dy).sqrt() > 1e-2 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
// Segment-list wires (e.g. LwPolyline) store each segment as an
|
|
||||||
// independent NaN-separated pair, so every shared corner repeats
|
|
||||||
// (`A B | B C | C D | D A`). Collapse that back into a clean ring:
|
|
||||||
// skip the NaN separators and any vertex coincident with the
|
|
||||||
// previous one, so consumers (point-in-polygon, the hatch /
|
|
||||||
// boundary commands) see one vertex per corner — not the doubled
|
|
||||||
// ring that otherwise shows two grips at every corner.
|
|
||||||
let mut ring: Vec<[f64; 2]> = Vec::with_capacity(pts.len());
|
|
||||||
for q in pts {
|
|
||||||
if let Some(&last) = ring.last() {
|
|
||||||
if (last[0] - q[0]).abs() < 1e-4 && (last[1] - q[1]).abs() < 1e-4 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ring.push(q);
|
|
||||||
}
|
|
||||||
// Drop a trailing vertex equal to the first — the ring is closed
|
|
||||||
// implicitly, so keeping it would be a duplicate corner.
|
|
||||||
if ring.len() > 1 {
|
|
||||||
let first = ring[0];
|
|
||||||
let last = *ring.last().unwrap();
|
|
||||||
if (first[0] - last[0]).abs() < 1e-4 && (first[1] - last[1]).abs() < 1e-4 {
|
|
||||||
ring.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ring.len() < 3 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(ring)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wire set for the Model layout, shared by every tile.
|
/// Wire set for the Model layout, shared by every tile.
|
||||||
///
|
///
|
||||||
/// The model wire geometry is **camera-independent**, so it is tessellated
|
/// The model wire geometry is **camera-independent**, so it is tessellated
|
||||||
|
|||||||
@@ -50,12 +50,19 @@ pub enum GradientKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl GradientKind {
|
impl GradientKind {
|
||||||
pub const ALL: [GradientKind; 5] = [
|
/// User-facing gradient definitions in their standard persisted order.
|
||||||
GradientKind::Linear,
|
/// Inverted definitions are real named patterns rather than a separate
|
||||||
GradientKind::Cylinder,
|
/// property, so Properties and the draw command share this single list.
|
||||||
GradientKind::Spherical,
|
pub const CHOICES: [(GradientKind, bool); 9] = [
|
||||||
GradientKind::Hemispherical,
|
(GradientKind::Linear, false),
|
||||||
GradientKind::Curved,
|
(GradientKind::Cylinder, false),
|
||||||
|
(GradientKind::Cylinder, true),
|
||||||
|
(GradientKind::Spherical, false),
|
||||||
|
(GradientKind::Hemispherical, false),
|
||||||
|
(GradientKind::Curved, false),
|
||||||
|
(GradientKind::Spherical, true),
|
||||||
|
(GradientKind::Hemispherical, true),
|
||||||
|
(GradientKind::Curved, true),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Radial fills shade from the boundary centre outward.
|
/// Radial fills shade from the boundary centre outward.
|
||||||
@@ -63,18 +70,25 @@ impl GradientKind {
|
|||||||
matches!(self, GradientKind::Spherical | GradientKind::Hemispherical)
|
matches!(self, GradientKind::Spherical | GradientKind::Hemispherical)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn label(self) -> &'static str {
|
pub fn choice_label(self, inverted: bool) -> &'static str {
|
||||||
match self {
|
match (self, inverted) {
|
||||||
GradientKind::Linear => "Linear",
|
(GradientKind::Linear, _) => "Linear",
|
||||||
GradientKind::Cylinder => "Cylinder",
|
(GradientKind::Cylinder, false) => "Cylindrical",
|
||||||
GradientKind::Spherical => "Spherical",
|
(GradientKind::Cylinder, true) => "Inverted cylindrical",
|
||||||
GradientKind::Hemispherical => "Hemispherical",
|
(GradientKind::Spherical, false) => "Spherical",
|
||||||
GradientKind::Curved => "Curved",
|
(GradientKind::Spherical, true) => "Inverted spherical",
|
||||||
|
(GradientKind::Hemispherical, false) => "Hemispherical",
|
||||||
|
(GradientKind::Hemispherical, true) => "Inverted hemispherical",
|
||||||
|
(GradientKind::Curved, false) => "Curved",
|
||||||
|
(GradientKind::Curved, true) => "Inverted curved",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_label(label: &str) -> Option<Self> {
|
pub fn from_choice_label(label: &str) -> Option<(Self, bool)> {
|
||||||
Self::ALL.iter().copied().find(|k| k.label() == label)
|
Self::CHOICES
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.find(|(kind, inverted)| kind.choice_label(*inverted).eq_ignore_ascii_case(label))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse the DXF gradient name (`LINEAR`, `INVCYLINDER`, …) into the kind
|
/// Parse the DXF gradient name (`LINEAR`, `INVCYLINDER`, …) into the kind
|
||||||
@@ -139,6 +153,8 @@ pub enum HatchPattern {
|
|||||||
color2: [f32; 4],
|
color2: [f32; 4],
|
||||||
kind: GradientKind,
|
kind: GradientKind,
|
||||||
invert: bool,
|
invert: bool,
|
||||||
|
/// 0 = centred, 1 = shifted towards the upper-left light source.
|
||||||
|
shift: f32,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,6 +224,25 @@ pub struct HatchModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HatchModel {
|
impl HatchModel {
|
||||||
|
pub(crate) fn gradient_frame(
|
||||||
|
&self,
|
||||||
|
angle_deg: f32,
|
||||||
|
shift: f32,
|
||||||
|
) -> Option<cadkernel::geom2d::GradientFrame> {
|
||||||
|
let boundary: Vec<[f64; 2]> = self
|
||||||
|
.boundary
|
||||||
|
.iter()
|
||||||
|
.filter(|point| point[0].is_finite() && point[1].is_finite())
|
||||||
|
.map(|point| [point[0] as f64, point[1] as f64])
|
||||||
|
.collect();
|
||||||
|
cadkernel::geom2d::gradient_frame(
|
||||||
|
&boundary,
|
||||||
|
(angle_deg as f64).to_radians(),
|
||||||
|
shift as f64,
|
||||||
|
Tolerance::default(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Indexed local-space fill mesh with even-odd loop containment.
|
/// Indexed local-space fill mesh with even-odd loop containment.
|
||||||
pub(crate) fn fill_mesh(&self) -> (Vec<[f32; 2]>, Vec<u32>) {
|
pub(crate) fn fill_mesh(&self) -> (Vec<[f32; 2]>, Vec<u32>) {
|
||||||
let mut rings = Vec::new();
|
let mut rings = Vec::new();
|
||||||
|
|||||||
@@ -262,21 +262,26 @@ impl StorageHatchBatch {
|
|||||||
let mut grad_kind = 0u32;
|
let mut grad_kind = 0u32;
|
||||||
let (mode, color2, grad_cos, grad_sin, grad_min, grad_range) = match &h.pattern {
|
let (mode, color2, grad_cos, grad_sin, grad_min, grad_range) = match &h.pattern {
|
||||||
HatchPattern::Solid => (1u32, [0.0; 4], 0.0, 0.0, 0.0, 1.0),
|
HatchPattern::Solid => (1u32, [0.0; 4], 0.0, 0.0, 0.0, 1.0),
|
||||||
HatchPattern::Gradient { angle_deg, color2, kind, invert } => {
|
HatchPattern::Gradient {
|
||||||
|
angle_deg,
|
||||||
|
color2,
|
||||||
|
kind,
|
||||||
|
invert,
|
||||||
|
shift,
|
||||||
|
} => {
|
||||||
grad_kind = kind.shader_kind() | if *invert { 16 } else { 0 };
|
grad_kind = kind.shader_kind() | if *invert { 16 } else { 0 };
|
||||||
|
let frame = h.gradient_frame(*angle_deg, *shift);
|
||||||
if kind.radial() {
|
if kind.radial() {
|
||||||
// Radial fill: the boundary is stored relative to its
|
let center = frame.map_or([0.0, 0.0], |frame| {
|
||||||
// centre (`world_origin`), so the centre is the local
|
[frame.center[0] as f32, frame.center[1] as f32]
|
||||||
// origin; radius = the farthest boundary vertex. mode 3.
|
});
|
||||||
let radius = radial_radius(&h.boundary);
|
let radius = frame.map_or(1.0, |frame| frame.radius as f32);
|
||||||
(3u32, *color2, 0.0, 0.0, 0.0, radius)
|
(3u32, *color2, center[0], center[1], 0.0, radius)
|
||||||
} else {
|
} else {
|
||||||
let r = angle_deg.to_radians();
|
let r = angle_deg.to_radians();
|
||||||
// Gradient projection range (proj_min / proj_range) —
|
let minimum = frame.map_or(0.0, |frame| frame.projection_min as f32);
|
||||||
// computed at upload time, identical to per-hatch path.
|
let span = frame.map_or(1.0, |frame| frame.projection_span as f32);
|
||||||
let (gmin, gmax) = boundary_projection_range(&h.boundary, r);
|
(2u32, *color2, r.cos(), r.sin(), minimum, span)
|
||||||
let grange = (gmax - gmin).max(1.0);
|
|
||||||
(2u32, *color2, r.cos(), r.sin(), gmin, grange)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HatchPattern::Pattern(fams) => {
|
HatchPattern::Pattern(fams) => {
|
||||||
@@ -635,42 +640,6 @@ impl LineFamilyGpu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Project every boundary vertex onto the gradient direction
|
|
||||||
/// `(cos θ, sin θ)` and return the (min, max) projection. Used to set
|
|
||||||
/// up the gradient's normalized parameter range. Same math as the
|
|
||||||
/// per-hatch path; duplicated here to keep this module self-contained.
|
|
||||||
fn boundary_projection_range(boundary: &[[f32; 2]], theta: f32) -> (f32, f32) {
|
|
||||||
let (cs, sn) = (theta.cos(), theta.sin());
|
|
||||||
let mut lo = f32::INFINITY;
|
|
||||||
let mut hi = f32::NEG_INFINITY;
|
|
||||||
for &[x, y] in boundary {
|
|
||||||
if !x.is_finite() || !y.is_finite() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let p = x * cs + y * sn;
|
|
||||||
if p < lo { lo = p; }
|
|
||||||
if p > hi { hi = p; }
|
|
||||||
}
|
|
||||||
if !lo.is_finite() {
|
|
||||||
return (0.0, 1.0);
|
|
||||||
}
|
|
||||||
(lo, hi)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Radius of a radial gradient — the distance from the boundary centre (the
|
|
||||||
/// local origin, since vertices are stored relative to it) to the farthest
|
|
||||||
/// vertex, so `t = 1` (the end colour) reaches the corners.
|
|
||||||
fn radial_radius(boundary: &[[f32; 2]]) -> f32 {
|
|
||||||
boundary
|
|
||||||
.iter()
|
|
||||||
.filter(|p| p[0].is_finite() && p[1].is_finite())
|
|
||||||
.map(|p| (p[0] * p[0] + p[1] * p[1]).sqrt())
|
|
||||||
.fold(0.0_f32, f32::max)
|
|
||||||
.max(1.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PatFamily is re-exported by hatch_model so we don't need to import
|
// PatFamily is re-exported by hatch_model so we don't need to import
|
||||||
// it explicitly anywhere else — but rust needs the type referenced to
|
// it explicitly anywhere else — but rust needs the type referenced to
|
||||||
// confirm the layout assumption above.
|
// confirm the layout assumption above.
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ impl TextureHatch {
|
|||||||
let (mode, color2, grad_cos, grad_sin) = match &model.pattern {
|
let (mode, color2, grad_cos, grad_sin) = match &model.pattern {
|
||||||
HatchPattern::Solid => (1u32, [0.0f32; 4], 0.0f32, 0.0f32),
|
HatchPattern::Solid => (1u32, [0.0f32; 4], 0.0f32, 0.0f32),
|
||||||
HatchPattern::Pattern(_) => (0u32, [0.0f32; 4], 0.0f32, 0.0f32),
|
HatchPattern::Pattern(_) => (0u32, [0.0f32; 4], 0.0f32, 0.0f32),
|
||||||
HatchPattern::Gradient { angle_deg, color2, kind, invert } => {
|
HatchPattern::Gradient { angle_deg, color2, kind, invert, .. } => {
|
||||||
let gk = (kind.shader_kind() | if *invert { 16 } else { 0 }) << 8;
|
let gk = (kind.shader_kind() | if *invert { 16 } else { 0 }) << 8;
|
||||||
if kind.radial() {
|
if kind.radial() {
|
||||||
// Radial: centre is the local origin; grad_cos/sin unused.
|
// Radial: centre is the local origin; grad_cos/sin unused.
|
||||||
@@ -191,7 +191,6 @@ impl TextureHatch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let origin = model.world_origin;
|
let origin = model.world_origin;
|
||||||
let drift = [0.0f32, 0.0f32];
|
|
||||||
let (mesh_points, indices) = model.fill_mesh();
|
let (mesh_points, indices) = model.fill_mesh();
|
||||||
if mesh_points.is_empty() || indices.is_empty() {
|
if mesh_points.is_empty() || indices.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
@@ -239,33 +238,31 @@ impl TextureHatch {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── Gradient projection range (snapped-local space) ───────────────
|
// ── Gradient projection range (snapped-local space) ───────────────
|
||||||
let (grad_min, grad_range) = if mode == 2 {
|
let base_mode = mode & 0xFF;
|
||||||
let projs: Vec<f32> = model
|
let frame = match &model.pattern {
|
||||||
.boundary
|
HatchPattern::Gradient {
|
||||||
.iter()
|
angle_deg, shift, ..
|
||||||
.filter(|v| v[0].is_finite() && v[1].is_finite())
|
} => model.gradient_frame(*angle_deg, *shift),
|
||||||
.map(|&[x, y]| (x + drift[0]) * grad_cos + (y + drift[1]) * grad_sin)
|
_ => None,
|
||||||
.collect();
|
};
|
||||||
if projs.is_empty() {
|
let (grad_min, grad_range, radial_center) = if base_mode == 2 {
|
||||||
(0.0, 1.0)
|
frame.map_or((0.0, 1.0, [0.0, 0.0]), |frame| {
|
||||||
} else {
|
(
|
||||||
let proj_min = projs.iter().cloned().fold(f32::INFINITY, f32::min);
|
frame.projection_min as f32,
|
||||||
let proj_max = projs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
|
frame.projection_span as f32,
|
||||||
// Floor matches the storage backend.
|
[0.0, 0.0],
|
||||||
(proj_min, (proj_max - proj_min).max(1.0))
|
)
|
||||||
}
|
})
|
||||||
} else if mode == 3 {
|
} else if base_mode == 3 {
|
||||||
// Radial: range = the farthest boundary vertex from the centre.
|
frame.map_or((0.0, 1.0, [0.0, 0.0]), |frame| {
|
||||||
let radius = model
|
(
|
||||||
.boundary
|
0.0,
|
||||||
.iter()
|
frame.radius as f32,
|
||||||
.filter(|v| v[0].is_finite() && v[1].is_finite())
|
[frame.center[0] as f32, frame.center[1] as f32],
|
||||||
.map(|&[x, y]| (x * x + y * y).sqrt())
|
)
|
||||||
.fold(0.0_f32, f32::max)
|
})
|
||||||
.max(1.0);
|
|
||||||
(0.0, radius)
|
|
||||||
} else {
|
} else {
|
||||||
(0.0, 1.0)
|
(0.0, 1.0, [0.0, 0.0])
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Pack the data texture: families | dashes ─────────────────────
|
// ── Pack the data texture: families | dashes ─────────────────────
|
||||||
@@ -355,8 +352,8 @@ impl TextureHatch {
|
|||||||
// Clamp like the desktop renderer so scale==0 can't make perp_step 0
|
// Clamp like the desktop renderer so scale==0 can't make perp_step 0
|
||||||
// → round(perp/0)=NaN → an invisible hatch.
|
// → round(perp/0)=NaN → an invisible hatch.
|
||||||
scale: model.scale.max(1e-6),
|
scale: model.scale.max(1e-6),
|
||||||
grad_cos,
|
grad_cos: if base_mode == 3 { radial_center[0] } else { grad_cos },
|
||||||
grad_sin,
|
grad_sin: if base_mode == 3 { radial_center[1] } else { grad_sin },
|
||||||
grad_min,
|
grad_min,
|
||||||
grad_range,
|
grad_range,
|
||||||
origin: [origin[0] as f32, origin[1] as f32],
|
origin: [origin[0] as f32, origin[1] as f32],
|
||||||
|
|||||||
@@ -1345,6 +1345,7 @@ fn render_signature(vp: &ViewportData, clip_w: u32, clip_h: u32) -> u64 {
|
|||||||
color2,
|
color2,
|
||||||
kind,
|
kind,
|
||||||
invert,
|
invert,
|
||||||
|
shift,
|
||||||
} => {
|
} => {
|
||||||
2_u8.hash(&mut h);
|
2_u8.hash(&mut h);
|
||||||
angle_deg.to_bits().hash(&mut h);
|
angle_deg.to_bits().hash(&mut h);
|
||||||
@@ -1353,6 +1354,7 @@ fn render_signature(vp: &ViewportData, clip_w: u32, clip_h: u32) -> u64 {
|
|||||||
}
|
}
|
||||||
kind.shader_kind().hash(&mut h);
|
kind.shader_kind().hash(&mut h);
|
||||||
invert.hash(&mut h);
|
invert.hash(&mut h);
|
||||||
|
shift.to_bits().hash(&mut h);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user