forked from 1b-agent/opencadstudio-upstream
fix(save): baked dimension blocks and block clones must not share extension dictionaries
bake_dimension_blocks copied the dimension's whole EntityCommon, including its extension dictionary handle, into every baked child, so many entities claimed one dictionary and file validation reported an error per entity. Baked children now get no extension dictionary. The same defect is fixed in define_transformed_block and the cross-document paste clone. Each has a test.
This commit is contained in:
1 parent
cd07141cde
commit
a456367b26
3 files changed
+292
No files matched your search
@@ -707,6 +707,11 @@ fn add_entity_clone_doc_inner(
|
||||
}
|
||||
entity.common_mut().handle = Handle::NULL;
|
||||
entity.common_mut().owner_handle = owner;
|
||||
// An extension dictionary is exclusively owned; the clone must not claim
|
||||
// the source's (cross-drawing it would dangle into the source document),
|
||||
// or file validation reports "Extension dictionary <H> Cannot access".
|
||||
// Paste reattaches a recreated subtree afterwards (finalize_paste).
|
||||
entity.common_mut().xdictionary_handle = None;
|
||||
}
|
||||
|
||||
/// Document-level fresh `*D<n>` block from `subs`: the `&mut CadDocument`
|
||||
@@ -1469,3 +1474,86 @@ mod headless_reuse_proof_tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod define_transformed_block_doc_xdict_tests {
|
||||
use super::define_transformed_block_doc;
|
||||
|
||||
/// No extension dictionary may be referenced by more than one entity.
|
||||
fn assert_no_shared_xdicts(doc: &codec::CadDocument) {
|
||||
use std::collections::HashMap;
|
||||
let mut owners: HashMap<codec::Handle, usize> = HashMap::new();
|
||||
for entity in doc.entities() {
|
||||
if let Some(xd) = entity.common().xdictionary_handle {
|
||||
if !xd.is_null() {
|
||||
*owners.entry(xd).or_default() += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let shared: Vec<_> = owners
|
||||
.iter()
|
||||
.filter(|(_, &n)| n > 1)
|
||||
.map(|(h, n)| format!("{h:?} x{n}"))
|
||||
.collect();
|
||||
assert!(
|
||||
shared.is_empty(),
|
||||
"shared extension dictionaries: {}",
|
||||
shared.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
/// Pasted/derived sub-entities must not share the source's extension
|
||||
/// dictionary. `define_transformed_block_doc` is the verbatim
|
||||
/// document-level port of `Scene::define_transformed_block`, and its
|
||||
/// clone helper likewise preserves `xdictionary_handle`; across drawings
|
||||
/// that handle would dangle into the source document.
|
||||
#[test]
|
||||
fn cloned_subs_do_not_share_source_xdict() {
|
||||
let mut doc = codec::CadDocument::new();
|
||||
|
||||
let dict_handle = doc.allocate_handle();
|
||||
let mut dict = codec::objects::Dictionary::new();
|
||||
dict.handle = dict_handle;
|
||||
doc.objects
|
||||
.insert(dict_handle, codec::objects::ObjectType::Dictionary(dict));
|
||||
|
||||
let mut line = codec::entities::Line::new();
|
||||
line.common.xdictionary_handle = Some(dict_handle);
|
||||
let src_handle = doc.add_entity(codec::EntityType::Line(line)).unwrap();
|
||||
|
||||
let subs = vec![doc.get_entity(src_handle).unwrap().clone()];
|
||||
let name = define_transformed_block_doc(
|
||||
&mut doc,
|
||||
&subs,
|
||||
&crate::command::EntityTransform::Translate(glam::DVec3::ZERO),
|
||||
)
|
||||
.expect("clone block must be created");
|
||||
assert!(
|
||||
name.starts_with("*D"),
|
||||
"anonymous block expected, got {name}"
|
||||
);
|
||||
|
||||
assert_no_shared_xdicts(&doc);
|
||||
// Source keeps its dictionary.
|
||||
match doc.get_entity(src_handle) {
|
||||
Some(e) => assert_eq!(
|
||||
e.common().xdictionary_handle,
|
||||
Some(dict_handle),
|
||||
"the source must keep its own dictionary"
|
||||
),
|
||||
None => panic!("source entity missing"),
|
||||
}
|
||||
// Clones carry none.
|
||||
let owned = doc.block_records.get(&name).unwrap().entity_handles.clone();
|
||||
assert!(!owned.is_empty(), "clone block should own sub-entities");
|
||||
for h in &owned {
|
||||
match doc.get_entity(*h) {
|
||||
Some(e) => assert!(
|
||||
e.common().xdictionary_handle.is_none(),
|
||||
"cloned child {h:?} must not carry an extension dictionary"
|
||||
),
|
||||
None => panic!("cloned child {h:?} missing"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -869,6 +869,10 @@ fn dim_common(
|
||||
) -> codec::entities::EntityCommon {
|
||||
let mut c = base.clone();
|
||||
c.handle = Handle::NULL;
|
||||
// A baked child must not claim its dimension's extension dictionary:
|
||||
// xdict ownership is exclusive, and a second claimant fails file
|
||||
// validation with "Extension dictionary <H> Cannot access".
|
||||
c.xdictionary_handle = None;
|
||||
if clr != 0 && clr != 256 {
|
||||
c.color = codec::types::Color::from_index(clr);
|
||||
c.color_name = None;
|
||||
@@ -1415,6 +1419,9 @@ fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec<EntityType> {
|
||||
tent.common_mut().color_name = tc.color_name;
|
||||
tent.common_mut().color_book_handle = tc.color_book_handle;
|
||||
tent.common_mut().line_weight = tc.line_weight;
|
||||
// The live-render text entity carries the dimension's dictionary;
|
||||
// the baked copy must not share it (see above).
|
||||
tent.common_mut().xdictionary_handle = None;
|
||||
result.push(tent);
|
||||
}
|
||||
|
||||
@@ -2124,4 +2131,94 @@ mod tests {
|
||||
failures.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
/// No extension dictionary may be referenced by more than one entity.
|
||||
/// Shared xdict ownership fails file validation as
|
||||
/// "Extension dictionary <H> Cannot access".
|
||||
fn assert_no_shared_xdicts(doc: &CadDocument) {
|
||||
use std::collections::HashMap;
|
||||
let mut owners: HashMap<codec::Handle, usize> = HashMap::new();
|
||||
for entity in doc.entities() {
|
||||
if let Some(xd) = entity.common().xdictionary_handle {
|
||||
if !xd.is_null() {
|
||||
*owners.entry(xd).or_default() += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let shared: Vec<_> = owners
|
||||
.iter()
|
||||
.filter(|(_, &n)| n > 1)
|
||||
.map(|(h, n)| format!("{h:?} x{n}"))
|
||||
.collect();
|
||||
assert!(
|
||||
shared.is_empty(),
|
||||
"shared extension dictionaries: {}",
|
||||
shared.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
/// Baked dimension children must not share the dimension's extension
|
||||
/// dictionary. `dim_common` used to clone the dimension's full common
|
||||
/// block (including `xdictionary_handle`) onto every baked child, so an
|
||||
/// R2000 save of a drawing with N such children failed audit
|
||||
/// with exactly N "Cannot access" errors.
|
||||
#[test]
|
||||
fn baked_children_do_not_share_dimension_xdict() {
|
||||
let mut doc = CadDocument::new();
|
||||
|
||||
// Made-up dictionary object for the dimension to own.
|
||||
let dict_handle = doc.allocate_handle();
|
||||
let mut dict = codec::objects::Dictionary::new();
|
||||
dict.handle = dict_handle;
|
||||
doc.objects
|
||||
.insert(dict_handle, codec::objects::ObjectType::Dictionary(dict));
|
||||
|
||||
// Blockless dimension carrying that dictionary, as a file-read
|
||||
// dimension with an extension dictionary would.
|
||||
let mut d = DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
|
||||
d.definition_point = Vector3::new(0.0, 5.0, 0.0);
|
||||
d.base.text_middle_point = Vector3::new(5.0, 5.0, 0.0);
|
||||
d.base.common.xdictionary_handle = Some(dict_handle);
|
||||
let dim_handle = doc
|
||||
.add_entity(EntityType::Dimension(Dimension::Linear(d)))
|
||||
.unwrap();
|
||||
|
||||
bake_dimension_blocks(&mut doc);
|
||||
|
||||
let block_name = match doc.get_entity(dim_handle) {
|
||||
Some(EntityType::Dimension(d)) => d.base().block_name.clone(),
|
||||
_ => panic!("dimension missing"),
|
||||
};
|
||||
let owned: Vec<Handle> = doc
|
||||
.block_records
|
||||
.get(&block_name)
|
||||
.unwrap()
|
||||
.entity_handles
|
||||
.clone();
|
||||
assert!(
|
||||
!owned.is_empty(),
|
||||
"baked block should own sub-entities for the invariant to mean anything"
|
||||
);
|
||||
// (a) no shared dictionary anywhere in the document.
|
||||
assert_no_shared_xdicts(&doc);
|
||||
// (b) the dimension keeps its dictionary.
|
||||
match doc.get_entity(dim_handle) {
|
||||
Some(EntityType::Dimension(d)) => assert_eq!(
|
||||
d.base().common.xdictionary_handle,
|
||||
Some(dict_handle),
|
||||
"the dimension must keep its own dictionary"
|
||||
),
|
||||
_ => panic!("dimension missing"),
|
||||
}
|
||||
// (c) the baked children carry none.
|
||||
for h in &owned {
|
||||
match doc.get_entity(*h) {
|
||||
Some(e) => assert!(
|
||||
e.common().xdictionary_handle.is_none(),
|
||||
"baked child {h:?} must not carry an extension dictionary"
|
||||
),
|
||||
None => panic!("baked child {h:?} missing"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -671,6 +671,10 @@ impl Scene {
|
||||
Self::reset_clone_subhandles(&mut self.document, &mut sub);
|
||||
sub.common_mut().handle = Handle::NULL;
|
||||
sub.common_mut().owner_handle = br_handle;
|
||||
// An extension dictionary is exclusively owned; the clone must
|
||||
// not claim the source's, or file validation reports
|
||||
// "Extension dictionary <H> Cannot access".
|
||||
sub.common_mut().xdictionary_handle = None;
|
||||
let _ = self.document.add_entity(sub);
|
||||
}
|
||||
Some(new_name)
|
||||
@@ -2207,3 +2211,106 @@ pub(crate) fn remap_copied_reactors(
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod define_transformed_block_xdict_tests {
|
||||
use super::*;
|
||||
|
||||
/// No extension dictionary may be referenced by more than one entity.
|
||||
fn assert_no_shared_xdicts(doc: &codec::CadDocument) {
|
||||
use std::collections::HashMap;
|
||||
let mut owners: HashMap<codec::Handle, usize> = HashMap::new();
|
||||
for entity in doc.entities() {
|
||||
if let Some(xd) = entity.common().xdictionary_handle {
|
||||
if !xd.is_null() {
|
||||
*owners.entry(xd).or_default() += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let shared: Vec<_> = owners
|
||||
.iter()
|
||||
.filter(|(_, &n)| n > 1)
|
||||
.map(|(h, n)| format!("{h:?} x{n}"))
|
||||
.collect();
|
||||
assert!(
|
||||
shared.is_empty(),
|
||||
"shared extension dictionaries: {}",
|
||||
shared.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
/// Cloned sub-entities must not share the source's extension dictionary.
|
||||
/// `define_transformed_block` clones each sub wholesale; without clearing
|
||||
/// `xdictionary_handle` the clone claims the source's dictionary and
|
||||
/// file validation reports "Extension dictionary <H> Cannot access".
|
||||
#[test]
|
||||
fn cloned_subs_do_not_share_source_xdict() {
|
||||
let mut scene = Scene::new();
|
||||
|
||||
let dict_handle = scene.document.allocate_handle();
|
||||
let mut dict = codec::objects::Dictionary::new();
|
||||
dict.handle = dict_handle;
|
||||
scene
|
||||
.document
|
||||
.objects
|
||||
.insert(dict_handle, codec::objects::ObjectType::Dictionary(dict));
|
||||
|
||||
let mut br = codec::tables::BlockRecord::new("SRC");
|
||||
br.handle = scene.document.allocate_handle();
|
||||
scene.document.block_records.add(br).unwrap();
|
||||
let mut line = codec::entities::Line::new();
|
||||
line.common.xdictionary_handle = Some(dict_handle);
|
||||
let src_handle = scene
|
||||
.document
|
||||
.add_entity(codec::EntityType::Line(line))
|
||||
.unwrap();
|
||||
scene
|
||||
.document
|
||||
.block_records
|
||||
.get_mut("SRC")
|
||||
.unwrap()
|
||||
.entity_handles
|
||||
.push(src_handle);
|
||||
|
||||
let subs = vec![scene.document.get_entity(src_handle).unwrap().clone()];
|
||||
let name = scene
|
||||
.define_transformed_block(
|
||||
&subs,
|
||||
&crate::command::EntityTransform::Translate(glam::DVec3::ZERO),
|
||||
)
|
||||
.expect("clone block must be created");
|
||||
assert!(
|
||||
name.starts_with("*D"),
|
||||
"anonymous block expected, got {name}"
|
||||
);
|
||||
|
||||
assert_no_shared_xdicts(&scene.document);
|
||||
// Source keeps its dictionary.
|
||||
match scene.document.get_entity(src_handle) {
|
||||
Some(e) => assert_eq!(
|
||||
e.common().xdictionary_handle,
|
||||
Some(dict_handle),
|
||||
"the source must keep its own dictionary"
|
||||
),
|
||||
None => panic!("source entity missing"),
|
||||
}
|
||||
// Clones carry none.
|
||||
let owned = scene
|
||||
.document
|
||||
.block_records
|
||||
.get(&name)
|
||||
.unwrap()
|
||||
.entity_handles
|
||||
.clone();
|
||||
assert!(!owned.is_empty(), "clone block should own sub-entities");
|
||||
for h in &owned {
|
||||
match scene.document.get_entity(*h) {
|
||||
Some(e) => assert!(
|
||||
e.common().xdictionary_handle.is_none(),
|
||||
"cloned child {h:?} must not carry an extension dictionary"
|
||||
),
|
||||
None => panic!("cloned child {h:?} missing"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user