diff --git a/src/app/mod.rs b/src/app/mod.rs index e742d3637..afc432fde 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1558,6 +1558,12 @@ pub enum Message { /// User picked an MTEXT background fill colour — sets `background_color` /// and switches the background mode to Fill so it shows immediately. PropBgColorChanged(AcadColor), + /// Toggle the generic per-field colour picker (e.g. a hatch gradient colour) + /// identified by its property field name. + PropColorFieldToggle(String), + /// User picked a colour for a generic per-field colour row (hatch gradient + /// `Color 1` / `Color 2`), routed by the field name. + PropColorFieldChanged { field: String, color: AcadColor }, /// Collapse the inline color picker dropdown. Fired when another /// properties-panel dropdown (a combo_box) opens, so at most one panel /// dropdown is open at a time and they can't overlap. (#235) diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 87cc226ea..61828ff9e 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -2279,13 +2279,18 @@ impl OpenCADStudio { if !handles.is_empty() { self.push_undo_snapshot(i, "CHPROP"); for &handle in &handles { - if let Some(acadrust::EntityType::MText(m)) = - self.tabs[i].scene.document.get_entity_mut(handle) - { - m.background_color = color.clone(); - // Picking a colour turns the background on in Fill - // mode (specific colour), preserving the frame bit. - m.background_fill_flags = (m.background_fill_flags & !0x02) | 0x01; + match self.tabs[i].scene.document.get_entity_mut(handle) { + Some(acadrust::EntityType::MText(m)) => { + m.background_color = color.clone(); + // Picking a colour turns the background on in Fill + // mode (specific colour), preserving the frame bit. + m.background_fill_flags = + (m.background_fill_flags & !0x02) | 0x01; + } + Some(acadrust::EntityType::Hatch(h)) => { + crate::entities::hatch::set_background_color(h, &color); + } + _ => {} } } self.invalidate_property_targets(i, &handles); @@ -2296,6 +2301,54 @@ impl OpenCADStudio { Task::none() } + Message::PropColorFieldToggle(field) => { + let i = self.active_tab; + let p = &mut self.tabs[i].properties; + p.open_color_field = if p.open_color_field.as_deref() == Some(field.as_str()) { + None + } else { + Some(field) + }; + Task::none() + } + + Message::PropColorFieldChanged { field, color } => { + let i = self.active_tab; + let handles = self.property_target_handles(i); + if !handles.is_empty() { + self.push_undo_snapshot(i, "CHPROP"); + let idx = if field == "gradient_color_2" { 1 } else { 0 }; + for &handle in &handles { + if let Some(acadrust::EntityType::Hatch(h)) = + self.tabs[i].scene.document.get_entity_mut(handle) + { + while h.gradient_color.colors.len() <= idx { + let value = if h.gradient_color.colors.is_empty() { + 0.0 + } else { + 1.0 + }; + h.gradient_color.colors.push( + acadrust::entities::hatch::GradientColorEntry { + value, + color: acadrust::types::Color::Index(7), + }, + ); + } + h.gradient_color.colors[idx].color = color.clone(); + } + } + // Rebuild hatch seeds so the gradient fill picks up the new + // colour (synced_hatch_models only patches the main colour). + self.tabs[i].scene.populate_hatches_from_document(); + self.invalidate_property_targets(i, &handles); + self.tabs[i].properties.open_color_field = None; + self.tabs[i].dirty = true; + self.refresh_properties(); + } + Task::none() + } + Message::PropColorPickerClose => { let i = self.active_tab; self.tabs[i].properties.color_picker_open = false; diff --git a/src/entities/hatch.rs b/src/entities/hatch.rs index d2b6b00ad..178266202 100644 --- a/src/entities/hatch.rs +++ b/src/entities/hatch.rs @@ -99,9 +99,51 @@ fn boundary_centroid(h: &Hatch) -> Option<(f64, f64)> { (n > 0.0).then(|| (sx / n, sy / n)) } -/// Format a color for a read-only row (`ByLayer` / `ByBlock` / index / RGB). -fn color_str(c: acadrust::types::Color) -> String { - c.to_string() +/// Read the hatch background colour from its `HATCHBACKGROUNDCOLOR` extended +/// data (group 1071, packed true colour: high byte = colour method, low three = +/// RGB). `None` when unset or not a true-colour value. +pub fn background_color(h: &Hatch) -> Option { + let rec = h.common.extended_data.get_record("HATCHBACKGROUNDCOLOR")?; + let raw = rec.values.iter().find_map(|v| match v { + acadrust::xdata::XDataValue::Integer32(n) => Some(*n as u32), + _ => None, + })?; + match (raw >> 24) & 0xFF { + 0xC2 | 0xC3 => Some(acadrust::types::Color::Rgb { + r: ((raw >> 16) & 0xFF) as u8, + g: ((raw >> 8) & 0xFF) as u8, + b: (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 +} + +/// Replace (or insert) the hatch's `HATCHBACKGROUNDCOLOR` extended-data record, +/// preserving every other application's records. +pub fn set_background_color(h: &mut Hatch, c: &acadrust::types::Color) { + use acadrust::xdata::{ExtendedDataRecord, XDataValue}; + 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); + } + let mut rec = ExtendedDataRecord::new(APP); + rec.add_value(XDataValue::Integer32(pack_background_color(c))); + h.common.extended_data.add_record(rec); } fn properties(h: &Hatch) -> Vec { @@ -117,8 +159,10 @@ fn properties(h: &Hatch) -> Vec { }; let area = boundary_area(h); let g = &h.gradient_color; - let grad_c1 = g.colors.first().map(|e| color_str(e.color)).unwrap_or_default(); - let grad_c2 = g.colors.get(1).map(|e| color_str(e.color)).unwrap_or_default(); + 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_col = background_color(h).unwrap_or(default_col); if g.enabled { // ── Gradient fill ────────────────────────────────────────────────── @@ -128,10 +172,25 @@ fn properties(h: &Hatch) -> Vec { PropSection { title: "Pattern".into(), props: vec![ - ro("Type", "fill_type", grad_type), + Property { + label: "Type".into(), + field: "fill_type", + value: PropValue::Choice { + selected: grad_type.to_string(), + options: vec!["Two color".into(), "One color".into()], + }, + }, ro("Gradient name", "gradient_name", g.name.clone()), - ro("Color 1", "gradient_color_1", grad_c1), - ro("Color 2", "gradient_color_2", grad_c2), + Property { + label: "Color 1".into(), + field: "gradient_color_1", + value: PropValue::ColorChoice(grad_c1), + }, + Property { + label: "Color 2".into(), + field: "gradient_color_2", + value: PropValue::ColorChoice(grad_c2), + }, edit("Angle", "pattern_angle", g.angle.to_degrees()), ro("Centered", "gradient_centered", centered), ], @@ -140,8 +199,6 @@ fn properties(h: &Hatch) -> Vec { title: "Geometry".into(), props: vec![ edit("Elevation", "elevation", h.elevation), - ro("Origin X", "origin_x", String::new()), - ro("Origin Y", "origin_y", String::new()), ro("Area", "area", format!("{:.4}", area)), ro("Cumulative area", "cumulative_area", format!("{:.4}", area)), ], @@ -156,7 +213,11 @@ fn properties(h: &Hatch) -> Vec { ), ro("Annotative", "annotative", String::new()), ro("Island detection style", "style", style), - ro("Background color", "background_color", String::new()), + Property { + label: "Background color".into(), + field: "background_color", + value: PropValue::ColorChoice(bg_col), + }, ], }, ]; @@ -234,12 +295,11 @@ fn properties(h: &Hatch) -> Vec { double_row, associative_row, island_row, - ro("Background color", "background_color", String::new()), - // ── Gradient info (read-only) ────────────────────────────── - ro("Gradient colors", "gradient_colors", g.colors.len().to_string()), - ro("Gradient tint", "gradient_tint", format!("{:.4}", g.color_tint)), - ro("Gradient color 1", "gradient_color_1", grad_c1), - ro("Gradient color 2", "gradient_color_2", grad_c2), + Property { + label: "Background color".into(), + field: "background_color", + value: PropValue::ColorChoice(bg_col), + }, ], }, PropSection { @@ -292,6 +352,10 @@ fn apply_geom_prop(h: &mut Hatch, field: &str, value: &str) { }; return; } + "fill_type" => { + h.gradient_color.is_single_color = value == "One color"; + return; + } _ => {} } let Some(v) = parse_f64(value) else { diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 9c18a67d8..2d8ec6b68 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -483,12 +483,23 @@ impl Scene { self.model_space_block_handle(), ) }) - .map(|(&handle, model)| { + .flat_map(|(&handle, model)| { let entity = self.document.get_entity(handle); let mut m = model.clone(); + // Optional solid backdrop drawn behind the pattern/gradient when + // the hatch carries a HATCHBACKGROUNDCOLOR. Same draw_depth + + // emitted first so LessEqual layering keeps it underneath. + let mut backdrop: Option = None; if let Some(e) = entity { m.color = self.render_style(e).0; if let EntityType::Hatch(dxf) = e { + 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); + b.name = "SOLID".into(); + backdrop = Some(b); + } match &mut m.pattern { // Pattern built from the hatch's own stored lines is // already final (scale 1 / angle 0) — don't re-apply @@ -516,8 +527,12 @@ impl Scene { if self.selected.contains(&handle) { m.color = [0.15, 0.55, 1.00, m.color[3]]; } - m.draw_depth = depth_map.get(&handle.value()).copied().unwrap_or(0.0); - m + let d = depth_map.get(&handle.value()).copied().unwrap_or(0.0); + m.draw_depth = d; + if let Some(b) = &mut backdrop { + b.draw_depth = d; + } + backdrop.into_iter().chain(std::iter::once(m)) }) .collect(); diff --git a/src/ui/properties.rs b/src/ui/properties.rs index a215c663b..a372bb308 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -149,6 +149,9 @@ pub struct PropertiesPanel { /// from `color_picker_open` so the entity colour and the background colour /// pickers are independent. pub bg_color_picker_open: bool, + /// Field name of the generic per-field colour picker currently open (e.g. a + /// hatch gradient colour), or `None`. Keeps each field's picker independent. + pub open_color_field: Option, /// Which vertex a multi-vertex entity (polyline) is focused on — driven by /// the Current Vertex ◀ / ▶ stepper. Reset to 0 when the selection changes. pub prop_vertex: usize, @@ -173,6 +176,7 @@ impl Default for PropertiesPanel { color_picker_open: false, color_palette_open: false, bg_color_picker_open: false, + open_color_field: None, prop_vertex: 0, } } @@ -430,6 +434,27 @@ impl PropertiesPanel { ); return prop_row_widget(label, selector); } + // Generic per-field colour picker (hatch gradient colours) — routes to + // the field, not the entity's main colour. + if field == "gradient_color_1" || field == "gradient_color_2" { + let open = self.open_color_field.as_deref() == Some(field); + let fsel = field.to_string(); + let selector = crate::ui::color_select::color_selector( + color, + open, + crate::ui::color_select::ColorExtras { + by_layer: false, + by_block: false, + }, + move |c| Message::PropColorFieldChanged { + field: fsel.clone(), + color: c, + }, + Message::PropColorFieldToggle(field.to_string()), + Message::PropColorFieldToggle(field.to_string()), + ); + return prop_row_widget(label, selector); + } let selector = crate::ui::color_select::color_selector( color, self.color_picker_open,