forked from 1b-agent/opencadstudio-upstream
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 <[email protected]>
This commit is contained in:
1 parent
0c4272fbb0
commit
04bd49c715
5 files changed
+227
-32
No files matched your search
+5
-21
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
+5
-1
@@ -1967,7 +1967,11 @@ impl Scene {
|
||||
let scalelist_h = match self.scalelist_dict_handle() {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
let root_h = self.document.header.named_objects_dict_handle;
|
||||
// Resolve (or synthesise) the root robustly — a drawing whose
|
||||
// header root pointer is unresolvable would otherwise register
|
||||
// the new dictionary against a missing root and silently orphan
|
||||
// it. See `annotative::root_named_dict_handle`.
|
||||
let root_h = crate::scene::annotative::root_named_dict_handle(&mut self.document);
|
||||
let h = self.document.allocate_handle();
|
||||
let mut d = Dictionary::new();
|
||||
d.handle = h;
|
||||
|
||||
+42
-10
@@ -9,10 +9,34 @@ use acadrust::objects::{Dictionary, ObjectType, PlotSettings};
|
||||
use acadrust::Handle;
|
||||
|
||||
impl Scene {
|
||||
/// Handle of the `ACAD_PLOTSETTINGS` dictionary, located robustly.
|
||||
///
|
||||
/// The canonical path is the header's `acad_plotsettings_dict_handle`, but
|
||||
/// DWGs written by other programs don't always leave that pointer resolvable
|
||||
/// (the header handle points at no loaded dictionary — see
|
||||
/// [`crate::scene::annotative::root_named_dict_handle`]). In that case fall
|
||||
/// back to the dictionary that owns the drawing's `PlotSettings` objects,
|
||||
/// mirroring [`Scene::scalelist_dict_handle`]. Returns `None` when the
|
||||
/// drawing genuinely has no named page setups.
|
||||
fn plotsettings_dict_handle(&self) -> Option<Handle> {
|
||||
let dh = self.document.header.acad_plotsettings_dict_handle;
|
||||
if matches!(self.document.objects.get(&dh), Some(ObjectType::Dictionary(_))) {
|
||||
return Some(dh);
|
||||
}
|
||||
let owner = self.document.objects.values().find_map(|o| match o {
|
||||
ObjectType::PlotSettings(ps) => Some(ps.owner),
|
||||
_ => None,
|
||||
})?;
|
||||
matches!(
|
||||
self.document.objects.get(&owner),
|
||||
Some(ObjectType::Dictionary(_))
|
||||
)
|
||||
.then_some(owner)
|
||||
}
|
||||
|
||||
/// Names of the document's named page setups, in dictionary order.
|
||||
pub fn page_setup_names(&self) -> Vec<String> {
|
||||
let dh = self.document.header.acad_plotsettings_dict_handle;
|
||||
match self.document.objects.get(&dh) {
|
||||
match self.plotsettings_dict_handle().and_then(|h| self.document.objects.get(&h)) {
|
||||
Some(ObjectType::Dictionary(d)) => d.entries.iter().map(|(k, _)| k.clone()).collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
@@ -20,7 +44,7 @@ impl Scene {
|
||||
|
||||
/// Clone the named page setup's `PlotSettings`, or `None` if absent.
|
||||
pub fn page_setup_get(&self, name: &str) -> Option<PlotSettings> {
|
||||
let dh = self.document.header.acad_plotsettings_dict_handle;
|
||||
let dh = self.plotsettings_dict_handle()?;
|
||||
let ObjectType::Dictionary(d) = self.document.objects.get(&dh)? else {
|
||||
return None;
|
||||
};
|
||||
@@ -32,13 +56,17 @@ impl Scene {
|
||||
}
|
||||
|
||||
/// Handle of the `ACAD_PLOTSETTINGS` dictionary, creating it (and its entry
|
||||
/// in the root named-objects dictionary) if the drawing has none yet.
|
||||
/// in the root named-objects dictionary) if the drawing has none yet. The
|
||||
/// root itself is resolved (or synthesised) robustly so registration
|
||||
/// persists even on drawings whose header root pointer is unresolvable.
|
||||
fn ensure_plotsettings_dict(&mut self) -> Handle {
|
||||
let dh = self.document.header.acad_plotsettings_dict_handle;
|
||||
if matches!(self.document.objects.get(&dh), Some(ObjectType::Dictionary(_))) {
|
||||
if let Some(dh) = self.plotsettings_dict_handle() {
|
||||
// Keep the header pointer in sync with the located dictionary so the
|
||||
// writer and later reads agree on it.
|
||||
self.document.header.acad_plotsettings_dict_handle = dh;
|
||||
return dh;
|
||||
}
|
||||
let root = self.document.header.named_objects_dict_handle;
|
||||
let root = crate::scene::annotative::root_named_dict_handle(&mut self.document);
|
||||
let mut dict = Dictionary::new();
|
||||
dict.handle = self.document.allocate_handle();
|
||||
dict.owner = root;
|
||||
@@ -56,7 +84,7 @@ impl Scene {
|
||||
|
||||
/// Handle of the named page setup, if it exists.
|
||||
fn page_setup_handle(&self, name: &str) -> Option<Handle> {
|
||||
let dh = self.document.header.acad_plotsettings_dict_handle;
|
||||
let dh = self.plotsettings_dict_handle()?;
|
||||
let ObjectType::Dictionary(d) = self.document.objects.get(&dh)? else {
|
||||
return None;
|
||||
};
|
||||
@@ -85,7 +113,9 @@ impl Scene {
|
||||
|
||||
/// Remove the named page setup (object + dictionary entry). No-op if absent.
|
||||
pub fn page_setup_delete(&mut self, name: &str) {
|
||||
let dict_handle = self.document.header.acad_plotsettings_dict_handle;
|
||||
let Some(dict_handle) = self.plotsettings_dict_handle() else {
|
||||
return;
|
||||
};
|
||||
let handle = if let Some(ObjectType::Dictionary(d)) =
|
||||
self.document.objects.get_mut(&dict_handle)
|
||||
{
|
||||
@@ -109,7 +139,9 @@ impl Scene {
|
||||
if self.page_setup_handle(new).is_some() {
|
||||
return; // name collision
|
||||
}
|
||||
let dict_handle = self.document.header.acad_plotsettings_dict_handle;
|
||||
let Some(dict_handle) = self.plotsettings_dict_handle() else {
|
||||
return;
|
||||
};
|
||||
let mut renamed = None;
|
||||
if let Some(ObjectType::Dictionary(d)) = self.document.objects.get_mut(&dict_handle) {
|
||||
if let Some(e) = d.entries.iter_mut().find(|(k, _)| k == old) {
|
||||
|
||||
Reference in new issue
Block a user