feat(properties): show & edit block attributes in panel

Selecting an INSERT with attributes now adds an editable "Attributes"
section to the Properties panel — each tag is a row whose value can be
changed inline (Enter commits, undo works, the attribute text repaints).
Previously attribute values could only be reached through ATTEDIT, which
didn't let them be changed (#192).

Attribute tags are runtime strings, so they can't ride the geometry
edit path whose field key is `&'static str`; a dedicated PropValue::
AttrText variant plus PropAttrInput/PropAttrCommit messages carry the
tag. Values are stored verbatim (no expression evaluation, since
attribute text is free-form).

Also guard the panel's in-progress edit buffer: it now only carries
across a rebuild when the selection is unchanged (a commit-triggered
refresh), so a typed-but-uncommitted value can't display or commit onto
a different entity — e.g. two title blocks sharing a REV1 tag. This
closes a latent stale-buffer leak that affected geometry fields too.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-01 07:42:50 +03:00
1 parent 3da81571a1
commit 5f2e920b75
7 files changed
+133 -1

No files matched your search

+5
View File
@@ -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.
+37 -1
View File
@@ -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<acadrust::Handle> = 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
};
+31
View File
@@ -1173,4 +1173,35 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
}
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<Message> {
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()
}
}
+10
View File
@@ -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 =