mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-10 04:19:59 -06:00
DXF/DWG: a null-owner object goes with a hard owner that is kept
hard_owners took the first object naming a null-owner object in a 360 as its owner, kept or not.
When an object that is left out named it first, the reader dropped the object, though a kept one
held it too, and the save wrote the kept holder's 360 as 0.
hard_owners now lists every object naming it, in file order, and owner_of takes the first of
them that is kept, else the first. The read (retained_objects) and the write liveness pass in
finish run until what they keep settles; a file where no object has two hard owners takes one
pass, as before. The write pass no longer keeps a converted copy of every object's groups.
The test adds an object nothing keeps that names the held record before its holder does: the
held record stays with the holder and the stray object is dropped, DXF and DWG, twice. It fails
before this change ("DXF 1: no record held").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H43qQA41CJLauqXnPgBQ7r
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1363afb883
commit
adf02f9f04
+130
-86
@@ -517,65 +517,104 @@ fn retained_objects(
|
||||
// hundreds of thousands of groups) would be read once per child (#862).
|
||||
let mut xdict_memo: HashMap<usize, bool> = HashMap::new();
|
||||
let mut is_xdict = |index: usize| *xdict_memo.entry(index).or_insert_with(|| is_xdict(index));
|
||||
let hard_owners = hard_owners(objects.iter().map(|(_, _, t)| *t));
|
||||
let mut retained = HashMap::new();
|
||||
for index in 0..objects.len() {
|
||||
let mut path = Vec::new();
|
||||
let mut current = index;
|
||||
let keep = loop {
|
||||
if let Some(&known) = retained.get(¤t) {
|
||||
break known;
|
||||
}
|
||||
if path.contains(¤t) || path.len() >= MAX_DEPTH {
|
||||
break false;
|
||||
}
|
||||
path.push(current);
|
||||
let Some((_, _, tags)) = objects.get(current) else { break false };
|
||||
let header = head(tags);
|
||||
let Some(handle) = header.handle else { break false };
|
||||
let Some(owner) = owner_of(header.owner, &handle, &hard_owners) else { break false };
|
||||
if by_handle.get(&handle) != Some(¤t) {
|
||||
break false;
|
||||
}
|
||||
if root == Some(owner.as_str()) {
|
||||
break root_key(&handle).is_some_and(|key| !modelled_key(key));
|
||||
}
|
||||
if let Some(&parent) = by_handle.get(&owner) {
|
||||
if is_xdict(parent) && objects.get(parent).is_some_and(|object| modelled_entry(object.2, &handle)) {
|
||||
let hard = hard_owners(objects.iter().filter_map(|(_, _, t)| Some((head(t).handle?, held(t)))));
|
||||
// One pass with `kept` the objects an earlier pass kept: a null-owner object goes with the
|
||||
// first of its hard owners that is kept.
|
||||
let mut pass = |kept: &HashMap<usize, bool>| {
|
||||
let kept = |h: &str| by_handle.get(h).and_then(|i| kept.get(i)) == Some(&true);
|
||||
let mut retained = HashMap::new();
|
||||
for index in 0..objects.len() {
|
||||
let mut path = Vec::new();
|
||||
let mut current = index;
|
||||
let keep = loop {
|
||||
if let Some(&known) = retained.get(¤t) {
|
||||
break known;
|
||||
}
|
||||
if path.contains(¤t) || path.len() >= MAX_DEPTH {
|
||||
break false;
|
||||
}
|
||||
current = parent;
|
||||
continue;
|
||||
path.push(current);
|
||||
let Some((_, _, tags)) = objects.get(current) else { break false };
|
||||
let header = head(tags);
|
||||
let Some(handle) = header.handle else { break false };
|
||||
let Some(owner) = owner_of(header.owner, &handle, &hard, kept) else { break false };
|
||||
if by_handle.get(&handle) != Some(¤t) {
|
||||
break false;
|
||||
}
|
||||
if root == Some(owner.as_str()) {
|
||||
break root_key(&handle).is_some_and(|key| !modelled_key(key));
|
||||
}
|
||||
if let Some(&parent) = by_handle.get(&owner) {
|
||||
if is_xdict(parent) && objects.get(parent).is_some_and(|object| modelled_entry(object.2, &handle)) {
|
||||
break false;
|
||||
}
|
||||
current = parent;
|
||||
continue;
|
||||
}
|
||||
if is_xdict(current) {
|
||||
break entries(tags).iter().any(|(key, _)| !MODELLED_XDICT_KEYS.contains(&up(key).as_str()));
|
||||
}
|
||||
break entities.get(&owner) == Some(&true);
|
||||
};
|
||||
for visited in path {
|
||||
retained.insert(visited, keep);
|
||||
}
|
||||
if is_xdict(current) {
|
||||
break entries(tags).iter().any(|(key, _)| !MODELLED_XDICT_KEYS.contains(&up(key).as_str()));
|
||||
}
|
||||
break entities.get(&owner) == Some(&true);
|
||||
};
|
||||
for visited in path {
|
||||
retained.insert(visited, keep);
|
||||
}
|
||||
}
|
||||
retained
|
||||
retained
|
||||
};
|
||||
settle(&hard, |kept| pass(kept))
|
||||
}
|
||||
|
||||
/// The object that owns each object by a hard owner reference (group 360), the first one
|
||||
/// to name it: `child handle → owner handle`.
|
||||
fn hard_owners<'a>(objects: impl Iterator<Item = &'a [Tag]>) -> HashMap<String, String> {
|
||||
let mut out = HashMap::new();
|
||||
for t in objects {
|
||||
let Some(h) = head(t).handle else { continue };
|
||||
for c in t.iter().take_while(|g| g.code < 1000).filter(|g| g.code == 360) {
|
||||
out.entry(norm(&c.str())).or_insert_with(|| h.clone());
|
||||
/// Run `pass` until what it keeps settles. The first pass sees nothing kept, so a null-owner
|
||||
/// object goes with its first hard owner; while one of its other hard owners is kept and that
|
||||
/// one is not, the next pass moves it there. A file where no object has two hard owners takes
|
||||
/// one pass; otherwise it stops when a pass changes nothing, after at most [`MAX_DEPTH`] more.
|
||||
fn settle<K: Eq + std::hash::Hash>(
|
||||
hard: &HashMap<String, Vec<String>>,
|
||||
mut pass: impl FnMut(&HashMap<K, bool>) -> HashMap<K, bool>,
|
||||
) -> HashMap<K, bool> {
|
||||
let mut kept = pass(&HashMap::new());
|
||||
if hard.values().all(|owners| owners.len() < 2) {
|
||||
return kept;
|
||||
}
|
||||
for _ in 0..MAX_DEPTH {
|
||||
let next = pass(&kept);
|
||||
if next == kept {
|
||||
break;
|
||||
}
|
||||
kept = next;
|
||||
}
|
||||
kept
|
||||
}
|
||||
|
||||
/// The handles an object names by a hard owner reference (group 360).
|
||||
fn held(tags: &[Tag]) -> impl Iterator<Item = String> + '_ {
|
||||
tags.iter().take_while(|g| g.code < 1000).filter(|g| g.code == 360).map(|g| g.str())
|
||||
}
|
||||
|
||||
/// The objects that own each object by a hard owner reference (group 360), in file order:
|
||||
/// `child handle → owner handles`.
|
||||
fn hard_owners<S: AsRef<str>>(objects: impl Iterator<Item = (String, impl Iterator<Item = S>)>) -> HashMap<String, Vec<String>> {
|
||||
let mut out: HashMap<String, Vec<String>> = HashMap::new();
|
||||
for (h, children) in objects {
|
||||
for c in children {
|
||||
let owners = out.entry(norm(c.as_ref())).or_default();
|
||||
if !owners.contains(&h) {
|
||||
owners.push(h.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// An object's owner: its group 330, or for one whose owner is null (another application's
|
||||
/// proxy that another proxy holds) the object that hard-owns it ([`hard_owners`]).
|
||||
fn owner_of(owner: Option<String>, handle: &str, hard: &HashMap<String, String>) -> Option<String> {
|
||||
owner.filter(|o| o != "0").or_else(|| hard.get(handle).cloned())
|
||||
/// proxy that another proxy holds) the object that hard-owns it ([`hard_owners`]): the first
|
||||
/// one that is `kept`, or else the first one.
|
||||
fn owner_of(owner: Option<String>, handle: &str, hard: &HashMap<String, Vec<String>>, kept: impl Fn(&str) -> bool) -> Option<String> {
|
||||
owner.filter(|o| o != "0").or_else(|| {
|
||||
let owners = hard.get(handle)?;
|
||||
owners.iter().find(|o| kept(o)).or_else(|| owners.first()).cloned()
|
||||
})
|
||||
}
|
||||
|
||||
/// Proxy records (DXF Reference, ACAD_PROXY_OBJECT and ACAD_PROXY_ENTITY): another application's
|
||||
@@ -922,50 +961,55 @@ pub(crate) fn finish(tags: &mut Vec<Tag>, d: &Drawing, reactors: &HashMap<Handle
|
||||
let objs: HashMap<String, &ForeignObject> = f.objects.iter().map(|o| (o.handle.hex(), o)).collect();
|
||||
let dict_entries: HashMap<String, &str> = f.dictionary.iter().map(|(k, h)| (h.hex(), k.as_str())).collect();
|
||||
let nod = f.targets.iter().filter(|(_, k)| k.0 == "NOD" && k.1.is_empty()).map(|(h, _)| h.hex()).collect::<HashSet<_>>();
|
||||
let kept_tags: Vec<Vec<Tag>> = f.objects.iter().map(|o| o.tags.iter().take_while(|t| t.code < 1000).map(tag).collect()).collect();
|
||||
let hard = hard_owners(kept_tags.iter().map(Vec::as_slice));
|
||||
let owners: HashMap<String, String> = f
|
||||
let hard = hard_owners(
|
||||
f.objects.iter().map(|o| (o.handle.hex(), o.tags.iter().take_while(|t| t.code < 1000).filter(|t| t.code == 360).map(|t| t.value.trim()))),
|
||||
);
|
||||
// Each kept object's own owner (group 330), read one record at a time.
|
||||
let own: HashMap<String, Option<String>> = f
|
||||
.objects
|
||||
.iter()
|
||||
.zip(&kept_tags)
|
||||
.filter_map(|(o, t)| {
|
||||
let h = o.handle.hex();
|
||||
let owner = owner_of(head(t).owner, &h, &hard)?;
|
||||
Some((h, owner))
|
||||
.map(|o| {
|
||||
let t: Vec<Tag> = o.tags.iter().take_while(|t| t.code < 1000).map(tag).collect();
|
||||
(o.handle.hex(), head(&t).owner)
|
||||
})
|
||||
.collect();
|
||||
let mut alive_memo: HashMap<String, bool> = HashMap::new();
|
||||
for o in f.objects.iter().filter(|o| is_proxy(&o.kind) && renumber(class_number(o.tags.iter().map(|r| (r.code, r.value.as_str())))).is_none()) {
|
||||
alive_memo.insert(o.handle.hex(), false);
|
||||
}
|
||||
for o in &f.objects {
|
||||
let mut path: Vec<String> = Vec::new();
|
||||
let mut cur = o.handle.hex();
|
||||
let verdict = loop {
|
||||
if let Some(&v) = alive_memo.get(&cur) {
|
||||
break v;
|
||||
}
|
||||
if path.contains(&cur) || path.len() >= MAX_DEPTH {
|
||||
break false;
|
||||
}
|
||||
path.push(cur.clone());
|
||||
let Some(owner) = owners.get(&cur).cloned() else { break false };
|
||||
if objs.contains_key(&owner) {
|
||||
cur = owner;
|
||||
continue;
|
||||
}
|
||||
if nod.contains(&owner) {
|
||||
break dict_entries.get(&cur).is_some_and(|k| !root_keys.contains(&up(k))) && root.is_some();
|
||||
}
|
||||
if attached.contains(&cur) {
|
||||
break true;
|
||||
}
|
||||
break unknown.contains(&owner) && written.contains(&owner);
|
||||
};
|
||||
for p in path {
|
||||
alive_memo.insert(p, verdict);
|
||||
let alive_memo = settle(&hard, |kept: &HashMap<String, bool>| {
|
||||
let kept = |h: &str| kept.get(h) == Some(&true);
|
||||
let mut alive_memo: HashMap<String, bool> = HashMap::new();
|
||||
for o in f.objects.iter().filter(|o| is_proxy(&o.kind) && renumber(class_number(o.tags.iter().map(|r| (r.code, r.value.as_str())))).is_none())
|
||||
{
|
||||
alive_memo.insert(o.handle.hex(), false);
|
||||
}
|
||||
}
|
||||
for o in &f.objects {
|
||||
let mut path: Vec<String> = Vec::new();
|
||||
let mut cur = o.handle.hex();
|
||||
let verdict = loop {
|
||||
if let Some(&v) = alive_memo.get(&cur) {
|
||||
break v;
|
||||
}
|
||||
if path.contains(&cur) || path.len() >= MAX_DEPTH {
|
||||
break false;
|
||||
}
|
||||
path.push(cur.clone());
|
||||
let Some(owner) = owner_of(own.get(&cur).cloned().flatten(), &cur, &hard, kept) else { break false };
|
||||
if objs.contains_key(&owner) {
|
||||
cur = owner;
|
||||
continue;
|
||||
}
|
||||
if nod.contains(&owner) {
|
||||
break dict_entries.get(&cur).is_some_and(|k| !root_keys.contains(&up(k))) && root.is_some();
|
||||
}
|
||||
if attached.contains(&cur) {
|
||||
break true;
|
||||
}
|
||||
break unknown.contains(&owner) && written.contains(&owner);
|
||||
};
|
||||
for p in path {
|
||||
alive_memo.insert(p, verdict);
|
||||
}
|
||||
}
|
||||
alive_memo
|
||||
});
|
||||
// New handles for the written kept objects, above every written handle and the seed.
|
||||
let mut next = seed(tags).max(max_handle(tags).saturating_add(1));
|
||||
let mut alive: HashMap<String, String> = HashMap::new();
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
use cadcraft_io::{read, read_dxf, write, write_dxf};
|
||||
|
||||
/// A named object dictionary entry `OTHER_APP` (a dictionary) with an XRECORD that holds a second
|
||||
/// XRECORD by a 360 reference; the second one's owner is null.
|
||||
/// XRECORD by a 360 reference; the second one's owner is null. An object nothing keeps names the
|
||||
/// second XRECORD by a 360 first.
|
||||
fn source() -> String {
|
||||
let groups: &[(i32, &str)] = &[
|
||||
(0, "SECTION"),
|
||||
@@ -26,6 +27,15 @@ fn source() -> String {
|
||||
(100, "AcDbDictionary"),
|
||||
(3, "OTHER_APP"),
|
||||
(350, "50"),
|
||||
// An object nothing keeps (null owner, no holder) that also names the held record by a
|
||||
// 360, before the holder does: it is dropped, and the held record stays with the holder.
|
||||
(0, "XRECORD"),
|
||||
(5, "60"),
|
||||
(330, "0"),
|
||||
(100, "AcDbXrecord"),
|
||||
(280, "1"),
|
||||
(1, "stray"),
|
||||
(360, "52"),
|
||||
(0, "DICTIONARY"),
|
||||
(5, "50"),
|
||||
(330, "C"),
|
||||
@@ -76,6 +86,7 @@ fn check(text: &str, label: &str) {
|
||||
let Some((_, held_handle)) = holder.iter().find(|(c, _)| *c == "360") else { panic!("{label}: the holder holds nothing: {holder:?}") };
|
||||
let held = with("held");
|
||||
assert!(held.contains(&("5", *held_handle)), "{label}: the holder's 360 names the held record: {holder:?} {held:?}");
|
||||
assert!(!records.iter().any(|r| r.contains(&("1", "stray"))), "{label}: the object nothing keeps is dropped");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user