From a72b143628db7d7bc75c32f71cd1b3e6a60f4e2d Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 25 Jun 2026 22:30:45 +0300 Subject: [PATCH] fix(copy): duplicate a dimension's baked block so COPY copies it (#161) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/scene/mod.rs | 67 ++++++++++++++++++++++++++++++++++++++ tests/dim_copy_check.rs | 72 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 tests/dim_copy_check.rs diff --git a/src/scene/mod.rs b/src/scene/mod.rs index bde91392..030b1376 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -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 { + 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` 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 { let clones: Vec = 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); diff --git a/tests/dim_copy_check.rs b/tests/dim_copy_check.rs new file mode 100644 index 00000000..ac35fe2f --- /dev/null +++ b/tests/dim_copy_check.rs @@ -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)), + } +}