Merge PR #303: paste copies dimensions correctly (#290)

External contribution by sLuCHa. A dimension draws from its baked *D block
(baked in absolute WCS); the in-drawing COPY re-points block_name to a
transformed copy (#161) but clipboard PASTE did not, so a pasted dimension
rendered at the source location (same drawing) or was missing (cross drawing).

Copy now snapshots each copied dimension's *D block into the clipboard
(dim_blocks); paste builds a fresh copy transformed by the paste offset and
re-points block_name — same- and cross-drawing alike, no orphan blocks.
Refactors out shared define_transformed_block / snapshot_block helpers
(behaviour-preserving).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-07 21:57:51 +03:00
commit 8498b08abf
3 changed files with 121 additions and 34 deletions

View file

@ -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();

View file

@ -779,6 +779,12 @@ pub struct ClipboardDeps {
/// reference doesn't render empty in a drawing that lacks the
/// definition. (#135)
pub blocks: Vec<BlockDef>,
/// 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<BlockDef>,
/// 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<String> = 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<BlockDef> = 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<BlockDef> {
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()

View file

@ -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<EntityType> = sub_handles
.iter()
.filter_map(|&sh| self.document.get_entity(sh).cloned())
.collect();
self.define_transformed_block(&subs, t)
}
/// Build a fresh anonymous `*D<n>` 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<String> {
if subs.is_empty() {
return None;
}
// Smallest free `*D<n>` 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)
}