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 Seven 2026-07-13 16:28:21 +03:00
commit 04bd49c715
5 changed files with 227 additions and 32 deletions

View file

@ -550,27 +550,11 @@ pub fn set_saved_active_layout(doc: &mut CadDocument, name: &str) {
}
return;
}
// Attach a new CTAB entry to the root named-object dictionary. Prefer the
// header handle; fall back to scanning for the root dict (the one holding
// `ACAD_LAYOUT`) since a from-scratch document leaves the header handle null
// until it is built. No root dict at all → `$TILEMODE` still records
// model-vs-paper.
let named = doc.header.named_objects_dict_handle;
let root = if !named.is_null() && doc.objects.contains_key(&named) {
named
} else {
match doc.objects.iter().find_map(|(h, o)| match o {
ObjectType::Dictionary(d)
if d.entries.iter().any(|(k, _)| k.eq_ignore_ascii_case("ACAD_LAYOUT")) =>
{
Some(*h)
}
_ => None,
}) {
Some(h) => h,
None => return,
}
};
// Attach a new CTAB entry to the root named-object dictionary. Resolve it
// robustly (or synthesise one) so the current-tab record persists even on a
// from-scratch document, or a foreign DWG whose header root pointer is
// unresolvable. See `annotative::root_named_dict_handle`.
let root = crate::scene::annotative::root_named_dict_handle(doc);
let handle = doc.allocate_handle();
let mut var = DictionaryVariable::new("CTAB", name);
var.handle = handle;