feat(scene): remap extension-dictionary cross-refs across copied entities

A multi-entity COPY cloned each entity's extension-dictionary tree
independently, so refs crossing between the copied trees (root to
sibling root, nested sibling objects, entity handles inside XRecords)
stayed pointed at the sources. Precompute the union closure of every
copied tree, preallocate authoritative clone handles, skip re-cloning
sibling-owned objects during each per-entity pass, and rewrite
entity-valued refs through the completed copy map afterwards. Trees
outside the copy set are deep-cloned inline, matching the reference
product, which clones an out-of-set dictionary target rather than
keeping it verbatim.
This commit is contained in:
Karim Jerbi committed 2026-10-15 03:44:00 +01:00
1 parent 6996505d70
commit 71cc9a5a95
1 file changed
+379 -21
+379 -21
View File
@@ -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<Handle, Handle>,
owner_of: &rustc_hash::FxHashMap<Handle, Handle>,
created: &mut Vec<Handle>,
) -> Option<Handle> {
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<Handle> = 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<Handle>)> = copy_handles
let clones: Vec<(Handle, EntityType, Vec<Handle>, Option<Handle>)> = 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<Handle, Handle>,
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(&copy_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));
}
}