Files
OneCAD-Draft/src/scene/page_setup.rs
T

286 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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 codec::objects::{Dictionary, Layout, ObjectType, PlotSettings};
use codec::{CadDocument, Handle};
/// Give a freshly created paper layout the page setup a new drawing starts
/// with — ISO A4 landscape on no plotter, plotted 1:1 as a layout — stored the
/// way the file format expects it: the portrait medium under its canonical
/// name with the orientation in the rotation code. `plot_style` names the
/// plot style table to attach, or is empty.
pub fn apply_default_page_setup(layout: &mut Layout, plot_style: &str) {
use codec::objects::{PlotPaperUnits, PlotRotation, PlotType, ScaledType};
let a4 = crate::io::paper_catalog::default_paper();
let (paper_width, paper_height) = a4.portrait_mm();
layout.min_limits = (0.0, 0.0);
layout.max_limits = (paper_height, paper_width);
layout.min_extents = (0.0, 0.0, 0.0);
layout.max_extents = (paper_height, paper_width, 0.0);
layout.paper_width = paper_width;
layout.paper_height = paper_height;
layout.plot_rotation = PlotRotation::Degrees90.to_code();
layout.plot_paper_units = PlotPaperUnits::Millimeters.to_code();
layout.plot_type = PlotType::Layout.to_code();
layout.plot_scale_type = ScaledType::OneToOne.to_code();
layout.plot_scale_numerator = 1.0;
layout.plot_scale_denominator = 1.0;
layout.plot_scale_factor = 1.0;
layout.plot_flags.use_standard_scale = true;
layout.plot_flags.print_lineweights = true;
layout.plot_flags.draw_viewports_first = true;
layout.plot_flags.plot_plot_styles = !plot_style.is_empty();
layout.plot_flags.show_plot_styles = !plot_style.is_empty();
layout.plot_style_sheet = plot_style.to_string();
layout.paper_size = a4.canonical.to_string();
layout.plot_printer_name = crate::io::plot_device::PlotDevice::None.canonical_name();
}
/// The unprintable margins of a sheet as they appear on the rotated layout,
/// `(left, bottom, right, top)`. Stored margins belong to the medium's own
/// edges; a plot rotated by a quarter turn (`rotation` is the file's 0–3
/// code) shows the medium turned on the layout, so each margin moves to the
/// edge its side lands on — verified against limits written by the
/// commercial application: at 90° the medium's top margin is the layout's
/// left one and its left margin the layout's bottom one.
pub fn rotated_margins(
(left, bottom, right, top): (f64, f64, f64, f64),
rotation: i16,
) -> (f64, f64, f64, f64) {
match rotation {
1 => (top, left, bottom, right),
2 => (right, top, left, bottom),
3 => (bottom, right, top, left),
_ => (left, bottom, right, top),
}
}
/// Handle of a document's `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_in(document: &CadDocument) -> Option<Handle> {
let dh = document.header.acad_plotsettings_dict_handle;
if matches!(document.objects.get(&dh), Some(ObjectType::Dictionary(_))) {
return Some(dh);
}
let owner = document.objects.values().find_map(|o| match o {
ObjectType::PlotSettings(ps) => Some(ps.owner),
_ => None,
})?;
matches!(
document.objects.get(&owner),
Some(ObjectType::Dictionary(_))
)
.then_some(owner)
}
/// The named page setups of any document — `(name, settings)` in dictionary
/// order. Reads a drawing that is not open in the editor (the source of a
/// `PSETUPIN` import) the same way the editor reads its own.
pub fn document_page_setups(document: &CadDocument) -> Vec<(String, PlotSettings)> {
let Some(dh) = plotsettings_dict_handle_in(document) else {
return Vec::new();
};
let Some(ObjectType::Dictionary(dict)) = document.objects.get(&dh) else {
return Vec::new();
};
dict.entries
.iter()
.filter_map(|(name, handle)| match document.objects.get(handle) {
Some(ObjectType::PlotSettings(ps)) => Some((name.clone(), ps.clone())),
_ => None,
})
.collect()
}
/// A page setup copied out of another drawing, made safe for this one: the
/// handles it pointed at (a plot view, a visual style) meant something only
/// in its source, so they are dropped while the names, which travel, stay.
pub fn detach_page_setup(ps: &mut PlotSettings) {
ps.plot_view_handle = Handle::NULL;
ps.visual_style_handle = Handle::NULL;
ps.reactors.clear();
ps.xdictionary_handle = None;
}
impl Scene {
fn plotsettings_dict_handle(&self) -> Option<Handle> {
plotsettings_dict_handle_in(&self.document)
}
/// 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(),
}
}
/// Bring a page setup from another drawing in under `name`, replacing
/// one of that name the way `PSETUPIN` redefines it.
pub fn import_page_setup(&mut self, name: &str, mut ps: PlotSettings) {
detach_page_setup(&mut ps);
self.page_setup_save(name, ps);
}
/// 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.eq_ignore_ascii_case(name))
.map(|(_, h)| *h)
}
/// The dictionary's own spelling of `name` (page-setup names compare
/// case-insensitively, like every named-object dictionary key).
fn page_setup_key(&self, name: &str) -> Option<String> {
let dh = self.plotsettings_dict_handle()?;
let ObjectType::Dictionary(d) = self.document.objects.get(&dh)? else {
return None;
};
d.entries
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(name))
.map(|(k, _)| k.clone())
}
/// 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();
// Replacing keeps the entry's stored spelling so the dictionary key
// and the object's page_name stay one name.
ps.page_name = self
.page_setup_key(name)
.unwrap_or_else(|| 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.eq_ignore_ascii_case(name))
.map(|(_, h)| *h);
d.entries.retain(|(k, _)| !k.eq_ignore_ascii_case(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;
}
// A different spelling of the same name is a rename, not a collision.
if !old.eq_ignore_ascii_case(new) && 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.eq_ignore_ascii_case(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();
}
}
}
}