fix(blocks): enforce constant attributes in ATTEDIT and restore ATTDEFs on explode

Enforce constant attribute flag in ATTEDIT dialog by presenting constant attributes as disabled and ignoring modifications. When exploding an insert, restore block-definition attribute definitions as transformed ATTDEF entities while dropping instance attribute values.
This commit is contained in:
Karim Jerbi committed 2026-10-13 11:10:11 +01:00
1 parent df22824fbe
commit 59bb6faabd
5 files changed
+393 -33

No files matched your search

+172
View File
@@ -1347,3 +1347,175 @@ fn locked_purge_skips_a_block_with_an_open_bedit_session() {
assert!(record_exists(&app, &name));
let _ = app.dispatch_command("BEDIT_DISCARD");
}
#[test]
fn locked_attedit_attribute_flags_cases() {
use crate::ui::window::attribute_editor::AttrRow;
use codec::entities::{AttributeDefinition, AttributeEntity};
use codec::tables::BlockRecord;
// Verify flag behavior in ATTEDIT conversion and apply:
// 1. Normal attribute: editable, modifies value
let mut normal_entity =
AttributeEntity::new("NORM_TAG".to_string(), "original_norm".to_string());
let normal_row = AttrRow {
tag: "NORM_TAG".to_string(),
prompt: "Enter norm:".to_string(),
value: "edited_norm".to_string(),
text_style: "STANDARD".to_string(),
height: "2.5".to_string(),
rotation: "0".to_string(),
width_factor: "1".to_string(),
oblique: "0".to_string(),
h_align: codec::entities::HorizontalAlignment::Left,
v_align: codec::entities::VerticalAlignment::Baseline,
backwards: false,
upside_down: false,
layer: "0".to_string(),
color: codec::types::Color::ByLayer,
linetype: "ByLayer".to_string(),
line_weight: codec::types::LineWeight::ByLayer,
is_constant: false,
};
assert!(!normal_row.is_constant);
// 2. Constant attribute: is_constant is true, apply ignores value edit
let mut const_entity =
AttributeEntity::new("CONST_TAG".to_string(), "original_const".to_string());
const_entity.flags.constant = true;
let mut const_row = normal_row.clone();
const_row.tag = "CONST_TAG".to_string();
const_row.value = "attempted_edit".to_string();
const_row.is_constant = true;
// 3. Invisible attribute: editable in ATTEDIT (flags.invisible is true, but is_constant is false)
let mut invis_entity =
AttributeEntity::new("INVIS_TAG".to_string(), "original_invis".to_string());
invis_entity.flags.invisible = true;
let mut invis_row = normal_row.clone();
invis_row.tag = "INVIS_TAG".to_string();
invis_row.value = "edited_invis".to_string();
invis_row.is_constant = false;
// 4. Preset attribute: editable in ATTEDIT (preset only affects initial insertion prompt)
let mut preset_entity =
AttributeEntity::new("PRESET_TAG".to_string(), "original_preset".to_string());
preset_entity.flags.preset = true;
let mut preset_row = normal_row.clone();
preset_row.tag = "PRESET_TAG".to_string();
preset_row.value = "edited_preset".to_string();
preset_row.is_constant = false;
// Verify metadata mapping from CadDocument
let mut doc = codec::CadDocument::new();
let next = doc.next_handle();
let br_h = Handle::new(next);
let mut br = BlockRecord::new("FLAG_BLOCK");
br.handle = br_h;
br.block_entity_handle = Handle::new(next + 1);
br.block_end_handle = Handle::new(next + 2);
let mut ad_norm = AttributeDefinition::new(
"NORM_TAG".to_string(),
"Enter norm:".to_string(),
"def".to_string(),
);
let mut ad_const = AttributeDefinition::new(
"CONST_TAG".to_string(),
"Enter const:".to_string(),
"def".to_string(),
);
ad_const.flags.constant = true;
let mut ad_invis = AttributeDefinition::new(
"INVIS_TAG".to_string(),
"Enter invis:".to_string(),
"def".to_string(),
);
ad_invis.flags.invisible = true;
let mut ad_preset = AttributeDefinition::new(
"PRESET_TAG".to_string(),
"Enter preset:".to_string(),
"def".to_string(),
);
ad_preset.flags.preset = true;
let h1 = Handle::new(next + 3);
let h2 = Handle::new(next + 4);
let h3 = Handle::new(next + 5);
let h4 = Handle::new(next + 6);
ad_norm.common.handle = h1;
ad_const.common.handle = h2;
ad_invis.common.handle = h3;
ad_preset.common.handle = h4;
br.entity_handles = vec![h1, h2, h3, h4];
doc.block_records.add(br).unwrap();
doc.add_entity(EntityType::AttributeDefinition(ad_norm))
.unwrap();
doc.add_entity(EntityType::AttributeDefinition(ad_const))
.unwrap();
doc.add_entity(EntityType::AttributeDefinition(ad_invis))
.unwrap();
doc.add_entity(EntityType::AttributeDefinition(ad_preset))
.unwrap();
let meta = crate::app::update::command::block_attr_meta(&doc, "FLAG_BLOCK");
assert!(!meta.get("NORM_TAG").unwrap().constant);
assert!(meta.get("CONST_TAG").unwrap().constant);
assert!(!meta.get("INVIS_TAG").unwrap().constant);
assert!(!meta.get("PRESET_TAG").unwrap().constant);
// Test conversion from AttributeEntity to AttrRow
let r_norm = crate::app::update::command::attr_row_from_entity(&normal_entity, &meta);
assert!(!r_norm.is_constant);
assert_eq!(r_norm.prompt, "Enter norm:");
let r_const = crate::app::update::command::attr_row_from_entity(&const_entity, &meta);
assert!(r_const.is_constant);
assert_eq!(r_const.prompt, "Enter const:");
let r_invis = crate::app::update::command::attr_row_from_entity(&invis_entity, &meta);
assert!(!r_invis.is_constant);
assert_eq!(r_invis.prompt, "Enter invis:");
let r_preset = crate::app::update::command::attr_row_from_entity(&preset_entity, &meta);
assert!(!r_preset.is_constant);
assert_eq!(r_preset.prompt, "Enter preset:");
// Test apply_attr_row behavior on constant vs non-constant
// Normal: applies
let mut edited_norm_row = r_norm.clone();
edited_norm_row.value = "edited_norm".to_string();
assert!(crate::app::update::command::apply_attr_row(
&mut normal_entity,
&edited_norm_row
));
assert_eq!(normal_entity.get_value(), "edited_norm");
// Constant: ignores value change!
let mut edited_const_row = r_const.clone();
edited_const_row.value = "attempted_edit".to_string();
assert!(!crate::app::update::command::apply_attr_row(
&mut const_entity,
&edited_const_row
));
assert_eq!(const_entity.get_value(), "original_const");
// Invisible: applies
let mut edited_invis_row = r_invis.clone();
edited_invis_row.value = "edited_invis".to_string();
assert!(crate::app::update::command::apply_attr_row(
&mut invis_entity,
&edited_invis_row
));
assert_eq!(invis_entity.get_value(), "edited_invis");
// Preset: applies
let mut edited_preset_row = r_preset.clone();
edited_preset_row.value = "edited_preset".to_string();
assert!(crate::app::update::command::apply_attr_row(
&mut preset_entity,
&edited_preset_row
));
assert_eq!(preset_entity.get_value(), "edited_preset");
}
+40 -26
View File
@@ -4265,11 +4265,11 @@ impl OpenCADStudio {
Some(codec::EntityType::Insert(ins)) if !ins.attributes.is_empty() => {
// The prompt text lives on the block's ATTDEFs, not on the
// attribute instances — map tag → prompt from the definition.
let prompts = block_attr_prompts(doc, &ins.block_name);
let meta = block_attr_meta(doc, &ins.block_name);
let rows = ins
.attributes
.iter()
.map(|a| attr_row_from_entity(a, &prompts))
.map(|a| attr_row_from_entity(a, &meta))
.collect::<Vec<_>>();
Ok((ins.block_name.clone(), rows))
}
@@ -4363,16 +4363,47 @@ impl OpenCADStudio {
}
}
pub(crate) struct BlockAttrMeta {
pub(crate) prompt: String,
pub(crate) constant: bool,
}
/// Map each attribute tag to the metadata (prompt text, constant flag) declared on
/// the block's matching ATTDEF. Tags with no definition are absent.
pub(crate) fn block_attr_meta(
doc: &codec::CadDocument,
block_name: &str,
) -> rustc_hash::FxHashMap<String, BlockAttrMeta> {
let mut map = rustc_hash::FxHashMap::default();
if let Some(br) = doc.block_records.get(block_name) {
for &eh in &br.entity_handles {
if let Some(codec::EntityType::AttributeDefinition(ad)) = doc.get_entity(eh) {
map.insert(
ad.tag.clone(),
BlockAttrMeta {
prompt: ad.prompt.clone(),
constant: ad.flags.constant,
},
);
}
}
}
map
}
/// Build the editor's working copy of one attribute. Angles are shown in
/// degrees; numeric fields become strings so the user can type freely.
fn attr_row_from_entity(
pub(crate) fn attr_row_from_entity(
a: &codec::entities::AttributeEntity,
prompts: &rustc_hash::FxHashMap<String, String>,
meta: &rustc_hash::FxHashMap<String, BlockAttrMeta>,
) -> AttrRow {
let fmt = |v: f64| format!("{v}");
let ad_meta = meta.get(&a.tag);
let prompt = ad_meta.map(|m| m.prompt.clone()).unwrap_or_default();
let is_constant = a.flags.constant || ad_meta.map_or(false, |m| m.constant);
AttrRow {
tag: a.tag.clone(),
prompt: prompts.get(&a.tag).cloned().unwrap_or_default(),
prompt,
value: a.get_value().to_string(),
text_style: a.text_style.clone(),
height: fmt(a.height),
@@ -4387,15 +4418,16 @@ fn attr_row_from_entity(
color: a.common.color,
linetype: a.common.linetype.clone(),
line_weight: a.common.line_weight,
is_constant,
}
}
/// Write one edited row back onto its attribute; returns whether anything
/// changed. Numeric fields are parsed (angles from degrees); an unparseable
/// field is left as-is.
fn apply_attr_row(a: &mut codec::entities::AttributeEntity, row: &AttrRow) -> bool {
/// field is left as-is. Constant attributes ignore edits to their value.
pub(crate) fn apply_attr_row(a: &mut codec::entities::AttributeEntity, row: &AttrRow) -> bool {
let mut ch = false;
if a.get_value() != row.value {
if !row.is_constant && a.get_value() != row.value {
a.set_value(row.value.clone());
ch = true;
}
@@ -4466,24 +4498,6 @@ fn apply_attr_row(a: &mut codec::entities::AttributeEntity, row: &AttrRow) -> bo
ch
}
/// Map each attribute tag to the prompt text declared on the block's matching
/// ATTDEF. Attribute instances (ATTRIB) carry only tag + value; the prompt is
/// defined once on the block definition. Tags with no definition are absent.
fn block_attr_prompts(
doc: &codec::CadDocument,
block_name: &str,
) -> rustc_hash::FxHashMap<String, String> {
let mut map = rustc_hash::FxHashMap::default();
if let Some(br) = doc.block_records.get(block_name) {
for &eh in &br.entity_handles {
if let Some(codec::EntityType::AttributeDefinition(ad)) = doc.get_entity(eh) {
map.insert(ad.tag.clone(), ad.prompt.clone());
}
}
}
map
}
#[cfg(test)]
mod layer_rename_tests {
use crate::app::OpenCADStudio;