Files
OneCAD-Draft/src/scene/page_setup.rs
T
Hakan SevenandClaude Opus 4.8 04bd49c715 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>
2026-07-13 16:28:21 +03:00

159 lines
6.7 KiB
Rust

//! Named page setups — the reusable plot configurations a drawing stores in
//! its `ACAD_PLOTSETTINGS` dictionary (each a standalone `PlotSettings` object
//! keyed by name). These are distinct from a layout's own embedded plot
//! settings: a named setup can be applied to any layout. CRUD here keeps the
//! dictionary + objects consistent so the result round-trips through DWG/DXF.
use super::Scene;
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> {
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(),
}
}
/// Clone the named page setup's `PlotSettings`, or `None` if absent.
pub fn page_setup_get(&self, name: &str) -> Option<PlotSettings> {
let dh = self.plotsettings_dict_handle()?;
let ObjectType::Dictionary(d) = self.document.objects.get(&dh)? else {
return None;
};
let h = d.entries.iter().find(|(k, _)| k == name).map(|(_, h)| *h)?;
match self.document.objects.get(&h)? {
ObjectType::PlotSettings(ps) => Some(ps.clone()),
_ => None,
}
}
/// Handle of the `ACAD_PLOTSETTINGS` dictionary, creating it (and its entry
/// 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 {
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 = crate::scene::annotative::root_named_dict_handle(&mut self.document);
let mut dict = Dictionary::new();
dict.handle = self.document.allocate_handle();
dict.owner = root;
let new_handle = dict.handle;
self.document
.objects
.insert(new_handle, ObjectType::Dictionary(dict));
self.document.header.acad_plotsettings_dict_handle = new_handle;
if let Some(ObjectType::Dictionary(rd)) = self.document.objects.get_mut(&root) {
rd.entries.retain(|(k, _)| k != "ACAD_PLOTSETTINGS");
rd.entries.push(("ACAD_PLOTSETTINGS".to_string(), new_handle));
}
new_handle
}
/// Handle of the named page setup, if it exists.
fn page_setup_handle(&self, name: &str) -> Option<Handle> {
let dh = self.plotsettings_dict_handle()?;
let ObjectType::Dictionary(d) = self.document.objects.get(&dh)? else {
return None;
};
d.entries.iter().find(|(k, _)| k == name).map(|(_, h)| *h)
}
/// Create or update the named page setup from `ps` (its `page_name` and
/// `owner` are set here). Existing entries are updated in place; new ones
/// are inserted and registered in the dictionary.
pub fn page_setup_save(&mut self, name: &str, mut ps: PlotSettings) {
let dict_handle = self.ensure_plotsettings_dict();
ps.page_name = name.to_string();
ps.owner = dict_handle;
if let Some(h) = self.page_setup_handle(name) {
ps.handle = h;
self.document.objects.insert(h, ObjectType::PlotSettings(ps));
} else {
ps.handle = self.document.allocate_handle();
let h = ps.handle;
self.document.objects.insert(h, ObjectType::PlotSettings(ps));
if let Some(ObjectType::Dictionary(d)) = self.document.objects.get_mut(&dict_handle) {
d.entries.push((name.to_string(), h));
}
}
}
/// Remove the named page setup (object + dictionary entry). No-op if absent.
pub fn page_setup_delete(&mut self, name: &str) {
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)
{
let h = d.entries.iter().find(|(k, _)| k == name).map(|(_, h)| *h);
d.entries.retain(|(k, _)| k != name);
h
} else {
None
};
if let Some(h) = handle {
self.document.objects.remove(&h);
}
}
/// Rename a named page setup (dictionary key + the object's `page_name`).
/// No-op if `old` is absent or `new` already exists.
pub fn page_setup_rename(&mut self, old: &str, new: &str) {
if old == new || new.trim().is_empty() {
return;
}
if self.page_setup_handle(new).is_some() {
return; // name collision
}
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) {
e.0 = new.to_string();
renamed = Some(e.1);
}
}
if let Some(h) = renamed {
if let Some(ObjectType::PlotSettings(ps)) = self.document.objects.get_mut(&h) {
ps.page_name = new.to_string();
}
}
}
}