diff --git a/src/app/mod.rs b/src/app/mod.rs index 645a64104..8919738d5 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1385,6 +1385,11 @@ pub enum Message { }, /// User committed a geometry/common field edit (Enter pressed). PropGeomCommit(&'static str), + /// User is typing in a block-attribute value field (live buffer update), + /// keyed by the attribute tag. + PropAttrInput { tag: String, value: String }, + /// User committed a block-attribute value edit (Enter pressed). + PropAttrCommit(String), /// Toggle the inline color picker dropdown open/closed. PropColorPickerToggle, /// Toggle the full ACI colour palette expansion. diff --git a/src/app/properties.rs b/src/app/properties.rs index 8642b0137..ca4e60cde 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -15,6 +15,9 @@ impl OpenCADStudio { // closes, matching the deselect / reselect / click-away expectation. let color_palette_open = self.tabs[i].properties.color_palette_open; let edit_buf = std::mem::take(&mut self.tabs[i].properties.edit_buf); + // Which entities the previous panel was built for — an uncommitted + // edit buffer only survives a rebuild for the *same* selection. + let prev_handles = std::mem::take(&mut self.tabs[i].properties.source_handles); let selected_group = self.tabs[i].properties.selected_group.clone(); // Seed the per-thread unit context from the document header so the @@ -207,6 +210,30 @@ impl OpenCADStudio { } } + // Block attributes: expose each attribute tag as an + // editable value row so a block reference's attributes can + // be read and changed directly from the panel. + if let acadrust::EntityType::Insert(ins) = entity { + if !ins.attributes.is_empty() { + let props = ins + .attributes + .iter() + .map(|a| crate::scene::model::object::Property { + label: a.tag.clone(), + field: "attr", + value: crate::scene::model::object::PropValue::AttrText { + tag: a.tag.clone(), + value: a.get_value().to_string(), + }, + }) + .collect(); + sections.push(crate::scene::model::object::PropSection { + title: "Attributes".to_string(), + props, + }); + } + } + if !group_names.is_empty() { let label = group_names.join(", "); if let Some(general) = sections.first_mut() { @@ -310,7 +337,16 @@ impl OpenCADStudio { } }; panel.color_palette_open = color_palette_open; - panel.edit_buf = edit_buf; + let new_handles: Vec = selected.iter().map(|(h, _)| *h).collect(); + // Carry the in-progress edits only when the selection is unchanged + // (a commit-triggered rebuild); a genuine selection change starts + // with a clean buffer so no stale value leaks onto the new entity. + panel.edit_buf = if prev_handles == new_handles { + edit_buf + } else { + Default::default() + }; + panel.source_handles = new_handles; panel }; diff --git a/src/app/update/command.rs b/src/app/update/command.rs index bc0476904..2966a197c 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1173,4 +1173,35 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } Task::none() } + + /// Commit an edited block-attribute value from the Properties panel. + /// Writes the new value into the matching attribute (by tag) of every + /// selected INSERT, then re-tessellates so the attribute text repaints. + /// The value is stored verbatim (attribute values are free-form text, so + /// it is not run through the expression evaluator like numeric fields). + pub(super) fn on_prop_attr_commit(&mut self, tag: String) -> Task { + let i = self.active_tab; + let key = crate::ui::properties::attr_edit_key(&tag); + let handles = self.property_target_handles(i); + if handles.is_empty() { + return Task::none(); + } + let Some(val) = self.tabs[i].properties.edit_buf.remove(&key) else { + return Task::none(); + }; + self.push_undo_snapshot(i, "ATTEDIT"); + for &handle in &handles { + if let Some(acadrust::EntityType::Insert(ins)) = + self.tabs[i].scene.document.get_entity_mut(handle) + { + if let Some(attr) = ins.attributes.iter_mut().find(|a| a.tag == tag) { + attr.set_value(val.clone()); + } + } + } + self.invalidate_property_targets(i, &handles); + self.tabs[i].dirty = true; + self.refresh_properties(); + Task::none() + } } diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 35a4171f5..7738192bd 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -2001,6 +2001,16 @@ impl OpenCADStudio { Message::PropGeomCommit(field) => self.on_prop_geom_commit(field), + Message::PropAttrInput { tag, value } => { + self.tabs[self.active_tab] + .properties + .edit_buf + .insert(crate::ui::properties::attr_edit_key(&tag), value); + Task::none() + } + + Message::PropAttrCommit(tag) => self.on_prop_attr_commit(tag), + Message::PropColorPickerToggle => { let i = self.active_tab; self.tabs[i].properties.color_picker_open = diff --git a/src/scene/model/object.rs b/src/scene/model/object.rs index 7d254e70d..ad420e762 100644 --- a/src/scene/model/object.rs +++ b/src/scene/model/object.rs @@ -31,6 +31,10 @@ pub enum PropValue { BoolToggle { field: &'static str, value: bool }, /// Hatch pattern name — rendered as a combo_box from the catalog. HatchPatternChoice(String), + /// Block attribute value keyed by its (dynamic, runtime) tag — rendered as + /// an editable text_input. Unlike the other rows the routing key is the + /// tag carried here, not the row's `&'static str` field. + AttrText { tag: String, value: String }, } /// A single property row in the Properties panel. diff --git a/src/scene/selection.rs b/src/scene/selection.rs index 2e275027f..270fcc5e4 100644 --- a/src/scene/selection.rs +++ b/src/scene/selection.rs @@ -285,6 +285,7 @@ impl Scene { PropValue::LinetypeChoice(s) => s, PropValue::HatchPatternChoice(s) => s, PropValue::BoolToggle { value, .. } => value.to_string(), + PropValue::AttrText { value, .. } => value, PropValue::ColorVaries | PropValue::LwVaries => return None, }) } diff --git a/src/ui/properties.rs b/src/ui/properties.rs index ff936d0e0..c37efebf1 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -109,6 +109,13 @@ pub fn lw_options() -> Vec { .collect() } +/// Edit-buffer key for a block attribute value, keyed by its tag. Kept in one +/// place so the live-input handler and the row renderer agree. The `\x01` +/// sentinel guarantees no collision with a geometry field's `&'static str` key. +pub fn attr_edit_key(tag: &str) -> String { + format!("\x01attr\x01{tag}") +} + // ── PropertiesPanel ─────────────────────────────────────────────────────── #[derive(Clone)] @@ -127,6 +134,13 @@ pub struct PropertiesPanel { pub hatch_pattern_combo: combo_box::State, /// In-progress text edits keyed by `field` name. pub edit_buf: HashMap, + /// Entity handles this panel was built for. `refresh_properties` compares + /// it against the new selection to decide whether an uncommitted `edit_buf` + /// may carry over: same selection → keep (survive a commit-triggered + /// rebuild); different selection → drop, so a stale value can't display or + /// commit onto a different entity (e.g. two title blocks sharing a `REV1` + /// tag). + pub source_handles: Vec, /// Whether the quick color picker dropdown is open. pub color_picker_open: bool, /// Whether the full 16×16 ACI palette is expanded inside the color picker. @@ -148,6 +162,7 @@ impl Default for PropertiesPanel { linetype_combo: combo_box::State::new(vec![]), hatch_pattern_combo: combo_box::State::new(crate::scene::model::hatch_patterns::names()), edit_buf: HashMap::default(), + source_handles: vec![], color_picker_open: false, color_palette_open: false, } @@ -341,6 +356,9 @@ impl PropertiesPanel { PropValue::HatchPatternChoice(current) => { col = col.push(self.render_hatch_pattern_row(&prop.label, current)); } + PropValue::AttrText { tag, value } => { + col = col.push(self.render_attr_row(tag, value)); + } } } @@ -586,6 +604,33 @@ impl PropertiesPanel { prop_row_widget(label, ti.into()) } + /// One editable row for a block attribute: the tag is the row label and the + /// text box edits the value. Routing rides the tag (a runtime string), so + /// this uses the dedicated `PropAttr*` messages instead of the geometry + /// path whose field key is `&'static str`. The row label is the tag itself. + fn render_attr_row<'a>(&'a self, tag: &'a str, entity_val: &'a str) -> Element<'a, Message> { + let key = attr_edit_key(tag); + let display = self + .edit_buf + .get(&key) + .map(|s| s.as_str()) + .unwrap_or(entity_val); + + let tag_for_input = tag.to_string(); + let ti = text_input("", display) + .on_input(move |v| Message::PropAttrInput { + tag: tag_for_input.clone(), + value: v, + }) + .on_submit(Message::PropAttrCommit(tag.to_string())) + .size(FONT_SZ) + .style(text_input_style) + .padding([3, 6]) + .width(Length::Fill); + + prop_row_widget(tag, ti.into()) + } + fn render_hatch_pattern_row<'a>( &'a self, label: &'a str,