diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 42cceb508..1661853a3 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -549,8 +549,9 @@ impl OpenCADStudio { } } CmdResult::AttreqNeeded { block_name } => { - // Collect AttributeDefinitions owned by this block record. - let attdefs: Vec<(String, String, String)> = { + // Collect the full AttributeDefinitions owned by this block + // record so each created attribute keeps its geometry (#255). + let attdefs: Vec = { let doc = &self.tabs[i].scene.document; if let Some(br) = doc.block_records.get(&block_name) { br.entity_handles @@ -559,11 +560,7 @@ impl OpenCADStudio { if let Some(acadrust::EntityType::AttributeDefinition(ad)) = doc.get_entity(h) { - Some(( - ad.tag.clone(), - ad.prompt.clone(), - ad.default_value.clone(), - )) + Some(ad.clone()) } else { None } diff --git a/src/command.rs b/src/command.rs index 72383f9b8..dcfd6a8ea 100644 --- a/src/command.rs +++ b/src/command.rs @@ -673,9 +673,10 @@ pub trait CadCommand: Send { None } - /// Inject attribute definitions (tag, prompt, default_value) for ATTREQ - /// attr-filling after INSERT point is picked. - fn attreq_set_attdefs(&mut self, _attdefs: Vec<(String, String, String)>) {} + /// Inject the block's attribute definitions for ATTREQ attr-filling after + /// the INSERT point is picked. Carries the full definitions so the created + /// attributes inherit their geometry, not just tag / prompt / default (#255). + fn attreq_set_attdefs(&mut self, _attdefs: Vec) {} /// Returns the INSERT entity built so far (pending attr fill) if this is an /// ATTREQ-aware INSERT command waiting for attdef injection. diff --git a/src/entities/insert.rs b/src/entities/insert.rs index 2e222f401..582319b52 100644 --- a/src/entities/insert.rs +++ b/src/entities/insert.rs @@ -79,9 +79,21 @@ fn apply_geom_prop(ins: &mut Insert, field: &str, value: &str) { return; }; match field { - "ins_x" => ins.insert_point.x = v, - "ins_y" => ins.insert_point.y = v, - "ins_z" => ins.insert_point.z = v, + "ins_x" | "ins_y" | "ins_z" => { + // Move the attributes by the same world delta as the insertion + // 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), "y_scale" => ins.set_y_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 // for a +Z normal, so this matches the old direct assignment there. let ocs = Matrix3::arbitrary_axis(ins.normal); + let old_world = ocs * ins.insert_point; let world = match apply { GripApply::Absolute(p) => Vector3::new(p.x as f64, p.y as f64, p.z as f64), - GripApply::Translate(d) => { - ocs * ins.insert_point + Vector3::new(d.x as f64, d.y as f64, d.z as f64) - } + GripApply::Translate(d) => old_world + Vector3::new(d.x as f64, d.y as f64, d.z as f64), }; 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) { diff --git a/src/modules/insert/insert_block.rs b/src/modules/insert/insert_block.rs index ece7d8c62..f8941eb1b 100644 --- a/src/modules/insert/insert_block.rs +++ b/src/modules/insert/insert_block.rs @@ -1,4 +1,4 @@ -use acadrust::entities::{AttributeEntity, Insert}; +use acadrust::entities::{AttributeDefinition, AttributeEntity, Entity, Insert}; use acadrust::types::Vector3; use acadrust::EntityType; use glam::{DVec3, Vec3}; @@ -23,11 +23,13 @@ enum Step { }, /// Attribute filling: prompt the user tag by tag. FillAttr { - /// Attribute definitions: (tag, prompt_text, default_value). - attdefs: Vec<(String, String, String)>, + /// The block's attribute definitions — carried in full so the created + /// attributes inherit their position / alignment / height / rotation / + /// style, not just tag + prompt + default (#255). + attdefs: Vec, /// Index of the attdef currently being prompted. idx: usize, - /// (tag, value) pairs collected so far. + /// (tag, value) pairs collected so far, in attdef order. values: Vec<(String, String)>, }, } @@ -114,16 +116,16 @@ impl CadCommand for InsertBlockCommand { ), }, Step::FillAttr { attdefs, idx, .. } => { - if let Some((tag, prompt, default)) = attdefs.get(*idx) { - let default_hint = if default.is_empty() { + if let Some(ad) = attdefs.get(*idx) { + let default_hint = if ad.default_value.is_empty() { String::new() } else { - format!(" <{default}>") + format!(" <{}>", ad.default_value) }; - let prompt_text = if prompt.is_empty() { - tag.as_str() + let prompt_text = if ad.prompt.is_empty() { + ad.tag.as_str() } else { - prompt.as_str() + ad.prompt.as_str() }; format!("INSERT {prompt_text}{default_hint}:") } 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) { self.step = Step::FillAttr { attdefs, idx: 0, @@ -263,8 +265,8 @@ impl InsertBlockCommand { fn accept_attr_value(&mut self, text: &str) -> CmdResult { let (tag, default, next_idx, total) = match &self.step { Step::FillAttr { attdefs, idx, .. } => { - let (tag, _, default) = &attdefs[*idx]; - (tag.clone(), default.clone(), idx + 1, attdefs.len()) + let ad = &attdefs[*idx]; + (ad.tag.clone(), ad.default_value.clone(), idx + 1, attdefs.len()) } _ => return CmdResult::Cancel, }; @@ -286,22 +288,25 @@ impl InsertBlockCommand { } if next_idx >= total { - // All attdefs filled — build the INSERT with AttributeEntity list. - let values = match &self.step { - Step::FillAttr { values, .. } => values.clone(), - _ => vec![], + // All attdefs filled — build the INSERT's attribute list. Each + // attribute inherits its ATTDEF's geometry (position, alignment, + // height, rotation, style) via `from_definition`, then the block's + // 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() { Some(i) => i, None => return CmdResult::Cancel, }; - for (tag, value) in values { - let mut attr = AttributeEntity { - tag, - value: value.clone(), - ..Default::default() - }; - attr.set_value(&value); + let xform = ins.get_transform(); + for (ad, (_tag, value)) in attdefs.iter().zip(values.iter()) { + let mut attr = AttributeEntity::from_definition(ad, Some(value.clone())); + attr.apply_transform(&xform); ins.attributes.push(attr); } CmdResult::CommitAndExit(EntityType::Insert(ins))