diff --git a/Cargo.lock b/Cargo.lock index 86a9e3349..a7a156ff8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,7 +72,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.4.1" -source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=0975677#0975677029f2b472759db00e9692421a5831ad00" +source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=3742f38#3742f382e6f359178ec7bed6b7719c7ac85f3538" dependencies = [ "ahash 0.8.12", "anyhow", @@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=3d5de36#3d5de36cb5b391f17da924c5f80e34a0cdc1d948" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=927f888#927f88839bbffe024579e14f2acd4cc87ed4a05a" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 3aaed7a55..40099097e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,8 @@ glam = { version = "0.33", features = ["bytemuck"] } rfd = "0.17" clap = { version = "4", features = ["derive"] } env_logger = "0.11" -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "0975677", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "3d5de36", features = ["acis", "offset"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "3742f38", features = ["serde"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "927f888", features = ["acis", "offset"] } 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 5a540bdc4..dd1ab036a 100644 --- a/crates/ocs_plugin_api/Cargo.toml +++ b/crates/ocs_plugin_api/Cargo.toml @@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] } # Pulled in only by the `host` feature, which adds the `acadrust`-typed # `HostApi` runtime surface. The default crate stays dependency-free so engine # crates and external tooling can depend on the manifest/ribbon contract cheaply. -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "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). interprocess = { version = "2", optional = true } @@ -37,7 +37,7 @@ serde_json = "1" serde = { version = "1", features = ["derive"] } cargo-lock = "11" # acadrust is scanned at build time to generate the embedded type registry. -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "0975677", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "3742f38", features = ["serde"] } [dev-dependencies] serde_json = "1" diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml index a38821d6d..2b46eae81 100644 --- a/crates/ocs_web_worker/Cargo.toml +++ b/crates/ocs_web_worker/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib"] [dependencies] -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "0975677", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "3742f38", 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 1f6f77259..bf4bee898 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -2025,6 +2025,11 @@ model = .cv-hull = غلاف نقاط التحكم .class-entity = الفئة / الكيان .curved = منحني + .cylindrical = أسطواني + .inverted-curved = منحني معكوس + .inverted-cylindrical = أسطواني معكوس + .inverted-hemispherical = نصف كروي معكوس + .inverted-spherical = كروي معكوس .data-size = حجم البيانات .database-curves = منحنيات قاعدة البيانات .design = التصميم diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 31406c0d3..edeabd690 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -2012,6 +2012,11 @@ model = .cv-hull = CV обвивка .class-entity = Клас / Обект .curved = Извит + .cylindrical = Цилиндричен + .inverted-curved = Обърнат извит + .inverted-cylindrical = Обърнат цилиндричен + .inverted-hemispherical = Обърнат полусферичен + .inverted-spherical = Обърнат сферичен .data-size = Размер на данни .database-curves = Криви в базата данни .design = Дизайн diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 58ededaae..d134b68c3 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -2012,6 +2012,11 @@ model = .cv-hull = CV Trup .class-entity =Třída / Entita .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 .database-curves = Databázové křivky .design = Design diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 191cea3fd..82e16e565 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -2011,6 +2011,11 @@ model = .cv-hull = Kontrollpolygon .class-entity = Klasse / Objekt .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 .database-curves = Datenbankkurven .design = Design diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 6e6192fa9..f4bf7ed2c 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -2014,6 +2014,11 @@ model = .cv-hull = CV Hull .class-entity = Class / Entity .curved = Curved + .cylindrical = Cylindrical + .inverted-curved = Inverted curved + .inverted-cylindrical = Inverted cylindrical + .inverted-hemispherical = Inverted hemispherical + .inverted-spherical = Inverted spherical .data-size = Data Size .database-curves = Database Curves .design = Design diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index d44ddbcca..52111a54e 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -2013,6 +2013,11 @@ model = .cv-hull = Envolvente de vértices de control .class-entity = Clase/Entidad .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 .database-curves = Curvas de base de datos .design = Diseño diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 677962f94..41803063c 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -2012,6 +2012,11 @@ model = .cv-hull = CV Runko .class-entity =Luokka / Entiteetti .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 .database-curves = Tietokantakäyrät .design = Suunnittelu diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 494202760..b3e4f5eb4 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -2013,6 +2013,11 @@ model = .cv-hull = CV Houille .class-entity = Catégorie/entité .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 .database-curves = Courbes de base de données .design = Conception diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 55aaf3126..3f7104394 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -2001,6 +2001,11 @@ model = .cv-hull = CV हल .class-entity = क्लास / ऑब्जेक्ट .curved = घुमावदार + .cylindrical = बेलनाकार + .inverted-curved = उलटा घुमावदार + .inverted-cylindrical = उलटा बेलनाकार + .inverted-hemispherical = उलटा अर्धगोलाकार + .inverted-spherical = उलटा गोलाकार .data-size = डेटा आकार .database-curves = डेटाबेस वक्र .design = डिजाइन diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index dd17a2151..2b7b9d0b1 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -2012,6 +2012,11 @@ model = .cv-hull = CV Ház .class-entity =Osztály / Entitás .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 .database-curves = Adatbázis görbék .design = Tervezés diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 1ad809462..03b94d3d6 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -2012,6 +2012,11 @@ model = .cv-hull = CV Scafo .class-entity =Classe/Entità .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 .database-curves = Curve di database .design = Progettazione diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 0e98fa03e..f19e343fe 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -2001,6 +2001,11 @@ model = .cv-hull = 制御点ハル .class-entity = クラス / 図形 .curved = 曲線 + .cylindrical = 円筒状 + .inverted-curved = 反転曲線 + .inverted-cylindrical = 反転円筒状 + .inverted-hemispherical = 反転半球状 + .inverted-spherical = 反転球状 .data-size = データサイズ .database-curves = データベース曲線 .design = 設計 diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index c90e6a014..7d7aeb2ff 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -2012,6 +2012,11 @@ model = .cv-hull = CV 선체 .class-entity =클래스/엔티티 .curved = 곡선 + .cylindrical = 원통형 + .inverted-curved = 반전 곡선형 + .inverted-cylindrical = 반전 원통형 + .inverted-hemispherical = 반전 반구형 + .inverted-spherical = 반전 구형 .data-size = 데이터 크기 .database-curves = 데이터베이스 곡선 .design = 디자인 diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 9f74cf064..3606e275a 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -2013,6 +2013,11 @@ model = .cv-hull = CV Romp .class-entity = Klasse/object .curved = gebogen + .cylindrical = Cilindrisch + .inverted-curved = Omgekeerd gebogen + .inverted-cylindrical = Omgekeerd cilindrisch + .inverted-hemispherical = Omgekeerd hemisferisch + .inverted-spherical = Omgekeerd sferisch .data-size = Gegevensgrootte .database-curves = Databasecurves .design = Ontwerp diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 3e5cc3341..740041e0b 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -2012,6 +2012,11 @@ model = .cv-hull = Kadłub CV .class-entity =Klasa/jednostka .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 .database-curves = Krzywe bazy danych .design = Projekt diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 122e2737c..662cba1f7 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -2013,6 +2013,11 @@ model = .cv-hull = Casco de VC .class-entity = Classe / Entidade .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 .database-curves = Curvas do banco de dados .design = Projeto diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index f78373041..132b5fc41 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -2016,6 +2016,11 @@ model = .cv-hull = CV Корпус .class-entity = Класс / Объект .curved = искривленный + .cylindrical = цилиндрический + .inverted-curved = инвертированный искривленный + .inverted-cylindrical = инвертированный цилиндрический + .inverted-hemispherical = инвертированный полусферический + .inverted-spherical = инвертированный сферический .data-size = Размер данных .database-curves = Кривые базы данных .design = Дизайн diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 8ca5741cb..5d765ea88 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -1974,6 +1974,11 @@ model = .cv-hull = Kontrol Noktası Kabuğu .class-entity = Sınıf / Nesne .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 .database-curves = Veritabanı Eğrileri .design = Tasarım diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index c464f3b5b..2c801034f 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -1996,6 +1996,11 @@ model = .cv-hull = CV 外壳 .class-entity = 类/实体 .curved = 曲线 + .cylindrical = 圆柱形 + .inverted-curved = 反向曲线形 + .inverted-cylindrical = 反向圆柱形 + .inverted-hemispherical = 反向半球形 + .inverted-spherical = 反向球形 .data-size = 数据大小 .database-curves = 数据库曲线 .design = 设计 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 9a252fedb..f0b3c3069 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -2008,6 +2008,11 @@ model = .cv-hull = CV 外殼 .class-entity = 類/實體 .curved = 曲線 + .cylindrical = 圓柱形 + .inverted-curved = 反向曲線形 + .inverted-cylindrical = 反向圓柱形 + .inverted-hemispherical = 反向半球形 + .inverted-spherical = 反向球形 .data-size = 資料大小 .database-curves = 資料庫曲線 .design = 設計 diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 2933bd22a..b05809d90 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -676,8 +676,9 @@ impl OpenCADStudio { "GRADIENT" => { use crate::modules::draw::draw::hatch::GradientCommand; - let outlines = self.tabs[i].scene.closed_outlines(); - let new_cmd = GradientCommand::new(outlines); + let outlines = self.tabs[i].scene.hatch_boundary_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.tabs[i].active_cmd = Some(Box::new(new_cmd)); } diff --git a/src/app/properties.rs b/src/app/properties.rs index 15e92b0b8..94ba10d7c 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -1224,7 +1224,8 @@ impl OpenCADStudio { acadrust::EntityType::Text(_) | acadrust::EntityType::MText(_) | acadrust::EntityType::Insert(_) - | acadrust::EntityType::Leader(_) => Some(("annotative", None)), + | acadrust::EntityType::Leader(_) + | acadrust::EntityType::Hatch(_) => Some(("annotative", None)), acadrust::EntityType::MultiLeader(_) => { Some(("enable_annotation_scale", None)) } @@ -1267,7 +1268,8 @@ impl OpenCADStudio { }, ), acadrust::EntityType::Text(_) - | acadrust::EntityType::Insert(_) => set_row_value( + | acadrust::EntityType::Insert(_) + | acadrust::EntityType::Hatch(_) => set_row_value( &mut sections, "annotative", crate::scene::model::object::PropValue::BoolToggle { diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 28aa223b7..ff561f64d 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1749,33 +1749,84 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { let handles = self.property_target_handles(i); 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 Some(acadrust::EntityType::Spline(spline)) = - self.tabs[i].scene.document.get_entity(*handle) - else { - return false; - }; - match field { - "spline_method" => { - value == if crate::entities::spline::shows_fit_points(spline) { - "Fit" - } else { - "Control Vertices" + match self.tabs[i].scene.document.get_entity(*handle) { + Some(acadrust::EntityType::Spline(spline)) => match field { + "spline_method" => { + value + == if crate::entities::spline::shows_fit_points(spline) { + "Fit" + } else { + "Control Vertices" + } } - } - "knot_param" => { - let current = match spline.knot_parameterization { - 0 => "Chord", - 1 => "Square Root", - 2 => "Uniform", - _ => "Custom", - }; - value == current - } - "cv_frame" => { - value == if spline.cv_frame_visible { "Show" } else { "Hide" } - } + "knot_param" => { + let current = match spline.knot_parameterization { + 0 => "Chord", + 1 => "Square Root", + 2 => "Uniform", + _ => "Custom", + }; + value == current + } + "cv_frame" => { + value + == 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, } }); diff --git a/src/entities/hatch.rs b/src/entities/hatch.rs index d0f2201a6..1010bdc7a 100644 --- a/src/entities/hatch.rs +++ b/src/entities/hatch.rs @@ -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 { let pattern_type = match h.pattern_type { acadrust::entities::HatchPatternType::Predefined => "Predefined", @@ -345,59 +372,66 @@ fn properties(h: &Hatch) -> Vec { if g.enabled { // ── Gradient fill ────────────────────────────────────────────────── 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 mut sections = vec![ + let (kind, inverted) = + 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 { title: t!("Pattern").into_owned(), - props: vec![ - 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), - ], + props: pattern_props, }, PropSection { title: t!("Geometry").into_owned(), @@ -410,33 +444,19 @@ fn properties(h: &Hatch) -> Vec { PropSection { title: t!("Misc").into_owned(), props: vec![ - ro(t!("Associative").as_ref(), - "associative", - if h.is_associative { "Yes" } else { "No" }, - ), + associative_property(h), ro(t!("Annotative").as_ref(), "annotative", String::new()), - ro(t!("Island detection style").as_ref(), "style", style), Property { - label: t!("Background").into_owned(), - field: "bg_enabled", - value: PropValue::BoolToggle { - field: "bg_enabled", - value: bg_on, + label: t!("Island detection style").into_owned(), + field: "style", + value: PropValue::Choice { + selected: style.to_string(), + 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 { field: "pattern_name", value: PropValue::HatchPatternChoice(h.pattern.name.clone()), }; - let associative_row = Property { - label: t!("Associative").into_owned(), - field: "associative", - value: PropValue::BoolToggle { - field: "associative", - value: h.is_associative, - }, - }; + let associative_row = associative_property(h); let double_row = Property { label: t!("Double").into_owned(), field: "double", @@ -567,11 +580,22 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) { return; } "associative" => { - h.is_associative = if value == "toggle" { + let requested = if value == "toggle" { !h.is_associative } else { 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; } "pattern_type_label" => { @@ -593,31 +617,27 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) { return; } "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; } - // 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" => { use crate::scene::model::hatch_model::GradientKind; - let (_, invert) = GradientKind::from_name(&h.gradient_color.name); - if let Some(kind) = GradientKind::from_label(value) { + if let Some((kind, invert)) = GradientKind::from_choice_label(value) { h.gradient_color.name = kind.dxf_name(invert).to_string(); } return; } - "gradient_invert" => { - use crate::scene::model::hatch_model::GradientKind; - let (kind, invert) = GradientKind::from_name(&h.gradient_color.name); - let invert = !invert; - if matches!(kind, GradientKind::Linear) { - if h.gradient_color.colors.len() >= 2 { - h.gradient_color.colors.swap(0, 1); - } + "gradient_centered" => { + let centered = if value == "toggle" { + h.gradient_color.shift >= 0.5 } 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; } _ => {} @@ -630,6 +650,9 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) { h.gradient_color.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" => { let angle = v.to_radians(); let delta = angle - h.pattern_angle; diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index eb22b4922..940da5bca 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -701,6 +701,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Cutting edges" => Some(("modify", "cutting-edges")), "Cyan" => Some(("common", "cyan")), "Cylinder" => Some(("common", "cylinder")), + "Cylindrical" => Some(("model", "cylindrical")), "DATAEXTRACTION {rows} rows → \"{fname}\"" => Some(("common", "dataextraction-rows-rows-fname")), "DATAEXTRACTION: write failed: {e}" => Some(("common", "dataextraction-write-failed-e")), "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 Square" => Some(("properties", "inverse-square")), "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: {} object(s) selected." => Some(("common", "invert-selection-object-s-selected")), "Invert: off" => Some(("draw", "invert-off")), diff --git a/src/modules/draw/draw/hatch.rs b/src/modules/draw/draw/hatch.rs index b86a61b56..3f97f4a70 100644 --- a/src/modules/draw/draw/hatch.rs +++ b/src/modules/draw/draw/hatch.rs @@ -306,7 +306,7 @@ impl HatchCommand { let (rel, origin, wcs) = pack_rings(&rings); let exterior = cadkernel::geom2d::ring_nesting_depths(&rings) .into_iter() - .map(|depth| depth % 2 == 0) + .map(|depth| depth == 0) .collect(); let boundary_sources = rings .iter() @@ -645,6 +645,7 @@ impl CadCommand for HatchCommand { pub struct GradientCommand { outlines: Vec>, + boundary_sources: rustc_hash::FxHashMap>, mode: Mode, manual_pts: Vec, missed: bool, @@ -655,9 +656,13 @@ pub struct GradientCommand { } impl GradientCommand { - pub fn new(outlines: Vec>) -> Self { + pub fn new( + outlines: Vec>, + boundary_sources: rustc_hash::FxHashMap>, + ) -> Self { Self { outlines, + boundary_sources, mode: Mode::PickInside, manual_pts: vec![], missed: false, @@ -668,6 +673,14 @@ impl GradientCommand { fn make_hatch(&self, rings: Vec>) -> HatchModel { 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 { render_instance: None, boundary: std::sync::Arc::new(rel), @@ -676,6 +689,7 @@ impl GradientCommand { color2: [0.18, 0.18, 0.18, 0.0], kind: self.kind, invert: self.invert, + shift: 0.0, }, name: self.kind.dxf_name(self.invert).into(), color: [0.30, 0.60, 0.95, 0.80], @@ -685,8 +699,8 @@ impl GradientCommand { scale: 1.0, world_origin: origin, boundary_wcs: Some(std::sync::Arc::new(wcs)), - boundary_exterior: None, - boundary_sources: None, + boundary_exterior: Some(std::sync::Arc::new(exterior)), + boundary_sources: Some(std::sync::Arc::new(boundary_sources)), draw_depth: 0.0, } } @@ -705,15 +719,10 @@ impl CadCommand for GradientCommand { } else { std::borrow::Cow::Borrowed("") }; - let invert = if self.invert { - t!(", inverted") - } else { - std::borrow::Cow::Borrowed("") - }; t!( "GRADIENT (%{kind}%{invert}) Pick internal point:%{miss}", - kind = t!(self.kind.label()), - invert = invert, + kind = t!(self.kind.choice_label(self.invert)), + invert = std::borrow::Cow::Borrowed(""), miss = miss ) .into_owned() @@ -733,15 +742,14 @@ impl CadCommand for GradientCommand { match &self.mode { Mode::PickInside => { let mut opts = vec![CmdOption::new("Draw manually", "S")]; - for k in crate::scene::model::hatch_model::GradientKind::ALL { - if k != self.kind { - opts.push(CmdOption::new(k.label(), k.label())); + for (kind, inverted) in + crate::scene::model::hatch_model::GradientKind::CHOICES + { + 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 } Mode::Manual => { @@ -806,17 +814,11 @@ impl CadCommand for GradientCommand { self.missed = false; return Some(CmdResult::NeedPoint); } - // Gradient type keywords / buttons + the invert toggle (#415). - if t.eq_ignore_ascii_case("i") || t.eq_ignore_ascii_case("invert") { - 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)) + if let Some((kind, inverted)) = + crate::scene::model::hatch_model::GradientKind::from_choice_label(t) { - self.kind = k; + self.kind = kind; + self.invert = inverted; return Some(CmdResult::NeedPoint); } None diff --git a/src/scene/boundary.rs b/src/scene/boundary.rs index 0ec5f65c4..aff47c86c 100644 --- a/src/scene/boundary.rs +++ b/src/scene/boundary.rs @@ -545,8 +545,8 @@ impl Scene { /// Build closed planar regions from the visible wire geometry. /// - /// Unlike `closed_outlines()`, the source entities do not need to be closed - /// individually. Intersections are inserted as temporary graph vertices and + /// Source entities do not need to be closed individually. Intersections are + /// inserted as temporary graph vertices and /// the bounded faces of that planar graph are returned as hatch candidates. /// /// Curved entities participate through their already-tessellated WireModel diff --git a/src/scene/entity.rs b/src/scene/entity.rs index d3b1ae2d3..62f7f2c99 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -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 { // ── Entity management ───────────────────────────────────────────────── @@ -1041,8 +1071,13 @@ impl Scene { m.angle_offset = dxf.pattern_angle as f32; m.scale = dxf.pattern_scale as f32; } - model::hatch_model::HatchPattern::Gradient { angle_deg, .. } => { - *angle_deg = dxf.pattern_angle.to_degrees() as f32; + model::hatch_model::HatchPattern::Gradient { + 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::Solid => {} @@ -1520,8 +1555,8 @@ impl Scene { return None; } - let mut boundary: Vec<[f64; 2]> = Vec::new(); - let mut boundary_exterior = Vec::new(); + let mut rings = Vec::new(); + let mut ring_sources = Vec::new(); for path in &dxf.paths { // Skip TEXTBOX boundary paths (flag bit 3). These are text @@ -1531,12 +1566,6 @@ impl Scene { if path.flags.bits() & 8 != 0 { 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::new(); for edge in &path.edges { if let Some(curve) = crate::entities::hatch::edge_curve(edge) { @@ -1549,29 +1578,44 @@ impl Scene { ); } } - boundary.extend(chain_path_edges(edge_polys)); - - if boundary.len() == path_start { - boundary.truncate(before_path); + let mut ring = chain_path_edges(edge_polys); + if ring.is_empty() { continue; } - boundary_exterior.push(path.flags.is_external() || path.flags.is_outermost()); - if boundary.len() >= path_start + 3 { - let first = boundary[path_start]; - let last = *boundary.last().unwrap(); + if ring.len() >= 3 { + let first = ring[0]; + let last = *ring.last().unwrap(); 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; } - if !boundary_exterior.iter().any(|role| *role) { - if let Some(first) = boundary_exterior.first_mut() { - *first = true; + + let depths = cadkernel::geom2d::ring_nesting_depths(&rings); + 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 // buffer (no fixed length), so a hatch with many island loops must @@ -1618,8 +1662,13 @@ impl Scene { ) }; gradient_color1 = stop(0); - let color2 = stop(1).unwrap_or(color); - let angle_deg = dxf.pattern_angle.to_degrees() as f32; + let color1 = gradient_color1.unwrap_or(color); + 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) = model::hatch_model::GradientKind::from_name(&dxf.gradient_color.name); model::hatch_model::HatchPattern::Gradient { @@ -1627,6 +1676,7 @@ impl Scene { color2, kind, invert, + shift: dxf.gradient_color.shift as f32, } } else if dxf.is_solid { model::hatch_model::HatchPattern::Solid @@ -1768,13 +1818,7 @@ impl Scene { boundary: std::sync::Arc::new(boundary_f32), boundary_wcs: None, boundary_exterior: Some(std::sync::Arc::new(boundary_exterior)), - boundary_sources: Some(std::sync::Arc::new( - dxf.paths - .iter() - .filter(|path| path.flags.bits() & 8 == 0) - .map(|path| path.boundary_handles.clone()) - .collect(), - )), + boundary_sources: Some(std::sync::Arc::new(boundary_sources)), pattern, name, // A gradient starts from its first stop; other fills use the @@ -2144,27 +2188,28 @@ impl Scene { let mut push_ring = |r: &mut Vec, is_outer: bool, index: usize| { if !r.is_empty() { let edge = PolylineEdge::new(std::mem::take(r), true); - let mut path = if is_outer { - 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 + let handles: Vec<_> = model .boundary_sources .as_deref() .and_then(|sources| sources.get(index)) - { - for &handle in handles { - if handle.is_valid() { - path.add_boundary_handle(handle); - } - } + .into_iter() + .flatten() + .copied() + .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); } @@ -2254,6 +2299,7 @@ impl Scene { color2, kind, invert, + shift, } = &model.pattern { let to_color = |c: [f32; 4]| acadrust::types::Color::Rgb { @@ -2264,10 +2310,10 @@ impl Scene { dxf.is_solid = true; dxf.gradient_color.enabled = true; dxf.gradient_color.name = kind.dxf_name(*invert).to_string(); - // The render model reads the gradient angle from pattern_angle - // (radians); the gradient record keeps its own copy for the file. + // Keep both persisted angle fields aligned. dxf.pattern_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; // Linear has no INV name in the standard set: persist an inverted // linear by swapping the colour stops instead. diff --git a/src/scene/mod.rs b/src/scene/mod.rs index b39801d61..4ec529ff7 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -4652,71 +4652,6 @@ impl Scene { names } - /// Collect closed polygon outlines (world XY) from the current layout. - pub fn closed_outlines(&self) -> Vec> { - 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. /// /// The model wire geometry is **camera-independent**, so it is tessellated diff --git a/src/scene/model/hatch_model.rs b/src/scene/model/hatch_model.rs index 0afc3e667..2a0792f4b 100644 --- a/src/scene/model/hatch_model.rs +++ b/src/scene/model/hatch_model.rs @@ -50,12 +50,19 @@ pub enum GradientKind { } impl GradientKind { - pub const ALL: [GradientKind; 5] = [ - GradientKind::Linear, - GradientKind::Cylinder, - GradientKind::Spherical, - GradientKind::Hemispherical, - GradientKind::Curved, + /// User-facing gradient definitions in their standard persisted order. + /// Inverted definitions are real named patterns rather than a separate + /// property, so Properties and the draw command share this single list. + pub const CHOICES: [(GradientKind, bool); 9] = [ + (GradientKind::Linear, false), + (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. @@ -63,18 +70,25 @@ impl GradientKind { matches!(self, GradientKind::Spherical | GradientKind::Hemispherical) } - pub fn label(self) -> &'static str { - match self { - GradientKind::Linear => "Linear", - GradientKind::Cylinder => "Cylinder", - GradientKind::Spherical => "Spherical", - GradientKind::Hemispherical => "Hemispherical", - GradientKind::Curved => "Curved", + pub fn choice_label(self, inverted: bool) -> &'static str { + match (self, inverted) { + (GradientKind::Linear, _) => "Linear", + (GradientKind::Cylinder, false) => "Cylindrical", + (GradientKind::Cylinder, true) => "Inverted cylindrical", + (GradientKind::Spherical, false) => "Spherical", + (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::ALL.iter().copied().find(|k| k.label() == label) + pub fn from_choice_label(label: &str) -> Option<(Self, bool)> { + 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 @@ -139,6 +153,8 @@ pub enum HatchPattern { color2: [f32; 4], kind: GradientKind, invert: bool, + /// 0 = centred, 1 = shifted towards the upper-left light source. + shift: f32, }, } @@ -208,6 +224,25 @@ pub struct HatchModel { } impl HatchModel { + pub(crate) fn gradient_frame( + &self, + angle_deg: f32, + shift: f32, + ) -> Option { + 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. pub(crate) fn fill_mesh(&self) -> (Vec<[f32; 2]>, Vec) { let mut rings = Vec::new(); diff --git a/src/scene/pipeline/hatch_gpu/storage.rs b/src/scene/pipeline/hatch_gpu/storage.rs index 4ab94131e..43a8edd1c 100644 --- a/src/scene/pipeline/hatch_gpu/storage.rs +++ b/src/scene/pipeline/hatch_gpu/storage.rs @@ -262,21 +262,26 @@ impl StorageHatchBatch { let mut grad_kind = 0u32; 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::Gradient { angle_deg, color2, kind, invert } => { + HatchPattern::Gradient { + angle_deg, + color2, + kind, + invert, + shift, + } => { grad_kind = kind.shader_kind() | if *invert { 16 } else { 0 }; + let frame = h.gradient_frame(*angle_deg, *shift); if kind.radial() { - // Radial fill: the boundary is stored relative to its - // centre (`world_origin`), so the centre is the local - // origin; radius = the farthest boundary vertex. mode 3. - let radius = radial_radius(&h.boundary); - (3u32, *color2, 0.0, 0.0, 0.0, radius) + let center = frame.map_or([0.0, 0.0], |frame| { + [frame.center[0] as f32, frame.center[1] as f32] + }); + let radius = frame.map_or(1.0, |frame| frame.radius as f32); + (3u32, *color2, center[0], center[1], 0.0, radius) } else { let r = angle_deg.to_radians(); - // Gradient projection range (proj_min / proj_range) — - // computed at upload time, identical to per-hatch path. - let (gmin, gmax) = boundary_projection_range(&h.boundary, r); - let grange = (gmax - gmin).max(1.0); - (2u32, *color2, r.cos(), r.sin(), gmin, grange) + let minimum = frame.map_or(0.0, |frame| frame.projection_min as f32); + let span = frame.map_or(1.0, |frame| frame.projection_span as f32); + (2u32, *color2, r.cos(), r.sin(), minimum, span) } } 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 // it explicitly anywhere else — but rust needs the type referenced to // confirm the layout assumption above. diff --git a/src/scene/pipeline/hatch_gpu/texture.rs b/src/scene/pipeline/hatch_gpu/texture.rs index 83fe314b8..f0d58b25c 100644 --- a/src/scene/pipeline/hatch_gpu/texture.rs +++ b/src/scene/pipeline/hatch_gpu/texture.rs @@ -165,7 +165,7 @@ impl TextureHatch { let (mode, color2, grad_cos, grad_sin) = match &model.pattern { HatchPattern::Solid => (1u32, [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; if kind.radial() { // Radial: centre is the local origin; grad_cos/sin unused. @@ -191,7 +191,6 @@ impl TextureHatch { } let origin = model.world_origin; - let drift = [0.0f32, 0.0f32]; let (mesh_points, indices) = model.fill_mesh(); if mesh_points.is_empty() || indices.is_empty() { return None; @@ -239,33 +238,31 @@ impl TextureHatch { }); // ── Gradient projection range (snapped-local space) ─────────────── - let (grad_min, grad_range) = if mode == 2 { - let projs: Vec = model - .boundary - .iter() - .filter(|v| v[0].is_finite() && v[1].is_finite()) - .map(|&[x, y]| (x + drift[0]) * grad_cos + (y + drift[1]) * grad_sin) - .collect(); - if projs.is_empty() { - (0.0, 1.0) - } else { - let proj_min = projs.iter().cloned().fold(f32::INFINITY, f32::min); - let proj_max = projs.iter().cloned().fold(f32::NEG_INFINITY, f32::max); - // Floor matches the storage backend. - (proj_min, (proj_max - proj_min).max(1.0)) - } - } else if mode == 3 { - // Radial: range = the farthest boundary vertex from the centre. - let radius = model - .boundary - .iter() - .filter(|v| v[0].is_finite() && v[1].is_finite()) - .map(|&[x, y]| (x * x + y * y).sqrt()) - .fold(0.0_f32, f32::max) - .max(1.0); - (0.0, radius) + let base_mode = mode & 0xFF; + let frame = match &model.pattern { + HatchPattern::Gradient { + angle_deg, shift, .. + } => model.gradient_frame(*angle_deg, *shift), + _ => None, + }; + let (grad_min, grad_range, radial_center) = if base_mode == 2 { + frame.map_or((0.0, 1.0, [0.0, 0.0]), |frame| { + ( + frame.projection_min as f32, + frame.projection_span as f32, + [0.0, 0.0], + ) + }) + } else if base_mode == 3 { + frame.map_or((0.0, 1.0, [0.0, 0.0]), |frame| { + ( + 0.0, + frame.radius as f32, + [frame.center[0] as f32, frame.center[1] as f32], + ) + }) } else { - (0.0, 1.0) + (0.0, 1.0, [0.0, 0.0]) }; // ── 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 // → round(perp/0)=NaN → an invisible hatch. scale: model.scale.max(1e-6), - grad_cos, - grad_sin, + grad_cos: if base_mode == 3 { radial_center[0] } else { grad_cos }, + grad_sin: if base_mode == 3 { radial_center[1] } else { grad_sin }, grad_min, grad_range, origin: [origin[0] as f32, origin[1] as f32], diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index d2ab7b66f..1259872d4 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -1345,6 +1345,7 @@ fn render_signature(vp: &ViewportData, clip_w: u32, clip_h: u32) -> u64 { color2, kind, invert, + shift, } => { 2_u8.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); invert.hash(&mut h); + shift.to_bits().hash(&mut h); } } }