forked from 1b-agent/opencadstudio-upstream
scene: deep-clone extension-dictionary trees on in-drawing copy
A copied entity used to keep its source's extension-dictionary handle, so the copy and source shared one tree and edits crossed between them. In-drawing copy now clones the whole tree onto fresh handles: dictionary entries, owners, reactors, nested xdictionary links, and XRecord handle references that land inside the tree are remapped; references leaving it stay pointed at the originals. A dangling or missing root leaves the copy without an extension dictionary rather than sharing. Annotative context trees and the paste/export clone path are unchanged. Undo stays on the full snapshot path when a tree is cloned.
This commit is contained in:
1 parent
881d237629
commit
acb8c5ea54
1 file changed
+567
-4
+567
-4
@@ -581,6 +581,122 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
|
||||
/// Deep-clone the extension-dictionary tree rooted at `src_root` (owned by
|
||||
/// the entity `src_owner`) and return the clone's root, wired for
|
||||
/// `new_owner`. In-drawing COPY parity: the copy gets its own tree, so
|
||||
/// edits on it never touch the source. Dictionary entries, owners,
|
||||
/// reactors, nested xdictionary links, and XRecord handle references that
|
||||
/// land inside the tree are remapped onto fresh handles; references
|
||||
/// leaving the tree are preserved verbatim. A cyclic tree terminates via
|
||||
/// the visited set and remaps consistently. Atomic: nothing enters
|
||||
/// `document.objects` unless the whole tree clones; a missing or dangling
|
||||
/// root returns `None` so the caller leaves the copy without an extension
|
||||
/// dictionary — never shared.
|
||||
pub(super) fn clone_xdict_tree(
|
||||
doc: &mut codec::CadDocument,
|
||||
src_owner: Handle,
|
||||
src_root: Handle,
|
||||
new_owner: 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).
|
||||
let mut order: Vec<Handle> = Vec::new();
|
||||
let mut index = rustc_hash::FxHashMap::default();
|
||||
let mut stack = vec![src_root];
|
||||
while let Some(h) = stack.pop() {
|
||||
if index.contains_key(&h) {
|
||||
continue;
|
||||
}
|
||||
let Some(obj) = doc.objects.get(&h) else {
|
||||
if h == src_root {
|
||||
return None;
|
||||
}
|
||||
continue;
|
||||
};
|
||||
index.insert(h, order.len());
|
||||
order.push(h);
|
||||
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);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Clone and remap onto fresh handles; insert only once the whole tree
|
||||
// is built so a mid-walk failure leaves the document untouched.
|
||||
let mut remap = rustc_hash::FxHashMap::default();
|
||||
for &h in &order {
|
||||
remap.insert(h, doc.allocate_handle());
|
||||
}
|
||||
let rewrite = |handle: Handle| {
|
||||
if handle == src_owner {
|
||||
new_owner
|
||||
} else {
|
||||
*remap.get(&handle).unwrap_or(&handle)
|
||||
}
|
||||
};
|
||||
let mut clones = Vec::with_capacity(order.len());
|
||||
for &h in &order {
|
||||
let new_h = remap[&h];
|
||||
let obj = doc.objects.get(&h)?;
|
||||
let mut obj = obj.clone();
|
||||
match &mut obj {
|
||||
codec::objects::ObjectType::Dictionary(d) => {
|
||||
d.handle = new_h;
|
||||
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.handle = new_h;
|
||||
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);
|
||||
}
|
||||
// The raw DWG stream describes the source's object bytes;
|
||||
// the clone regenerates its own from the remapped entries.
|
||||
x.raw_dwg_data = None;
|
||||
x.raw_dwg_handle_bits = 0;
|
||||
x.raw_dwg_version = None;
|
||||
x.preserve_object_reference_stream = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
clones.push((new_h, obj));
|
||||
}
|
||||
for (new_h, obj) in clones {
|
||||
doc.objects.insert(new_h, obj);
|
||||
}
|
||||
Some(remap[&src_root])
|
||||
}
|
||||
|
||||
/// Add a clone to the active space with fresh top-level and inline handles.
|
||||
/// Source owner handles are document-local and must not cross drawings.
|
||||
/// (#129, #934)
|
||||
@@ -759,15 +875,35 @@ impl Scene {
|
||||
}
|
||||
Self::reset_clone_subhandles(&mut self.document, &mut entity);
|
||||
|
||||
// An annotative LEADER/MTEXT pair must receive a fresh extension dictionary.
|
||||
// Keeping this handle would make the copy share the source's context tree.
|
||||
if !annotation_scales.is_empty() {
|
||||
// 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
|
||||
};
|
||||
|
||||
entity.common_mut().handle = Handle::NULL;
|
||||
let h = self.document.add_entity(entity).unwrap_or(Handle::NULL);
|
||||
if !h.is_null() {
|
||||
// Deep-clone the extension-dictionary tree so edits on the
|
||||
// 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(clone) = self.document.get_entity_mut(h) {
|
||||
clone.common_mut().xdictionary_handle = Some(new_root);
|
||||
}
|
||||
// The cloned tree lives in document.objects — outside
|
||||
// a pure-entity delta; keep undo on the full snapshot.
|
||||
self.poison_undo_recording();
|
||||
}
|
||||
}
|
||||
if !annotation_scales.is_empty() {
|
||||
for scale_handle in annotation_scales {
|
||||
crate::scene::annotative::create_annotation_context(
|
||||
@@ -2379,3 +2515,430 @@ mod add_entity_clone_xdict_tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod copy_entities_xdict_tests {
|
||||
use super::*;
|
||||
use crate::command::EntityTransform;
|
||||
use codec::objects::{
|
||||
Dictionary, ObjectType, ProxyObjectReference, ProxyReferenceKind, XRecord, XRecordValue,
|
||||
};
|
||||
use glam::DVec3;
|
||||
|
||||
/// Line with an extension-dictionary tree:
|
||||
/// root { SUB -> sub { DEEP -> deep }, REC -> rec("CLONE_ME") };
|
||||
/// root owner and reactors both name the source entity.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn scene_with_xdict_tree() -> (
|
||||
Scene,
|
||||
codec::Handle,
|
||||
codec::Handle,
|
||||
codec::Handle,
|
||||
codec::Handle,
|
||||
codec::Handle,
|
||||
) {
|
||||
let mut scene = Scene::new();
|
||||
let root_h = scene.document.allocate_handle();
|
||||
let sub_h = scene.document.allocate_handle();
|
||||
let deep_h = scene.document.allocate_handle();
|
||||
let rec_h = scene.document.allocate_handle();
|
||||
|
||||
let mut deep = XRecord::named("DEEP");
|
||||
deep.handle = deep_h;
|
||||
deep.owner = sub_h;
|
||||
deep.add_string(1, "DEEP_VALUE");
|
||||
let mut sub = Dictionary::new();
|
||||
sub.handle = sub_h;
|
||||
sub.owner = root_h;
|
||||
sub.add_entry("DEEP", deep_h);
|
||||
let mut rec = XRecord::named("REC");
|
||||
rec.handle = rec_h;
|
||||
rec.owner = root_h;
|
||||
rec.add_string(1, "CLONE_ME");
|
||||
let mut root = Dictionary::new();
|
||||
root.handle = root_h;
|
||||
root.add_entry("SUB", sub_h);
|
||||
root.add_entry("REC", rec_h);
|
||||
|
||||
scene
|
||||
.document
|
||||
.objects
|
||||
.insert(root_h, ObjectType::Dictionary(root));
|
||||
scene
|
||||
.document
|
||||
.objects
|
||||
.insert(sub_h, ObjectType::Dictionary(sub));
|
||||
scene
|
||||
.document
|
||||
.objects
|
||||
.insert(deep_h, ObjectType::XRecord(deep));
|
||||
scene
|
||||
.document
|
||||
.objects
|
||||
.insert(rec_h, ObjectType::XRecord(rec));
|
||||
|
||||
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 source entity");
|
||||
if let Some(ObjectType::Dictionary(d)) = scene.document.objects.get_mut(&root_h) {
|
||||
d.owner = src;
|
||||
d.reactors.push(src);
|
||||
}
|
||||
(scene, src, root_h, sub_h, deep_h, rec_h)
|
||||
}
|
||||
|
||||
/// Every object reachable from `root` through dictionary entries and
|
||||
/// xdictionary links (cycle-safe).
|
||||
fn tree_handles(doc: &codec::CadDocument, root: codec::Handle) -> Vec<codec::Handle> {
|
||||
let mut out = Vec::new();
|
||||
let mut stack = vec![root];
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
while let Some(h) = stack.pop() {
|
||||
if !seen.insert(h) {
|
||||
continue;
|
||||
}
|
||||
out.push(h);
|
||||
match doc.objects.get(&h) {
|
||||
Some(ObjectType::Dictionary(d)) => {
|
||||
for (_, c) in &d.entries {
|
||||
stack.push(*c);
|
||||
}
|
||||
if let Some(xd) = d.xdictionary_handle {
|
||||
stack.push(xd);
|
||||
}
|
||||
}
|
||||
Some(ObjectType::XRecord(x)) => {
|
||||
if let Some(xd) = x.xdictionary_handle {
|
||||
stack.push(xd);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out.sort_unstable();
|
||||
out
|
||||
}
|
||||
|
||||
/// Byte-for-byte image of a tree for source-unchanged assertions.
|
||||
fn tree_image(
|
||||
doc: &codec::CadDocument,
|
||||
root: codec::Handle,
|
||||
) -> Vec<(codec::Handle, ObjectType)> {
|
||||
tree_handles(doc, root)
|
||||
.into_iter()
|
||||
.map(|h| (h, doc.objects.get(&h).expect("tree object").clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_deep_clones_extension_dictionary_tree() {
|
||||
let (mut scene, src, root, _sub, _deep, rec) = scene_with_xdict_tree();
|
||||
let before = tree_image(&scene.document, root);
|
||||
|
||||
let copies = scene.copy_entities(&[src], &EntityTransform::Translate(DVec3::ZERO));
|
||||
assert_eq!(copies.len(), 1);
|
||||
let copy = copies[0];
|
||||
assert!(!copy.is_null() && copy != src);
|
||||
|
||||
let copy_root = scene
|
||||
.document
|
||||
.get_entity(copy)
|
||||
.expect("copy entity")
|
||||
.common()
|
||||
.xdictionary_handle
|
||||
.expect("copy must own a deep-cloned extension dictionary");
|
||||
assert_ne!(copy_root, root, "copy must not claim the source's root");
|
||||
|
||||
// Structure mirrors the source shape on fresh handles.
|
||||
let copy_tree = tree_handles(&scene.document, copy_root);
|
||||
assert_eq!(
|
||||
copy_tree.len(),
|
||||
4,
|
||||
"cloned tree must mirror the source: {copy_tree:?}"
|
||||
);
|
||||
for h in ©_tree {
|
||||
assert!(
|
||||
!tree_handles(&scene.document, root).contains(h),
|
||||
"clone must not reuse source handles"
|
||||
);
|
||||
}
|
||||
|
||||
// Root wiring: owner and reactor rewritten to the copy.
|
||||
let ObjectType::Dictionary(copy_root_obj) = scene
|
||||
.document
|
||||
.objects
|
||||
.get(©_root)
|
||||
.expect("copy root object")
|
||||
else {
|
||||
panic!("copy root must be a dictionary");
|
||||
};
|
||||
assert_eq!(
|
||||
copy_root_obj.owner, copy,
|
||||
"cloned root must be owned by the copy"
|
||||
);
|
||||
assert_eq!(
|
||||
copy_root_obj.reactors,
|
||||
vec![copy],
|
||||
"reactor naming the source must be rewritten to the copy"
|
||||
);
|
||||
|
||||
// Children resolve by key with preserved content.
|
||||
let sub_h = copy_root_obj.get("SUB").expect("SUB entry on copy");
|
||||
let copy_rec = copy_root_obj.get("REC").expect("REC entry on copy");
|
||||
let ObjectType::Dictionary(sub_obj) =
|
||||
scene.document.objects.get(&sub_h).expect("copy sub object")
|
||||
else {
|
||||
panic!("copy SUB must be a dictionary");
|
||||
};
|
||||
assert_eq!(
|
||||
sub_obj.owner, copy_root,
|
||||
"nested owner must point at the cloned parent"
|
||||
);
|
||||
let deep_h = sub_obj.get("DEEP").expect("DEEP entry on copy");
|
||||
let ObjectType::XRecord(deep_obj) = scene
|
||||
.document
|
||||
.objects
|
||||
.get(&deep_h)
|
||||
.expect("copy deep object")
|
||||
else {
|
||||
panic!("copy DEEP must be an XRecord");
|
||||
};
|
||||
assert_eq!(deep_obj.name, "DEEP");
|
||||
let ObjectType::XRecord(copy_rec_obj) = scene
|
||||
.document
|
||||
.objects
|
||||
.get(©_rec)
|
||||
.expect("copy REC object")
|
||||
else {
|
||||
panic!("copy REC must be an XRecord");
|
||||
};
|
||||
assert_eq!(copy_rec_obj.name, "REC");
|
||||
assert!(
|
||||
copy_rec_obj
|
||||
.entries
|
||||
.iter()
|
||||
.any(|e| matches!(&e.value, XRecordValue::String(s) if s == "CLONE_ME")),
|
||||
"cloned XRecord must carry the string content"
|
||||
);
|
||||
|
||||
// Mutation independence: edit the copy, source stays untouched.
|
||||
if let Some(ObjectType::XRecord(x)) = scene.document.objects.get_mut(©_rec) {
|
||||
if let Some(e) = x
|
||||
.entries
|
||||
.iter_mut()
|
||||
.find(|e| matches!(&e.value, XRecordValue::String(s) if s == "CLONE_ME"))
|
||||
{
|
||||
e.value = XRecordValue::String("MUTATED".into());
|
||||
}
|
||||
}
|
||||
let ObjectType::XRecord(src_rec_obj) =
|
||||
scene.document.objects.get(&rec).expect("source REC object")
|
||||
else {
|
||||
panic!("source REC must be an XRecord");
|
||||
};
|
||||
assert!(
|
||||
src_rec_obj
|
||||
.entries
|
||||
.iter()
|
||||
.any(|e| matches!(&e.value, XRecordValue::String(s) if s == "CLONE_ME")),
|
||||
"source XRecord must be unaffected by edits on the copy"
|
||||
);
|
||||
|
||||
// Source tree byte-for-byte unchanged.
|
||||
let after = tree_image(&scene.document, root);
|
||||
assert_eq!(
|
||||
before, after,
|
||||
"source extension-dictionary tree must be byte-for-byte unchanged"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_without_xdict_adds_no_objects_and_keeps_delta_undo() {
|
||||
let mut scene = Scene::new();
|
||||
let line = codec::entities::Line::new();
|
||||
let src = scene
|
||||
.document
|
||||
.add_entity(codec::EntityType::Line(line))
|
||||
.expect("add source entity");
|
||||
let objects_before = scene.document.objects.len();
|
||||
|
||||
scene.begin_undo_recording();
|
||||
let copies = scene.copy_entities(&[src], &EntityTransform::Translate(DVec3::ZERO));
|
||||
let objects_after = scene.document.objects.len();
|
||||
|
||||
assert_eq!(copies.len(), 1);
|
||||
assert_ne!(copies[0], src);
|
||||
assert_eq!(
|
||||
objects_before, objects_after,
|
||||
"xdict-free copy must add no document.objects entries"
|
||||
);
|
||||
let recording = scene.take_undo_recording().expect("recording open");
|
||||
assert!(
|
||||
!recording.poisoned,
|
||||
"xdict-free copy must stay on the delta-undo path"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_with_dangling_xdict_root_leaves_copy_without_one() {
|
||||
let mut scene = Scene::new();
|
||||
let missing = scene.document.allocate_handle();
|
||||
let mut line = codec::entities::Line::new();
|
||||
line.common.xdictionary_handle = Some(missing);
|
||||
let src = scene
|
||||
.document
|
||||
.add_entity(codec::EntityType::Line(line))
|
||||
.expect("add source entity");
|
||||
let objects_before = scene.document.objects.len();
|
||||
|
||||
let copies = scene.copy_entities(&[src], &EntityTransform::Translate(DVec3::ZERO));
|
||||
|
||||
let copy_root = scene
|
||||
.document
|
||||
.get_entity(copies[0])
|
||||
.expect("copy entity")
|
||||
.common()
|
||||
.xdictionary_handle;
|
||||
assert!(
|
||||
copy_root.is_none(),
|
||||
"dangling root must leave the copy without an extension dictionary, never shared"
|
||||
);
|
||||
assert_eq!(
|
||||
objects_before,
|
||||
scene.document.objects.len(),
|
||||
"dangling root must not invent objects"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_cloned_xdict_cycle_terminates_and_remaps() {
|
||||
let (mut scene, src, root, ..) = scene_with_xdict_tree();
|
||||
if let Some(ObjectType::Dictionary(d)) = scene.document.objects.get_mut(&root) {
|
||||
d.add_entry("BACK", root);
|
||||
}
|
||||
|
||||
let copies = scene.copy_entities(&[src], &EntityTransform::Translate(DVec3::ZERO));
|
||||
let copy_root = scene
|
||||
.document
|
||||
.get_entity(copies[0])
|
||||
.expect("copy entity")
|
||||
.common()
|
||||
.xdictionary_handle
|
||||
.expect("copy dictionary");
|
||||
let ObjectType::Dictionary(d) = scene.document.objects.get(©_root).expect("dict")
|
||||
else {
|
||||
panic!("dictionary");
|
||||
};
|
||||
assert_eq!(
|
||||
d.get("BACK"),
|
||||
Some(copy_root),
|
||||
"cyclic entry must be remapped to the cloned root, consistently"
|
||||
);
|
||||
assert_eq!(
|
||||
tree_handles(&scene.document, copy_root).len(),
|
||||
4,
|
||||
"cycle must not duplicate the tree"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_keeps_references_outside_the_cloned_tree() {
|
||||
let (mut scene, src, root, ..) = scene_with_xdict_tree();
|
||||
let (_, rec) = {
|
||||
let ObjectType::Dictionary(d) = scene.document.objects.get(&root).expect("root") else {
|
||||
panic!("root must be a dictionary");
|
||||
};
|
||||
((), d.get("REC").expect("REC entry"))
|
||||
};
|
||||
// An entity recorded inside the tree (e.g. a field pointing back at a
|
||||
// drawing entity) stays pointed at the original; entities are never
|
||||
// part of the object-tree clone.
|
||||
let mut other_line = codec::entities::Line::new();
|
||||
other_line.common.handle = Handle::NULL;
|
||||
let other = scene
|
||||
.document
|
||||
.add_entity(codec::EntityType::Line(other_line))
|
||||
.expect("other entity");
|
||||
// An object outside the reachable closure, referenced only through
|
||||
// the XRecord handle stream, likewise stays pointed at the original.
|
||||
let outside_h = scene.document.allocate_handle();
|
||||
let mut outside = Dictionary::new();
|
||||
outside.handle = outside_h;
|
||||
scene
|
||||
.document
|
||||
.objects
|
||||
.insert(outside_h, ObjectType::Dictionary(outside));
|
||||
if let Some(ObjectType::XRecord(x)) = scene.document.objects.get_mut(&rec) {
|
||||
x.add_handle(340, other);
|
||||
x.object_references.push(ProxyObjectReference {
|
||||
handle: outside_h,
|
||||
kind: ProxyReferenceKind::SoftPointer,
|
||||
});
|
||||
}
|
||||
|
||||
let copies = scene.copy_entities(&[src], &EntityTransform::Translate(DVec3::ZERO));
|
||||
let copy_root = scene
|
||||
.document
|
||||
.get_entity(copies[0])
|
||||
.expect("copy entity")
|
||||
.common()
|
||||
.xdictionary_handle
|
||||
.expect("copy dictionary");
|
||||
let ObjectType::Dictionary(d) = scene.document.objects.get(©_root).expect("dict")
|
||||
else {
|
||||
panic!("dictionary");
|
||||
};
|
||||
let copy_rec = d.get("REC").expect("REC entry on copy");
|
||||
let ObjectType::XRecord(copy_rec_obj) =
|
||||
scene.document.objects.get(©_rec).expect("copy REC")
|
||||
else {
|
||||
panic!("copy REC must be an XRecord");
|
||||
};
|
||||
assert!(
|
||||
copy_rec_obj
|
||||
.entries
|
||||
.iter()
|
||||
.any(|e| matches!(&e.value, XRecordValue::Handle(h) if *h == other)),
|
||||
"entity references leaving the object tree must stay pointed at the original"
|
||||
);
|
||||
assert!(
|
||||
copy_rec_obj
|
||||
.object_references
|
||||
.iter()
|
||||
.any(|r| r.handle == outside_h),
|
||||
"handle-stream references leaving the source tree must stay pointed at the original"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_copy_gets_its_own_disjoint_tree() {
|
||||
let (mut scene, src, root, ..) = scene_with_xdict_tree();
|
||||
let source_tree = tree_handles(&scene.document, root);
|
||||
let mut all_trees = vec![source_tree];
|
||||
|
||||
for _ in 0..3 {
|
||||
let copies = scene.copy_entities(&[src], &EntityTransform::Translate(DVec3::ZERO));
|
||||
let copy_root = scene
|
||||
.document
|
||||
.get_entity(copies[0])
|
||||
.expect("copy entity")
|
||||
.common()
|
||||
.xdictionary_handle
|
||||
.expect("each copy must own a dictionary");
|
||||
all_trees.push(tree_handles(&scene.document, copy_root));
|
||||
}
|
||||
|
||||
for (i, a) in all_trees.iter().enumerate() {
|
||||
for (j, b) in all_trees.iter().enumerate() {
|
||||
if i == j {
|
||||
continue;
|
||||
}
|
||||
let overlap = a.iter().any(|h| b.contains(h));
|
||||
assert!(!overlap, "tree {i} and tree {j} must be disjoint");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user