fix(plot): write page setups in the file format's canonical form

One writer (plot_settings_from_dialog) now serves both the layout's embedded
settings and named page setups, so the two can no longer drift apart:

- media are stored as the portrait sheet with the orientation in the
  rotation code (0/90/180/270; upside-down adds a half turn) instead of
  swapped width/height with rotation limited to 0/180
- standard scales are written by their code (1:1 … 100:1) with matching
  numerator/denominator; other scales as a readable custom ratio in the
  layout's own paper unit, converting from the drawing's scale-family unit
  (a metre drawing plotting "1:100" stores 10 mm = 1 unit, an inch layout
  stores 1 in = 2540 units); reading converts back and keeps a ratio the
  scale list has no name for instead of snapping to 1:1
- the layout's paper unit and every flag the dialog does not edit survive a
  round trip untouched; layout limits follow the paper-space unit
- new drawings, new layouts, CLEAR and the automation "new" op share one
  default page setup (canonical A4 name, rotation 90, none_device, 1:1
  layout plot) through Scene::reset_to_new_drawing

Scene gains scale_family_unit_mm, scale_ratio and a scale-name ratio parser
(ratios and architectural names). Round-trip tests cover orientation,
standard/custom/fit scales, unit conversion, named-setup parity, preserved
flags and the default layout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
PanosandClaude Opus 5 committed 2026-09-17 18:04:38 +03:00
1 parent ef0c6bf403
commit 276e7c7bf1
8 files changed
+641 -263

No files matched your search

+73
View File
@@ -33,6 +33,7 @@ pub(crate) mod centermark;
pub(crate) mod dimension_assoc;
pub(crate) mod dimension_assoc_chain;
pub use dimension_assoc::{ReferenceStatus, ResolvedReference};
pub use page_setup::apply_default_page_setup;
mod dwg_native_constraints;
mod entity;
#[cfg(test)]
@@ -4953,6 +4954,78 @@ impl Scene {
.then_some(owner)
}
/// Millimetres in one unit of the paper side of this drawing's scale
/// family: 25.4 for an imperial drawing (whose `1/8" = 1'-0"` style scales
/// measure paper in inches), 1 for a metric one. Plot-scale factors built
/// from [`Scene::scale_list`] are expressed per this unit; page setups
/// store theirs per the layout's own paper unit, so the two are converted
/// through this value.
pub(crate) fn scale_family_unit_mm(&self) -> f64 {
if self.prefers_imperial_scales() == Some(true) {
25.4
} else {
1.0
}
}
/// The `paper : drawing` ratio behind a scale-list name, from the
/// drawing's own `Scale` object when it has one, otherwise from the
/// built-in family. A ratio keeps the plot scale readable in a page setup
/// (`1 : 250` rather than `0.004 : 1`).
pub(crate) fn scale_ratio(&self, name: &str) -> Option<(f64, f64)> {
use acadrust::objects::ObjectType;
let stored = self.document.objects.values().find_map(|o| match o {
ObjectType::Scale(s) if !s.is_temporary && s.name.eq_ignore_ascii_case(name) => {
Some((s.paper_units, s.drawing_units))
}
_ => None,
});
stored
.or_else(|| Self::parse_scale_name_ratio(name))
.filter(|(paper, drawing)| *paper > 0.0 && *drawing > 0.0)
}
/// The `paper : drawing` ratio spelled by a scale name: `1:250` gives
/// `(1, 250)`, an architectural `1/8" = 1'-0"` gives `(0.125, 12)` in
/// inches. Anything else is not a scale name.
pub(crate) fn parse_scale_name_ratio(name: &str) -> Option<(f64, f64)> {
if let Some((paper, drawing)) = name.split_once(':') {
let paper: f64 = paper.trim().parse().ok()?;
let drawing: f64 = drawing.trim().parse().ok()?;
return (paper > 0.0 && drawing > 0.0).then_some((paper, drawing));
}
let (paper, drawing) = name.split_once('=')?;
let paper = Self::parse_feet_inches(paper)?;
let drawing = Self::parse_feet_inches(drawing)?;
(paper > 0.0 && drawing > 0.0).then_some((paper, drawing))
}
/// Inches in a `F'-I"` / `I"` / `F'` length, where the inch part may be a
/// fraction (`3/32"`).
fn parse_feet_inches(text: &str) -> Option<f64> {
let text = text.trim();
let (feet, inches) = match text.split_once('\'') {
Some((feet, rest)) => (
feet.trim().parse::<f64>().ok()?,
rest.trim_start_matches('-').trim(),
),
None => (0.0, text),
};
let inches = inches.trim_end_matches('"').trim();
let inches = if inches.is_empty() {
0.0
} else if let Some((num, den)) = inches.split_once('/') {
let den: f64 = den.trim().parse().ok()?;
if den <= 0.0 {
return None;
}
num.trim().parse::<f64>().ok()? / den
} else {
inches.parse::<f64>().ok()?
};
Some(feet * 12.0 + inches)
}
/// Handle of the real (non-temporary) `Scale` object with this display name.
/// Scales are matched by their `name` field, not the dictionary key — some
/// files key the entries by an internal identifier (`*A1`, `A0`, …) that