fix(copy): duplicate a dimension's baked block so COPY copies it (#161)

A dimension's drawn geometry lives in a baked anonymous *D block. COPY
cloned the Dimension entity (translating its definition points) but left
its block_name pointing at the source block, whose sub-entities stay at
the original location — so the copy rendered on top of the original and
appeared not to copy at all.

Add `clone_transformed_block`: when copying a dimension that has a baked
block, duplicate that block under a fresh *D name with every sub-entity
transformed by the same offset, and repoint the copy at it. The copy now
lands at the drop point with its baked geometry and text preserved (no
synthesis, so diameter/radius values stay correct). Covered by a portable
regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-25 22:30:45 +03:00
commit a72b143628
2 changed files with 139 additions and 0 deletions

View file

@ -6060,6 +6060,60 @@ impl Scene {
self.add_entity(entity)
}
/// Duplicate the anonymous block `src_name`, transforming every sub-entity
/// by `t`, and return the new block's name. A dimension's drawn geometry
/// lives in such a baked `*D` block, so a copied dimension needs its own
/// transformed block — otherwise it still references the source block and
/// renders on top of the original instead of at the copy. Returns None when
/// the source block is missing or empty. (#161)
fn clone_transformed_block(&mut self, src_name: &str, t: &EntityTransform) -> Option<String> {
let sub_handles = self
.document
.block_records
.iter()
.find(|br| br.name.eq_ignore_ascii_case(src_name))
.map(|br| br.entity_handles.clone())?;
if sub_handles.is_empty() {
return None;
}
// Smallest free `*D<n>` anonymous name.
let mut n = 0u64;
let new_name = loop {
let cand = format!("*D{n}");
if self.document.block_records.get(&cand).is_none() {
break cand;
}
n += 1;
};
let next = self.document.next_handle();
let br_handle = Handle::new(next);
let block_handle = Handle::new(next + 1);
let end_handle = Handle::new(next + 2);
let mut br = acadrust::tables::BlockRecord::new(&new_name);
br.handle = br_handle;
br.block_entity_handle = block_handle;
br.block_end_handle = end_handle;
self.document.block_records.add(br).ok()?;
let mut block = Block::new(&new_name, acadrust::types::Vector3::ZERO);
block.common.handle = block_handle;
block.common.owner_handle = br_handle;
self.document.add_entity(EntityType::Block(block)).ok()?;
let mut block_end = BlockEnd::new();
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);
}
}
Some(new_name)
}
pub fn copy_entities(&mut self, handles: &[Handle], t: &EntityTransform) -> Vec<Handle> {
let clones: Vec<EntityType> = handles
.iter()
@ -6068,6 +6122,19 @@ impl Scene {
let mut new_handles = Vec::with_capacity(clones.len());
for mut entity in clones {
view::dispatch::apply_transform(&mut entity, t);
// A dimension draws from its baked `*D` block; give the copy its own
// transformed block so it lands at the copy position rather than
// rendering on top of the source. (#161)
if let EntityType::Dimension(d) = &entity {
let bn = d.base().block_name.clone();
if !bn.trim().is_empty() {
if let Some(new_bn) = self.clone_transformed_block(&bn, t) {
if let EntityType::Dimension(d) = &mut entity {
d.base_mut().block_name = new_bn;
}
}
}
}
Self::reset_clone_subhandles(&mut self.document, &mut entity);
entity.common_mut().handle = Handle::NULL;
let h = self.document.add_entity(entity).unwrap_or(Handle::NULL);

72
tests/dim_copy_check.rs Normal file
View file

@ -0,0 +1,72 @@
// Regression for #161: COPY must duplicate a dimension's baked block so the
// copy renders at the copy position, not on top of the original.
use acadrust::entities::{Dimension, DimensionLinear, Line};
use acadrust::tables::BlockRecord;
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle};
use glam::DVec3;
use OpenCADStudio::command::EntityTransform;
use OpenCADStudio::scene::Scene;
#[test]
fn copy_dimension_duplicates_its_block() {
let mut scene = Scene::new();
// A baked *D0 block holding one line at (0,0)-(10,0).
let br_h = Handle::new(scene.document.next_handle());
let mut br = BlockRecord::new("*D0");
br.handle = br_h;
scene.document.block_records.add(br).unwrap();
let mut sub = Line::new();
sub.start = Vector3::new(0.0, 0.0, 0.0);
sub.end = Vector3::new(10.0, 0.0, 0.0);
let mut sub_e = EntityType::Line(sub);
sub_e.common_mut().owner_handle = br_h; // route into *D0
scene.document.add_entity(sub_e).unwrap();
// A linear dimension whose drawn geometry is that baked block.
let mut dim = DimensionLinear::new(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(10.0, 0.0, 0.0),
);
dim.base.block_name = "*D0".to_string();
let dim_h = scene.add_entity(EntityType::Dimension(Dimension::Linear(dim)));
// Copy the dimension by (0, 50).
let copies = scene.copy_entities(
&[dim_h],
&EntityTransform::Translate(DVec3::new(0.0, 50.0, 0.0)),
);
assert_eq!(copies.len(), 1);
let copy_h = copies[0];
// The copy must reference its OWN block, not the source's.
let copy_block = match scene.document.get_entity(copy_h) {
Some(EntityType::Dimension(d)) => d.base().block_name.clone(),
_ => panic!("copy is not a dimension"),
};
assert_ne!(copy_block, "*D0", "copy must get its own block");
assert!(!copy_block.trim().is_empty(), "copy block name must be set");
// The copy block's sub-line must be translated by (0, 50): (0,50)-(10,50).
let new_br = scene
.document
.block_records
.iter()
.find(|b| b.name == copy_block)
.expect("copy block record exists");
let subh = *new_br
.entity_handles
.first()
.expect("copy block has a sub-entity");
match scene.document.get_entity(subh) {
Some(EntityType::Line(l)) => assert!(
(l.start.y - 50.0).abs() < 1e-6 && (l.end.y - 50.0).abs() < 1e-6,
"copy block sub-line not translated: {:?}-{:?}",
(l.start.x, l.start.y),
(l.end.x, l.end.y)
),
other => panic!("copy block sub is not a line: {:?}", other.map(std::mem::discriminant)),
}
}