forked from 1b-agent/opencadstudio-upstream
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>
159 lines
6.7 KiB
Rust
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();
|
|
}
|
|
}
|
|
}
|
|
}
|