fix(insert): place block attributes by their ATTDEF geometry, follow the block (#255)

Inserting a block piled every attribute at the origin: each AttributeEntity
was built from just tag + value with default geometry. Carry the full
AttributeDefinition through the attreq flow and build attributes with
AttributeEntity::from_definition, then apply the block's insertion transform
so each lands in WCS at its proper position / alignment / rotation.

Attributes are stored in world space, so moving the block must move them
too. The MOVE command already did (acadrust translate_insert); apply_grip
(grip-drag) and the Position X/Y/Z property edit now translate the
attributes by the same world delta so they no longer stay behind.

Rotate/scale/mirror of a block still don't carry the attributes — acadrust's
transform_insert has no attribute pass — tracked separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-03 16:47:39 +03:00
commit ab0a4f790c
4 changed files with 60 additions and 40 deletions

View file

@ -549,8 +549,9 @@ impl OpenCADStudio {
} }
} }
CmdResult::AttreqNeeded { block_name } => { CmdResult::AttreqNeeded { block_name } => {
// Collect AttributeDefinitions owned by this block record. // Collect the full AttributeDefinitions owned by this block
let attdefs: Vec<(String, String, String)> = { // record so each created attribute keeps its geometry (#255).
let attdefs: Vec<acadrust::entities::AttributeDefinition> = {
let doc = &self.tabs[i].scene.document; let doc = &self.tabs[i].scene.document;
if let Some(br) = doc.block_records.get(&block_name) { if let Some(br) = doc.block_records.get(&block_name) {
br.entity_handles br.entity_handles
@ -559,11 +560,7 @@ impl OpenCADStudio {
if let Some(acadrust::EntityType::AttributeDefinition(ad)) = if let Some(acadrust::EntityType::AttributeDefinition(ad)) =
doc.get_entity(h) doc.get_entity(h)
{ {
Some(( Some(ad.clone())
ad.tag.clone(),
ad.prompt.clone(),
ad.default_value.clone(),
))
} else { } else {
None None
} }

View file

@ -673,9 +673,10 @@ pub trait CadCommand: Send {
None None
} }
/// Inject attribute definitions (tag, prompt, default_value) for ATTREQ /// Inject the block's attribute definitions for ATTREQ attr-filling after
/// attr-filling after INSERT point is picked. /// the INSERT point is picked. Carries the full definitions so the created
fn attreq_set_attdefs(&mut self, _attdefs: Vec<(String, String, String)>) {} /// attributes inherit their geometry, not just tag / prompt / default (#255).
fn attreq_set_attdefs(&mut self, _attdefs: Vec<acadrust::entities::AttributeDefinition>) {}
/// Returns the INSERT entity built so far (pending attr fill) if this is an /// Returns the INSERT entity built so far (pending attr fill) if this is an
/// ATTREQ-aware INSERT command waiting for attdef injection. /// ATTREQ-aware INSERT command waiting for attdef injection.

View file

@ -79,9 +79,21 @@ fn apply_geom_prop(ins: &mut Insert, field: &str, value: &str) {
return; return;
}; };
match field { match field {
"ins_x" => ins.insert_point.x = v, "ins_x" | "ins_y" | "ins_z" => {
"ins_y" => ins.insert_point.y = v, // Move the attributes by the same world delta as the insertion
"ins_z" => ins.insert_point.z = v, // point so they follow the block instead of staying put (#255).
let ocs = Matrix3::arbitrary_axis(ins.normal);
let old_world = ocs * ins.insert_point;
match field {
"ins_x" => ins.insert_point.x = v,
"ins_y" => ins.insert_point.y = v,
_ => ins.insert_point.z = v,
}
let delta = ocs * ins.insert_point - old_world;
for att in &mut ins.attributes {
acadrust::Entity::translate(att, delta);
}
}
"x_scale" => ins.set_x_scale(v), "x_scale" => ins.set_x_scale(v),
"y_scale" => ins.set_y_scale(v), "y_scale" => ins.set_y_scale(v),
"z_scale" => ins.set_z_scale(v), "z_scale" => ins.set_z_scale(v),
@ -96,13 +108,18 @@ fn apply_grip(ins: &mut Insert, _grip_id: usize, apply: GripApply) {
// whose extrusion direction isn't +Z moves along world axes. Identity OCS // whose extrusion direction isn't +Z moves along world axes. Identity OCS
// for a +Z normal, so this matches the old direct assignment there. // for a +Z normal, so this matches the old direct assignment there.
let ocs = Matrix3::arbitrary_axis(ins.normal); let ocs = Matrix3::arbitrary_axis(ins.normal);
let old_world = ocs * ins.insert_point;
let world = match apply { let world = match apply {
GripApply::Absolute(p) => Vector3::new(p.x as f64, p.y as f64, p.z as f64), GripApply::Absolute(p) => Vector3::new(p.x as f64, p.y as f64, p.z as f64),
GripApply::Translate(d) => { GripApply::Translate(d) => old_world + Vector3::new(d.x as f64, d.y as f64, d.z as f64),
ocs * ins.insert_point + Vector3::new(d.x as f64, d.y as f64, d.z as f64)
}
}; };
ins.insert_point = ocs.transpose() * world; ins.insert_point = ocs.transpose() * world;
// Attributes sit in world space beside the block, so move them by the same
// world delta — otherwise a grip-drag leaves the attribute text behind (#255).
let delta = world - old_world;
for att in &mut ins.attributes {
acadrust::Entity::translate(att, delta);
}
} }
fn apply_transform(ins: &mut Insert, t: &EntityTransform) { fn apply_transform(ins: &mut Insert, t: &EntityTransform) {

View file

@ -1,4 +1,4 @@
use acadrust::entities::{AttributeEntity, Insert}; use acadrust::entities::{AttributeDefinition, AttributeEntity, Entity, Insert};
use acadrust::types::Vector3; use acadrust::types::Vector3;
use acadrust::EntityType; use acadrust::EntityType;
use glam::{DVec3, Vec3}; use glam::{DVec3, Vec3};
@ -23,11 +23,13 @@ enum Step {
}, },
/// Attribute filling: prompt the user tag by tag. /// Attribute filling: prompt the user tag by tag.
FillAttr { FillAttr {
/// Attribute definitions: (tag, prompt_text, default_value). /// The block's attribute definitions — carried in full so the created
attdefs: Vec<(String, String, String)>, /// attributes inherit their position / alignment / height / rotation /
/// style, not just tag + prompt + default (#255).
attdefs: Vec<AttributeDefinition>,
/// Index of the attdef currently being prompted. /// Index of the attdef currently being prompted.
idx: usize, idx: usize,
/// (tag, value) pairs collected so far. /// (tag, value) pairs collected so far, in attdef order.
values: Vec<(String, String)>, values: Vec<(String, String)>,
}, },
} }
@ -114,16 +116,16 @@ impl CadCommand for InsertBlockCommand {
), ),
}, },
Step::FillAttr { attdefs, idx, .. } => { Step::FillAttr { attdefs, idx, .. } => {
if let Some((tag, prompt, default)) = attdefs.get(*idx) { if let Some(ad) = attdefs.get(*idx) {
let default_hint = if default.is_empty() { let default_hint = if ad.default_value.is_empty() {
String::new() String::new()
} else { } else {
format!(" <{default}>") format!(" <{}>", ad.default_value)
}; };
let prompt_text = if prompt.is_empty() { let prompt_text = if ad.prompt.is_empty() {
tag.as_str() ad.tag.as_str()
} else { } else {
prompt.as_str() ad.prompt.as_str()
}; };
format!("INSERT {prompt_text}{default_hint}:") format!("INSERT {prompt_text}{default_hint}:")
} else { } else {
@ -242,7 +244,7 @@ impl CadCommand for InsertBlockCommand {
} }
} }
fn attreq_set_attdefs(&mut self, attdefs: Vec<(String, String, String)>) { fn attreq_set_attdefs(&mut self, attdefs: Vec<AttributeDefinition>) {
self.step = Step::FillAttr { self.step = Step::FillAttr {
attdefs, attdefs,
idx: 0, idx: 0,
@ -263,8 +265,8 @@ impl InsertBlockCommand {
fn accept_attr_value(&mut self, text: &str) -> CmdResult { fn accept_attr_value(&mut self, text: &str) -> CmdResult {
let (tag, default, next_idx, total) = match &self.step { let (tag, default, next_idx, total) = match &self.step {
Step::FillAttr { attdefs, idx, .. } => { Step::FillAttr { attdefs, idx, .. } => {
let (tag, _, default) = &attdefs[*idx]; let ad = &attdefs[*idx];
(tag.clone(), default.clone(), idx + 1, attdefs.len()) (ad.tag.clone(), ad.default_value.clone(), idx + 1, attdefs.len())
} }
_ => return CmdResult::Cancel, _ => return CmdResult::Cancel,
}; };
@ -286,22 +288,25 @@ impl InsertBlockCommand {
} }
if next_idx >= total { if next_idx >= total {
// All attdefs filled — build the INSERT with AttributeEntity list. // All attdefs filled — build the INSERT's attribute list. Each
let values = match &self.step { // attribute inherits its ATTDEF's geometry (position, alignment,
Step::FillAttr { values, .. } => values.clone(), // height, rotation, style) via `from_definition`, then the block's
_ => vec![], // insertion transform places it into WCS so it lands where the block
// put it instead of stacking at the origin (#255).
let (attdefs, values) = match &self.step {
Step::FillAttr {
attdefs, values, ..
} => (attdefs.clone(), values.clone()),
_ => (vec![], vec![]),
}; };
let mut ins = match self.pending_insert.take() { let mut ins = match self.pending_insert.take() {
Some(i) => i, Some(i) => i,
None => return CmdResult::Cancel, None => return CmdResult::Cancel,
}; };
for (tag, value) in values { let xform = ins.get_transform();
let mut attr = AttributeEntity { for (ad, (_tag, value)) in attdefs.iter().zip(values.iter()) {
tag, let mut attr = AttributeEntity::from_definition(ad, Some(value.clone()));
value: value.clone(), attr.apply_transform(&xform);
..Default::default()
};
attr.set_value(&value);
ins.attributes.push(attr); ins.attributes.push(attr);
} }
CmdResult::CommitAndExit(EntityType::Insert(ins)) CmdResult::CommitAndExit(EntityType::Insert(ins))