diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 8658ec9e1..6d93ecff8 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -2012,6 +2012,35 @@ impl OpenCADStudio { if let Some(t) = &translate { crate::scene::view::dispatch::apply_transform(&mut entity, t); } + // A dimension draws from its baked `*D` block (baked in WCS), so + // give the paste its own transformed copy of that block and + // re-point it — otherwise the pasted dimension renders at the + // source location instead of the paste point. The block was + // snapshotted into the clipboard at copy time, so this works + // cross-drawing too. Mirrors the in-drawing copy. (#290, #161) + if let acadrust::EntityType::Dimension(d) = &entity { + let bn = d.base().block_name.clone(); + if !bn.trim().is_empty() { + let subs = self + .clipboard_deps + .dim_blocks + .iter() + .find(|b| b.name.eq_ignore_ascii_case(&bn)) + .map(|def| def.entities.clone()); + if let Some(subs) = subs { + let bt = translate.clone().unwrap_or( + crate::command::EntityTransform::Translate(glam::DVec3::ZERO), + ); + if let Some(new_bn) = + self.tabs[i].scene.define_transformed_block(&subs, &bt) + { + if let acadrust::EntityType::Dimension(d) = &mut entity { + d.base_mut().block_name = new_bn; + } + } + } + } + } self.tabs[i].scene.add_entity_clone(entity) }) .collect(); diff --git a/src/app/mod.rs b/src/app/mod.rs index 35249746f..ad912dd81 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -779,6 +779,12 @@ pub struct ClipboardDeps { /// reference doesn't render empty in a drawing that lacks the /// definition. (#135) pub blocks: Vec, + /// Baked `*D` blocks referenced by copied dimensions, snapshotted from the + /// source drawing. On paste each pasted dimension gets its own transformed + /// copy of its block (its geometry is baked in WCS, so without this the + /// paste renders at the source location — or, cross-drawing, not at all). + /// See #290 (mirrors the in-drawing copy's #161 fix). + pub dim_blocks: Vec, /// Extension-dictionary object subtrees hanging off the copied entities /// (XCLIP spatial filters, attached XRecords, …). Each entity's whole /// `xdictionary` graph is snapshotted so a cross-drawing paste recreates it @@ -878,6 +884,27 @@ impl ClipboardDeps { } } + // Baked `*D` blocks for copied dimensions (top-level and inside captured + // blocks), so a pasted dimension can be given its own transformed block + // instead of aliasing the source's — whose geometry is baked in WCS at + // the source location. (#290) + let mut dim_block_names: BTreeSet = BTreeSet::new(); + for e in entities + .iter() + .chain(blocks.iter().flat_map(|d| d.entities.iter())) + { + if let EntityType::Dimension(d) = e { + let bn = d.base().block_name.clone(); + if !bn.trim().is_empty() { + dim_block_names.insert(bn); + } + } + } + let dim_blocks: Vec = dim_block_names + .iter() + .filter_map(|n| Self::snapshot_block(doc, n)) + .collect(); + ClipboardDeps { layers: layers .iter() @@ -896,6 +923,7 @@ impl ClipboardDeps { .filter_map(|n| doc.dim_styles.get(n).cloned()) .collect(), blocks, + dim_blocks, ext_objects, } } @@ -958,43 +986,55 @@ impl ClipboardDeps { } let mut defs = Vec::new(); while let Some(name) = queue.pop() { - let Some(br) = doc.block_records.get(&name) else { + let Some(def) = Self::snapshot_block(doc, &name) else { continue; }; - if name.starts_with("*Model_Space") - || name.starts_with("*Paper_Space") - || br.flags.is_xref - { - continue; - } - let base_point = match doc.get_entity(br.block_entity_handle) { - Some(EntityType::Block(b)) => b.base_point, - _ => acadrust::types::Vector3::ZERO, - }; - let mut owned = Vec::new(); - for &eh in &br.entity_handles { - let Some(e) = doc.get_entity(eh) else { - continue; - }; - if matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_)) { - continue; - } + // Follow nested INSERTs so their definitions are captured too. + for e in &def.entities { if let EntityType::Insert(ins) = e { if seen.insert(ins.block_name.clone()) { queue.push(ins.block_name.clone()); } } - owned.push(e.clone()); } - defs.push(BlockDef { - name, - base_point, - entities: owned, - }); + defs.push(def); } defs } + /// Snapshot one block definition as a portable `BlockDef`: its base point + /// and owned entities, minus the structural Block/BlockEnd markers. Returns + /// None for model/paper space and xref blocks (not portable definitions). + fn snapshot_block(doc: &acadrust::CadDocument, name: &str) -> Option { + use acadrust::EntityType; + let br = doc.block_records.get(name)?; + if name.starts_with("*Model_Space") + || name.starts_with("*Paper_Space") + || br.flags.is_xref + { + return None; + } + let base_point = match doc.get_entity(br.block_entity_handle) { + Some(EntityType::Block(b)) => b.base_point, + _ => acadrust::types::Vector3::ZERO, + }; + let mut owned = Vec::new(); + for &eh in &br.entity_handles { + let Some(e) = doc.get_entity(eh) else { + continue; + }; + if matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_)) { + continue; + } + owned.push(e.clone()); + } + Some(BlockDef { + name: name.to_string(), + base_point, + entities: owned, + }) + } + pub fn is_empty(&self) -> bool { self.layers.is_empty() && self.linetypes.is_empty() diff --git a/src/scene/modify.rs b/src/scene/modify.rs index 15c5f1285..e0a9c4bf4 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -223,7 +223,26 @@ impl Scene { .iter() .find(|br| br.name.eq_ignore_ascii_case(src_name)) .map(|br| br.entity_handles.clone())?; - if sub_handles.is_empty() { + let subs: Vec = sub_handles + .iter() + .filter_map(|&sh| self.document.get_entity(sh).cloned()) + .collect(); + self.define_transformed_block(&subs, t) + } + + /// Build a fresh anonymous `*D` block from `subs` — a source block's + /// sub-entities in its own (WCS-baked) coordinates — transforming each by + /// `t`, and return the new block's name. Shared by the in-drawing copy + /// (source block still lives in this document, via `clone_transformed_block`) + /// and clipboard paste (source block snapshotted into the clipboard, so a + /// pasted dimension gets its own transformed block cross-drawing too — see + /// `finalize_paste`, #290). Returns None when `subs` is empty. + pub(crate) fn define_transformed_block( + &mut self, + subs: &[EntityType], + t: &EntityTransform, + ) -> Option { + if subs.is_empty() { return None; } // Smallest free `*D` anonymous name. @@ -252,14 +271,13 @@ impl Scene { block_end.common.handle = end_handle; block_end.common.owner_handle = br_handle; self.document.add_entity(EntityType::BlockEnd(block_end)).ok()?; - for sh in sub_handles { - if let Some(mut sub) = self.document.get_entity(sh).cloned() { - view::dispatch::apply_transform(&mut sub, t); - Self::reset_clone_subhandles(&mut self.document, &mut sub); - sub.common_mut().handle = Handle::NULL; - sub.common_mut().owner_handle = br_handle; - let _ = self.document.add_entity(sub); - } + for sub in subs { + let mut sub = sub.clone(); + view::dispatch::apply_transform(&mut sub, t); + Self::reset_clone_subhandles(&mut self.document, &mut sub); + sub.common_mut().handle = Handle::NULL; + sub.common_mut().owner_handle = br_handle; + let _ = self.document.add_entity(sub); } Some(new_name) }