fix(io): resolve root named-objects dict robustly on foreign DWGs

Some DWGs written by other programs leave the root named-objects
dictionary unrecoverable: the header pointer names a non-dictionary and
the real NOD child dicts are owned by a table-control handle, so nothing
has owner==NULL. Navigating the root then silently no-ops, and
registering a new named-object entry (page setup, annotation scale, CTAB)
against the missing root orphans it — it never round-trips.

Add shared annotative::root_named_dict_handle(): resolve the header
pointer, else adopt the richest owner==NULL dict, else synthesise a fresh
root and write it back. Route ensure_plotsettings_dict, add_scale's
scalelist-create branch, and set_saved_active_layout through it. Resolve
the plot-settings dict via the PlotSettings owner when the header pointer
is stale, mirroring scalelist_dict_handle.

Verified page setups survive a DWG save/reload on the repro file with
layouts intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-13 16:28:21 +03:00
1 parent 0c4272fbb0
commit 04bd49c715
5 files changed
+227 -32

No files matched your search

+47
View File
@@ -18,6 +18,53 @@ pub fn as_dict(doc: &CadDocument, handle: Handle) -> Option<&Dictionary> {
}
}
/// Resolve the drawing's root named-objects dictionary, creating one if the
/// file has none reachable.
///
/// The canonical pointer is `header.named_objects_dict_handle`, but DWGs
/// written by some programs leave it dangling — pointing at a handle that never
/// loaded, or at a non-dictionary — while the real named-object sub-dictionaries
/// (`ACAD_LAYOUT`, `ACAD_SCALELIST`, …) are instead owned by an unrelated handle
/// that is not a dictionary. When the pointer can't be resolved we adopt any
/// top-level (`owner == NULL`) dictionary as the root; failing that we synthesise
/// a fresh, empty root so that *registering* a new named-object entry (a page
/// setup, an annotation scale, the `CTAB` variable) actually persists instead of
/// silently no-opping against a missing dictionary.
///
/// Idempotent: the resolved or created handle is written back to the header, so
/// later calls return the same dictionary rather than minting another root. On a
/// well-formed drawing this returns the existing root untouched.
pub fn root_named_dict_handle(doc: &mut CadDocument) -> Handle {
let h = doc.header.named_objects_dict_handle;
if matches!(doc.objects.get(&h), Some(ObjectType::Dictionary(_))) {
return h;
}
// A top-level dictionary is already present (the standard root shape) — adopt
// the richest one (matching the DWG writer's own root heuristic) and repair
// the stale header pointer.
if let Some(root) = doc
.objects
.iter()
.filter_map(|(k, o)| match o {
ObjectType::Dictionary(d) if d.owner.is_null() => Some((*k, d.entries.len())),
_ => None,
})
.max_by_key(|&(_, n)| n)
.map(|(k, _)| k)
{
doc.header.named_objects_dict_handle = root;
return root;
}
// Nothing reachable — build a fresh, empty root named-objects dictionary.
let nh = doc.allocate_handle();
let mut d = Dictionary::new();
d.handle = nh;
d.owner = Handle::NULL;
doc.objects.insert(nh, ObjectType::Dictionary(d));
doc.header.named_objects_dict_handle = nh;
nh
}
/// Set the per-object annotative flag on the entity types that carry one
/// (MTEXT, MULTILEADER). Turning it off also strips the per-object annotation
/// context and legacy markers via [`clear_annotation_context`] so the object