diff --git a/src/scene/modify.rs b/src/scene/modify.rs index 279668755..dc38f5d5c 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -597,12 +597,20 @@ impl Scene { src_owner: Handle, src_root: Handle, new_owner: Handle, + // Objects reachable from every copied entity's tree, preallocated to + // their authoritative clone handles; `owner_of` maps each such object + // to the copied entity whose tree contains it. + cross: &rustc_hash::FxHashMap, + owner_of: &rustc_hash::FxHashMap, + created: &mut Vec, ) -> Option { if src_root.is_null() { return None; } // Walk every object reachable through dictionary entries and - // xdictionary links, in discovery order (root first). + // xdictionary links, in discovery order (root first). An object owned + // by a sibling's tree is ref'd, never re-cloned here — the sibling's + // own pass fills its preallocated handle. let mut order: Vec = Vec::new(); let mut index = rustc_hash::FxHashMap::default(); let mut stack = vec![src_root]; @@ -610,6 +618,9 @@ impl Scene { if index.contains_key(&h) { continue; } + if owner_of.get(&h).is_some_and(|&o| o != src_owner) { + continue; + } let Some(obj) = doc.objects.get(&h) else { if h == src_root { return None; @@ -636,16 +647,25 @@ impl Scene { } } // Clone and remap onto fresh handles; insert only once the whole tree - // is built so a mid-walk failure leaves the document untouched. + // is built so a mid-walk failure leaves the document untouched. An + // object shared across the copy set lands on its preallocated handle. let mut remap = rustc_hash::FxHashMap::default(); for &h in &order { - remap.insert(h, doc.allocate_handle()); + let new_h = cross + .get(&h) + .copied() + .unwrap_or_else(|| doc.allocate_handle()); + remap.insert(h, new_h); } let rewrite = |handle: Handle| { if handle == src_owner { new_owner + } else if let Some(&n) = remap.get(&handle) { + n } else { - *remap.get(&handle).unwrap_or(&handle) + // A sibling tree object ref'd but never walked (e.g. an + // XRecord handle entry); the sibling's pass clones it. + *cross.get(&handle).unwrap_or(&handle) } }; let mut clones = Vec::with_capacity(order.len()); @@ -693,6 +713,7 @@ impl Scene { } for (new_h, obj) in clones { doc.objects.insert(new_h, obj); + created.push(new_h); } Some(remap[&src_root]) } @@ -812,11 +833,11 @@ impl Scene { let copy_handles = self.handles_expanded_for_leader_annotations(handles); // Objects on a locked layer can be selected but not copied. - let clones: Vec<(Handle, EntityType, Vec)> = copy_handles + let clones: Vec<(Handle, EntityType, Vec, Option)> = copy_handles .iter() .filter(|&&h| !self.is_layer_locked(h)) .filter_map(|&h| { - let entity = self.document.get_entity(h)?.clone(); + let mut entity = self.document.get_entity(h)?.clone(); // Any annotative entity (not only a LEADER/MTEXT pair) needs // its own context tree; a shared one ties the copy to the @@ -827,7 +848,18 @@ impl Scene { h, ); - Some((h, entity, annotation_scales)) + // An annotative entity's extension dictionary is a context tree + // recreated fresh on the copy; any other copy's tree is + // deep-cloned after insertion, so the source root is captured + // here and cleared before add_entity. + let src_xdict = if annotation_scales.is_empty() { + entity.common().xdictionary_handle + } else { + entity.common_mut().xdictionary_handle = None; + None + }; + + Some((h, entity, annotation_scales, src_xdict)) }) .collect(); // MIRRTEXT also governs the copy path (default MIRROR keeps the source @@ -840,7 +872,48 @@ impl Scene { let mut new_handles = Vec::with_capacity(clones.len()); let mut handle_map = rustc_hash::FxHashMap::default(); let mut refresh_solid_handles = Vec::new(); - for (src_handle, mut entity, annotation_scales) in clones { + // Precompute the union of every copied entity's tree so a ref crossing + // between two copied trees lands on the sibling's authoritative clone + // instead of a private duplicate; objects outside the union are + // deep-cloned inline (matching AutoCAD, which deep-clones an + // out-of-set dictionary target rather than keeping it verbatim). + let mut owner_of = rustc_hash::FxHashMap::default(); + let mut cross = rustc_hash::FxHashMap::default(); + for (src_handle, _, _, src_xdict) in &clones { + let Some(root) = *src_xdict else { + continue; + }; + let mut stack = vec![root]; + while let Some(h) = stack.pop() { + if owner_of.insert(h, *src_handle).is_some() { + continue; + } + let Some(obj) = self.document.objects.get(&h) else { + continue; + }; + match obj { + codec::objects::ObjectType::Dictionary(d) => { + for (_, child) in &d.entries { + stack.push(*child); + } + if let Some(xd) = d.xdictionary_handle { + stack.push(xd); + } + } + codec::objects::ObjectType::XRecord(x) => { + if let Some(xd) = x.xdictionary_handle { + stack.push(xd); + } + } + _ => {} + } + } + } + for &h in owner_of.keys() { + cross.insert(h, self.document.allocate_handle()); + } + let mut created_objects = Vec::new(); + for (src_handle, mut entity, annotation_scales, src_xdict) in clones { let text_orient = if preserve_text_orientation { capture_text_orient(&entity) } else { @@ -875,16 +948,9 @@ impl Scene { } Self::reset_clone_subhandles(&mut self.document, &mut entity); - // An annotative entity's extension dictionary is a context tree - // recreated fresh below; any other copy's tree is deep-cloned - // after insertion, so the clone must never carry the source's - // handle through add_entity. - let src_xdict = if annotation_scales.is_empty() { - entity.common_mut().xdictionary_handle.take() - } else { - entity.common_mut().xdictionary_handle = None; - None - }; + // The source root was captured above; the clone must never carry + // the source's handle through add_entity. + entity.common_mut().xdictionary_handle = None; entity.common_mut().handle = Handle::NULL; let h = self.document.add_entity(entity).unwrap_or(Handle::NULL); @@ -893,9 +959,15 @@ impl Scene { // copy never touch the source. A dangling or missing root // leaves the copy without one — never shared. if let Some(src_root) = src_xdict { - if let Some(new_root) = - Self::clone_xdict_tree(&mut self.document, src_handle, src_root, h) - { + if let Some(new_root) = Self::clone_xdict_tree( + &mut self.document, + src_handle, + src_root, + h, + &cross, + &owner_of, + &mut created_objects, + ) { if let Some(clone) = self.document.get_entity_mut(h) { clone.common_mut().xdictionary_handle = Some(new_root); } @@ -1000,6 +1072,9 @@ impl Scene { let _ = self.sync_displayed_annotation_context(leader_handle); } remap_copied_reactors(&mut self.document, &handle_map); + // A tree cloned in one pass can hold refs to sibling copied entities; + // only once every copy exists can those resolve. + remap_copied_xdict_cross_refs(&mut self.document, &handle_map, &created_objects); // Complete group copies record their new Group objects and dictionary // entry as targeted object deltas inside copy_complete_groups. self.copy_complete_groups(&handle_map); @@ -2360,6 +2435,55 @@ pub(crate) fn remap_copied_reactors( } } +/// Resolve entity-valued refs inside freshly cloned object trees through the +/// completed copy map. Object-to-object cross-refs are already resolved +/// against the shared prealloc map inside `clone_xdict_tree`; this pass only +/// rewrites refs to copied entities, which may not have existed yet when an +/// earlier tree in the same call was cloned. +fn remap_copied_xdict_cross_refs( + document: &mut codec::CadDocument, + handle_map: &rustc_hash::FxHashMap, + created: &[Handle], +) { + if handle_map.is_empty() { + return; + } + let rewrite = |h: Handle| handle_map.get(&h).copied().unwrap_or(h); + for &oh in created { + let Some(obj) = document.objects.get_mut(&oh) else { + continue; + }; + match obj { + codec::objects::ObjectType::Dictionary(d) => { + d.owner = rewrite(d.owner); + for (_, child) in &mut d.entries { + *child = rewrite(*child); + } + d.xdictionary_handle = d.xdictionary_handle.map(rewrite); + for r in &mut d.reactors { + *r = rewrite(*r); + } + } + codec::objects::ObjectType::XRecord(x) => { + x.owner = rewrite(x.owner); + x.xdictionary_handle = x.xdictionary_handle.map(rewrite); + for r in &mut x.reactors { + *r = rewrite(*r); + } + for entry in &mut x.entries { + if let codec::objects::XRecordValue::Handle(h) = &mut entry.value { + *h = rewrite(*h); + } + } + for entry in &mut x.object_references { + entry.handle = rewrite(entry.handle); + } + } + _ => {} + } + } +} + #[cfg(test)] mod define_transformed_block_xdict_tests { use super::*; @@ -2941,4 +3065,238 @@ mod copy_entities_xdict_tests { } } } + + /// A line whose extension dictionary is a fresh single-root dictionary + /// owned by (and reactor-linked to) that line. + fn add_line_with_root(scene: &mut Scene) -> (codec::Handle, codec::Handle) { + let root_h = scene.document.allocate_handle(); + let mut root = Dictionary::new(); + root.handle = root_h; + scene + .document + .objects + .insert(root_h, ObjectType::Dictionary(root)); + let mut line = codec::entities::Line::new(); + line.common.xdictionary_handle = Some(root_h); + let src = scene + .document + .add_entity(codec::EntityType::Line(line)) + .expect("add line"); + if let Some(ObjectType::Dictionary(d)) = scene.document.objects.get_mut(&root_h) { + d.owner = src; + d.reactors.push(src); + } + (src, root_h) + } + + fn xdict_root_of(scene: &Scene, entity: codec::Handle) -> codec::Handle { + scene + .document + .get_entity(entity) + .expect("entity") + .common() + .xdictionary_handle + .expect("entity must own an extension dictionary") + } + + #[test] + fn copy_entities_remaps_cross_tree_refs_between_copied_owners() { + let mut scene = Scene::new(); + let (a, a_root) = add_line_with_root(&mut scene); + let (b, b_root) = add_line_with_root(&mut scene); + if let Some(ObjectType::Dictionary(d)) = scene.document.objects.get_mut(&a_root) { + d.add_entry("XREF", b_root); + } + + let copies = scene.copy_entities(&[a, b], &EntityTransform::Translate(DVec3::ZERO)); + assert_eq!(copies.len(), 2); + let (a_copy, b_copy) = (copies[0], copies[1]); + let a_copy_root = xdict_root_of(&scene, a_copy); + let b_copy_root = xdict_root_of(&scene, b_copy); + assert_ne!(a_copy_root, a_root); + assert_ne!(b_copy_root, b_root); + + let Some(ObjectType::Dictionary(a_copy_d)) = scene.document.objects.get(&a_copy_root) else { + panic!("copied root must be a dictionary"); + }; + let xref = a_copy_d + .get("XREF") + .expect("cross-tree entry must survive on the copy"); + assert_eq!( + xref, b_copy_root, + "a cross-owner ref inside the copy set must land on the sibling's clone" + ); + assert_ne!( + xref, b_root, + "a cross-owner ref must never stay pointed at the source sibling" + ); + } + + #[test] + fn copy_entities_remaps_cross_tree_ref_cycles_between_owners() { + let mut scene = Scene::new(); + let (a, a_root) = add_line_with_root(&mut scene); + let (b, b_root) = add_line_with_root(&mut scene); + if let Some(ObjectType::Dictionary(d)) = scene.document.objects.get_mut(&a_root) { + d.add_entry("TO_B", b_root); + } + if let Some(ObjectType::Dictionary(d)) = scene.document.objects.get_mut(&b_root) { + d.add_entry("TO_A", a_root); + } + + let copies = scene.copy_entities(&[a, b], &EntityTransform::Translate(DVec3::ZERO)); + let a_copy_root = xdict_root_of(&scene, copies[0]); + let b_copy_root = xdict_root_of(&scene, copies[1]); + let Some(ObjectType::Dictionary(a_copy_d)) = scene.document.objects.get(&a_copy_root) else { + panic!("A copy root must be a dictionary"); + }; + let Some(ObjectType::Dictionary(b_copy_d)) = scene.document.objects.get(&b_copy_root) else { + panic!("B copy root must be a dictionary"); + }; + assert_eq!( + a_copy_d.get("TO_B"), + Some(b_copy_root), + "A's cross-ref must land on B's clone" + ); + assert_eq!( + b_copy_d.get("TO_A"), + Some(a_copy_root), + "B's cross-ref must land on A's clone" + ); + } + + #[test] + fn copy_entities_remaps_cross_refs_to_nested_sibling_objects() { + let mut scene = Scene::new(); + let (a, a_root) = add_line_with_root(&mut scene); + let (b, b_root) = add_line_with_root(&mut scene); + let b_sub = scene.document.allocate_handle(); + let mut sub = Dictionary::new(); + sub.handle = b_sub; + sub.owner = b_root; + scene + .document + .objects + .insert(b_sub, ObjectType::Dictionary(sub)); + if let Some(ObjectType::Dictionary(d)) = scene.document.objects.get_mut(&b_root) { + d.add_entry("SUB", b_sub); + } + if let Some(ObjectType::Dictionary(d)) = scene.document.objects.get_mut(&a_root) { + d.add_entry("XREF_SUB", b_sub); + } + + let copies = scene.copy_entities(&[a, b], &EntityTransform::Translate(DVec3::ZERO)); + let a_copy_root = xdict_root_of(&scene, copies[0]); + let b_copy_root = xdict_root_of(&scene, copies[1]); + let Some(ObjectType::Dictionary(b_copy_d)) = scene.document.objects.get(&b_copy_root) else { + panic!("B copy root must be a dictionary"); + }; + let b_copy_sub = b_copy_d.get("SUB").expect("SUB entry on B's clone"); + let Some(ObjectType::Dictionary(a_copy_d)) = scene.document.objects.get(&a_copy_root) else { + panic!("A copy root must be a dictionary"); + }; + let xref_sub = a_copy_d + .get("XREF_SUB") + .expect("cross-tree entry to a nested object must survive"); + assert_eq!( + xref_sub, b_copy_sub, + "a cross-owner ref to a nested tree object must land on that object's clone" + ); + assert_ne!( + xref_sub, b_sub, + "a nested cross-ref must never stay pointed at the source object" + ); + } + + #[test] + fn copy_entities_remaps_tree_refs_to_copied_entities() { + let mut scene = Scene::new(); + let (a, a_root) = add_line_with_root(&mut scene); + let (b, _b_root) = add_line_with_root(&mut scene); + let rec_h = scene.document.allocate_handle(); + let mut rec = XRecord::named("LINK"); + rec.handle = rec_h; + rec.owner = a_root; + rec.add_handle(340, b); + scene + .document + .objects + .insert(rec_h, ObjectType::XRecord(rec)); + if let Some(ObjectType::Dictionary(d)) = scene.document.objects.get_mut(&a_root) { + d.add_entry("LINK", rec_h); + } + + let copies = scene.copy_entities(&[a, b], &EntityTransform::Translate(DVec3::ZERO)); + let b_copy = copies[1]; + let a_copy_root = xdict_root_of(&scene, copies[0]); + let Some(ObjectType::Dictionary(a_copy_d)) = scene.document.objects.get(&a_copy_root) else { + panic!("A copy root must be a dictionary"); + }; + let copy_rec_h = a_copy_d.get("LINK").expect("LINK entry on the copy"); + let Some(ObjectType::XRecord(copy_rec)) = scene.document.objects.get(©_rec_h) else { + panic!("LINK must be an XRecord"); + }; + assert!( + copy_rec + .entries + .iter() + .any(|e| matches!(&e.value, XRecordValue::Handle(h) if *h == b_copy)), + "a tree ref to a copied entity must land on that entity's copy" + ); + } + + /// A ref to a tree outside the copy set must not dangle at the source: + /// AutoCAD deep-clones the out-of-set dictionary target (measured in + /// AutoCAD 2026, probe E), so the copy owns an isomorphic private clone. + #[test] + fn copy_entities_deep_clones_uncopied_owner_trees() { + let mut scene = Scene::new(); + let (a, a_root) = add_line_with_root(&mut scene); + let (_b, b_root) = add_line_with_root(&mut scene); + let b_sub = scene.document.allocate_handle(); + let mut sub = Dictionary::new(); + sub.handle = b_sub; + sub.owner = b_root; + scene + .document + .objects + .insert(b_sub, ObjectType::Dictionary(sub)); + if let Some(ObjectType::Dictionary(d)) = scene.document.objects.get_mut(&b_root) { + d.add_entry("SUB", b_sub); + } + if let Some(ObjectType::Dictionary(d)) = scene.document.objects.get_mut(&a_root) { + d.add_entry("XREF", b_root); + } + + let copies = scene.copy_entities(&[a], &EntityTransform::Translate(DVec3::ZERO)); + let a_copy_root = xdict_root_of(&scene, copies[0]); + let Some(ObjectType::Dictionary(a_copy_d)) = scene.document.objects.get(&a_copy_root) else { + panic!("A copy root must be a dictionary"); + }; + let xref = a_copy_d + .get("XREF") + .expect("cross-tree entry must survive on the copy"); + assert_ne!( + xref, b_root, + "an out-of-set target must be deep-cloned, never kept verbatim" + ); + assert_ne!( + xref, b_sub, + "the inline clone is a fresh root, not the source's nested object" + ); + // The inline clone is isomorphic: it carries its own SUB entry. + let Some(ObjectType::Dictionary(cloned)) = scene.document.objects.get(&xref) else { + panic!("out-of-set target clone must be a dictionary"); + }; + let cloned_sub = cloned.get("SUB").expect("SUB entry on the inline clone"); + assert_ne!( + cloned_sub, b_sub, + "the inline clone's subtree must also be cloned, not shared" + ); + // The source's own tree is untouched by the copy. + let Some(ObjectType::Dictionary(b_d)) = scene.document.objects.get(&b_root) else { + panic!("source B root must still exist"); + }; + assert_eq!(b_d.get("SUB"), Some(b_sub)); + } }