From 9bfcad18b9c24f7f98b7ae1560b57bd11d9d0e34 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 21 Jul 2026 12:22:15 +0300 Subject: [PATCH] fix(hatch): pattern-scale edits take effect + background on/off MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Property edits on a hatch were invisible: fills render from the prebuilt model cache, which move/copy refreshed but the properties path never did — so a pattern-scale change (which must also rescale the stored final pattern lines by the ratio) changed nothing on screen (#415). invalidate_property_targets now rebuilds the edited fill's cached model via Scene::refresh_fill_model. The background row gains an on/off box: off removes the HATCHBACKGROUNDCOLOR record entirely (the colour could never be reverted before), on seeds ByLayer. The codec covers the full colour-method range (ByLayer / ByBlock / RGB / ACI) and the backdrop resolves ByLayer / ByBlock through the style chain. "More Colors…" on the background picker now opens the palette window targeting the background colour — it used to just close the picker. Co-Authored-By: Claude Fable 5 --- src/app/mod.rs | 2 + src/app/properties.rs | 4 ++ src/app/update/style.rs | 3 + src/entities/hatch.rs | 118 +++++++++++++++++++++++++++++++++++----- src/scene/entity.rs | 38 ++++++++++++- src/ui/properties.rs | 5 +- 6 files changed, 154 insertions(+), 16 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 62a055512..e22bfd4ff 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -831,6 +831,8 @@ pub enum ColorPickTarget { Table(u8, &'static str), /// Selected entities' colour (left properties panel). Properties, + /// Selected entities' background colour (hatch / MTEXT background row). + PropertiesBg, /// Current creation colour (ribbon). Ribbon, /// A layer's colour, by panel row index. diff --git a/src/app/properties.rs b/src/app/properties.rs index 65ddf3c4a..845a39022 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -1087,6 +1087,10 @@ impl OpenCADStudio { pub(super) fn invalidate_property_targets(&mut self, i: usize, handles: &[Handle]) { for &handle in handles { self.tabs[i].scene.mark_entity_dirty(handle); + // Hatch / SOLID fills render from prebuilt cached models; rebuild + // them or pattern edits (scale, background, …) stay invisible + // (#415). + self.tabs[i].scene.refresh_fill_model(handle); } // Solid (ACIS) meshes bake their colour into the mesh, so a colour / // layer change needs an explicit recolour — re-tessellating wires diff --git a/src/app/update/style.rs b/src/app/update/style.rs index 0061d2657..ed98de514 100644 --- a/src/app/update/style.rs +++ b/src/app/update/style.rs @@ -981,6 +981,9 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task { Some(crate::app::ColorPickTarget::Properties) => { Some(Message::PropColorChanged(color)) } + Some(crate::app::ColorPickTarget::PropertiesBg) => { + Some(Message::PropBgColorChanged(color)) + } Some(crate::app::ColorPickTarget::MText) => { Some(Message::MTextColorChanged(color)) } diff --git a/src/entities/hatch.rs b/src/entities/hatch.rs index 178266202..decc6c309 100644 --- a/src/entities/hatch.rs +++ b/src/entities/hatch.rs @@ -109,19 +109,32 @@ pub fn background_color(h: &Hatch) -> Option { _ => None, })?; match (raw >> 24) & 0xFF { - 0xC2 | 0xC3 => Some(acadrust::types::Color::Rgb { + // Colour-method byte: C0 = ByLayer, C1 = ByBlock, C2 = true colour, + // C3 = ACI index. + 0xC0 => Some(acadrust::types::Color::ByLayer), + 0xC1 => Some(acadrust::types::Color::ByBlock), + 0xC2 => Some(acadrust::types::Color::Rgb { r: ((raw >> 16) & 0xFF) as u8, g: ((raw >> 8) & 0xFF) as u8, b: (raw & 0xFF) as u8, }), + 0xC3 => Some(acadrust::types::Color::Index((raw & 0xFF) as u8)), _ => None, } } /// Pack an RGB colour into a `HATCHBACKGROUNDCOLOR` group-1071 true-colour word. pub fn pack_background_color(c: &acadrust::types::Color) -> i32 { - let (r, g, b) = c.rgb().unwrap_or((255, 255, 255)); - (0xC2000000u32 | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)) as i32 + use acadrust::types::Color; + (match c { + Color::ByLayer => 0xC0000000u32, + Color::ByBlock => 0xC1000000u32, + Color::Index(i) => 0xC3000000u32 | (*i as u8 as u32), + _ => { + let (r, g, b) = c.rgb().unwrap_or((255, 255, 255)); + 0xC2000000u32 | ((r as u32) << 16) | ((g as u32) << 8) | (b as u32) + } + }) as i32 } /// Replace (or insert) the hatch's `HATCHBACKGROUNDCOLOR` extended-data record, @@ -146,6 +159,25 @@ pub fn set_background_color(h: &mut Hatch, c: &acadrust::types::Color) { h.common.extended_data.add_record(rec); } +/// Remove the hatch's `HATCHBACKGROUNDCOLOR` extended-data record (background +/// off), preserving every other application's records. +pub fn clear_background_color(h: &mut Hatch) { + use acadrust::xdata::ExtendedDataRecord; + const APP: &str = "HATCHBACKGROUNDCOLOR"; + let kept: Vec = h + .common + .extended_data + .records() + .iter() + .filter(|r| r.application_name != APP) + .cloned() + .collect(); + h.common.extended_data.clear(); + for r in kept { + h.common.extended_data.add_record(r); + } +} + fn properties(h: &Hatch) -> Vec { let pattern_type = match h.pattern_type { acadrust::entities::HatchPatternType::Predefined => "Predefined", @@ -162,13 +194,14 @@ fn properties(h: &Hatch) -> Vec { let default_col = acadrust::types::Color::Index(7); let grad_c1 = g.colors.first().map(|e| e.color).unwrap_or(default_col); let grad_c2 = g.colors.get(1).map(|e| e.color).unwrap_or(default_col); + let bg_on = background_color(h).is_some(); let bg_col = background_color(h).unwrap_or(default_col); 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" }; - return vec![ + let mut sections = vec![ PropSection { title: "Pattern".into(), props: vec![ @@ -214,15 +247,29 @@ fn properties(h: &Hatch) -> Vec { ro("Annotative", "annotative", String::new()), ro("Island detection style", "style", style), Property { - label: "Background color".into(), - field: "background_color", - value: PropValue::ColorChoice(bg_col), - }, + label: "Background".into(), + field: "bg_enabled", + value: PropValue::BoolToggle { + field: "bg_enabled", + value: bg_on, + }, + }, ], }, ]; + if bg_on { + if let Some(sec) = sections.last_mut() { + sec.props.push(Property { + label: "Background color".into(), + field: "background_color", + value: PropValue::ColorChoice(bg_col), + }); + } + } + return sections; } + // ── Hatch (pattern / solid) ──────────────────────────────────────────── // "Type" = pattern definition source (Predefined / User Defined / Custom). let type_row = Property { @@ -279,7 +326,7 @@ fn properties(h: &Hatch) -> Vec { .map(|l| (l.base_point.x, l.base_point.y)) .unwrap_or((0.0, 0.0)); - vec![ + let mut sections = vec![ PropSection { title: "Pattern".into(), props: vec![ @@ -296,9 +343,12 @@ fn properties(h: &Hatch) -> Vec { associative_row, island_row, Property { - label: "Background color".into(), - field: "background_color", - value: PropValue::ColorChoice(bg_col), + label: "Background".into(), + field: "bg_enabled", + value: PropValue::BoolToggle { + field: "bg_enabled", + value: bg_on, + }, }, ], }, @@ -310,7 +360,17 @@ fn properties(h: &Hatch) -> Vec { ro("Cumulative area", "cumulative_area", format!("{:.4}", area)), ], }, - ] + ]; + if bg_on { + if let Some(sec) = sections.first_mut() { + sec.props.push(Property { + label: "Background color".into(), + field: "background_color", + value: PropValue::ColorChoice(bg_col), + }); + } + } + sections } fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) { @@ -318,6 +378,17 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) { // Non-numeric fields (bool toggles / enum choices) — handled before the // f64 parse below, which would otherwise reject their string values. match field { + // Background on/off (#415): off removes the HATCHBACKGROUNDCOLOR + // record entirely (no background), on seeds a white one for the + // colour picker to change. + "bg_enabled" => { + if background_color(h).is_some() { + clear_background_color(h); + } else { + set_background_color(h, &acadrust::types::Color::ByLayer); + } + return; + } "double" => { h.is_double = if value == "toggle" { !h.is_double @@ -364,7 +435,26 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) { match field { "pattern_angle" if h.gradient_color.enabled => h.gradient_color.angle = v.to_radians(), "pattern_angle" => h.pattern_angle = v.to_radians(), - "pattern_scale" if v > 0.0 => h.pattern_scale = v, + // The stored pattern lines are the FINAL rendered geometry (offsets, + // base points and dashes in world units — see the prebaked path in + // hatch_model_from_dxf), so changing the scale must rescale them by + // the ratio or the edit has no visible effect (#415). + "pattern_scale" if v > 0.0 => { + let old = h.pattern_scale; + if old > 1e-12 { + let k = v / old; + for line in h.pattern.lines.iter_mut() { + line.base_point.x *= k; + line.base_point.y *= k; + line.offset.x *= k; + line.offset.y *= k; + for d in line.dash_lengths.iter_mut() { + *d *= k; + } + } + } + h.pattern_scale = v; + } // Scale every pattern line's offset so the first line's spacing = v, // preserving the relative spacing between lines. "spacing" if v > 0.0 => { diff --git a/src/scene/entity.rs b/src/scene/entity.rs index d0b84d9ef..0499c82ca 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -708,7 +708,22 @@ impl Scene { if let Some(bg) = crate::entities::hatch::background_color(dxf) { let mut b = m.clone(); b.pattern = model::hatch_model::HatchPattern::Solid; - b.color = crate::scene::convert::tess_util::aci_to_rgba(&bg); + // ByLayer / ByBlock backgrounds resolve through the + // normal style chain instead of the raw ACI table + // (#415). + b.color = match bg { + acadrust::types::Color::ByLayer => { + crate::scene::view::render::layer_render_style( + &self.document, + &dxf.common.layer, + ) + .color + } + acadrust::types::Color::ByBlock => self.render_style(e).0, + other => { + crate::scene::convert::tess_util::aci_to_rgba(&other) + } + }; b.name = "SOLID".into(); backdrop = Some(b); } @@ -1585,6 +1600,27 @@ impl Scene { self.bump_geometry(); } + /// Rebuild the cached fill model (hatch / DXF SOLID) for `handle` after + /// its document entity was edited in place. The fill models are prebuilt + /// at load, so a pattern-scale / background / boundary edit stays + /// invisible until the cached model is refreshed (#415). + pub fn refresh_fill_model(&mut self, handle: Handle) { + let new_model = match self.document.get_entity(handle) { + Some(EntityType::Hatch(dxf)) => { + let color = convert::tess_util::aci_to_rgba(&dxf.common.color); + Self::hatch_model_from_dxf(dxf, color) + } + Some(EntityType::Solid(s)) => { + let color = convert::tess_util::aci_to_rgba(&s.common.color); + Some(Self::solid_hatch_model(s, color)) + } + _ => None, + }; + if let Some(model) = new_model { + self.hatches.insert(handle, model); + } + } + pub fn populate_hatches_from_document(&mut self) { self.hatches.clear(); diff --git a/src/ui/properties.rs b/src/ui/properties.rs index c32dd03f7..68da12c7b 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -430,7 +430,10 @@ impl PropertiesPanel { }, Message::PropBgColorChanged, Message::PropBgColorPickerToggle, - Message::PropBgColorPickerToggle, + // "More Colors…" opens the full palette window targeting the + // background colour — this used to just close the picker + // (#415). + Message::OpenColorWindow(crate::app::ColorPickTarget::PropertiesBg), ); return prop_row_widget(label, selector); }