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 <[email protected]>
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

+1 -1
View File
@@ -294,7 +294,7 @@ impl OpenCADStudio {
match req["op"].as_str().unwrap_or("") {
"new" => {
let i = self.active_tab;
self.tabs[i].scene.clear();
self.tabs[i].scene.reset_to_new_drawing();
self.tabs[i].scene.material_base_dir = None;
self.tabs[i].current_path = None;
// The headless session starts on the welcome (Start) tab, which
+1 -19
View File
@@ -1,5 +1,4 @@
use crate::command::CadCommand;
use crate::io::linetypes;
use crate::modules::draw::modify::block_edit::BlockEditSession;
use crate::modules::draw::modify::refedit::RefEditSession;
use crate::scene::pick::grip::GripEdit;
@@ -567,24 +566,7 @@ impl DocumentTab {
pub(super) fn new_drawing(n: usize) -> Self {
let mut scene = Scene::new();
linetypes::populate_document(&mut scene.document);
// Paper layouts start as A4 landscape.
for obj in scene.document.objects.values_mut() {
if let acadrust::objects::ObjectType::Layout(l) = obj {
if l.name != "Model" {
l.min_limits = (0.0, 0.0);
l.max_limits = (297.0, 210.0);
l.min_extents = (0.0, 0.0, 0.0);
l.max_extents = (297.0, 210.0, 0.0);
l.paper_width = 297.0;
l.paper_height = 210.0;
l.plot_paper_units = 1;
l.plot_scale_numerator = 1.0;
l.plot_scale_denominator = 1.0;
l.paper_size = "ISO_A4_(297.00_x_210.00_MM)".into();
}
}
}
scene.populate_new_drawing_defaults();
Self {
id: NEXT_DOCUMENT_TAB_ID.fetch_add(1, Ordering::Relaxed),
scene,
+510 -222
View File
@@ -152,6 +152,68 @@ fn paper_name_for_settings(
}
}
/// The stored rotation for the dialog's sheet. Media are stored portrait, so
/// a landscape sheet is the portrait medium turned a quarter, and
/// "upside-down" adds a half turn on top (180° portrait, 270° landscape) —
/// the form every other reader of the file expects.
fn plot_dialog_rotation(
d: &crate::ui::window::plot::PlotDialogState,
) -> acadrust::objects::PlotRotation {
use crate::io::paper_catalog::Orientation;
use acadrust::objects::PlotRotation;
match (plot_dialog_orientation(d), d.upside_down) {
(Orientation::Portrait, false) => PlotRotation::None,
(Orientation::Landscape, false) => PlotRotation::Degrees90,
(Orientation::Portrait, true) => PlotRotation::Degrees180,
(Orientation::Landscape, true) => PlotRotation::Degrees270,
}
}
/// The file format's standard scale for a `paper : drawing` ratio, when it
/// has one. Only the metric ratios exist as standard codes here; an
/// architectural scale is stored as a custom ratio with the same value.
fn standard_scale_for_ratio(paper: f64, drawing: f64) -> Option<acadrust::objects::ScaledType> {
use acadrust::objects::ScaledType as S;
const STANDARD: &[(f64, f64, S)] = &[
(1.0, 1.0, S::OneToOne),
(1.0, 2.0, S::OneToTwo),
(1.0, 4.0, S::OneToFour),
(1.0, 8.0, S::OneToEight),
(1.0, 10.0, S::OneToTen),
(1.0, 16.0, S::OneToSixteen),
(1.0, 20.0, S::OneToTwenty),
(1.0, 30.0, S::OneToThirty),
(1.0, 40.0, S::OneToForty),
(1.0, 50.0, S::OneToFifty),
(1.0, 100.0, S::OneToHundred),
(2.0, 1.0, S::TwoToOne),
(4.0, 1.0, S::FourToOne),
(8.0, 1.0, S::EightToOne),
(10.0, 1.0, S::TenToOne),
(100.0, 1.0, S::HundredToOne),
];
let ratio = paper / drawing;
STANDARD
.iter()
.find(|(p, d, _)| (p / d - ratio).abs() <= 1e-9 * ratio.abs().max(1.0))
.map(|(_, _, scale)| *scale)
}
/// A scale-list name for a plot scale the drawing has no name for, spelled as
/// the ratio it is (`1:250`, `25.4:1`) so the value survives in the picker
/// instead of collapsing to 1:1.
fn ratio_scale_name(factor: f64) -> String {
let trim = |value: f64| {
let text = format!("{value:.4}");
text.trim_end_matches('0').trim_end_matches('.').to_string()
};
if factor >= 1.0 {
format!("{}:1", trim(factor))
} else {
format!("1:{}", trim(1.0 / factor))
}
}
/// The plot device the dialog currently targets.
fn plot_dialog_device(d: &crate::ui::window::plot::PlotDialogState) -> crate::io::plot_device::PlotDevice {
use crate::io::plot_device::PlotDevice;
@@ -3004,127 +3066,187 @@ impl OpenCADStudio {
iced::exit()
}
/// Write the given plot page settings into the active layout.
/// No-op on the Model tab (which has no paper layout). Marks the tab dirty
/// and re-tessellates the sheet. Called by the Plot dialog's Set current action.
#[allow(clippy::too_many_arguments)]
pub(super) fn apply_plot_page_settings(
&mut self,
w: f64,
h: f64,
plot_area: &str,
center: bool,
offset_x: f64,
offset_y: f64,
rotation: i16,
) {
let i = self.active_tab;
let dialog = self.plot_dialog.clone();
let layout_name = self.tabs[i].scene.current_layout.clone();
let plot_window = self.plot_window;
if layout_name != "Model" {
let w: f64 = w.max(1.0);
let h: f64 = h.max(1.0);
use acadrust::objects::{
PlotRotation, PlotSettings, PlotType, ScaledType, ShadePlotMode,
ShadePlotResolutionLevel,
};
let mut ps = self
.plot_setup_template
.clone()
.or_else(|| self.tabs[i].scene.plot_settings_for(&layout_name))
.unwrap_or_else(|| PlotSettings::new(""));
// Names and margins are decided against the stored settings before
// any field is overwritten.
let paper_size = paper_name_for_settings(&dialog, &ps);
let printer_name = device_name_for_settings(&dialog, &ps);
let margins = margins_for_settings(&dialog, &ps);
ps.paper_size = paper_size;
ps.printer_name = printer_name;
ps.margins = margins;
ps.paper_width = w;
ps.paper_height = h;
ps.plot_type = match plot_area {
"Window" => PlotType::Window,
"Display" => PlotType::LastScreenDisplay,
"Extents" => PlotType::Extents,
"Limits" => PlotType::Limits,
area if area.starts_with("View: ") => PlotType::View,
_ => PlotType::Layout,
};
ps.plot_view_name = plot_area.strip_prefix("View: ").unwrap_or("").to_string();
if plot_area == "Window" {
if let Some((x0, y0, x1, y1)) = plot_window {
ps.set_plot_window(x0, y0, x1, y1);
}
}
ps.flags.plot_centered = center && plot_area != "Layout";
ps.origin_x = if plot_area == "Layout" { 0.0 } else { offset_x };
ps.origin_y = if plot_area == "Layout" { 0.0 } else { offset_y };
ps.rotation = match rotation {
90 => PlotRotation::Degrees90,
180 => PlotRotation::Degrees180,
270 => PlotRotation::Degrees270,
_ => PlotRotation::None,
};
if dialog.fit_to_paper && plot_area != "Layout" {
ps.set_scale_to_fit();
} else if plot_area == "Layout" {
ps.set_standard_scale(ScaledType::OneToOne);
ps.standard_scale_factor = 1.0;
} else {
let factor = plot_dialog_scale_factor(&dialog);
ps.scale_type = ScaledType::CustomScale;
ps.scale_numerator = factor;
ps.scale_denominator = 1.0;
ps.standard_scale_factor = factor;
ps.flags.use_standard_scale = false;
}
ps.current_style_sheet = dialog.style_name.clone();
ps.flags.scale_lineweights = dialog.scale_lw;
ps.flags.print_lineweights = dialog.lineweights;
ps.flags.plot_plot_styles = dialog.apply_plot_styles && !dialog.style_name.is_empty();
ps.flags.show_plot_styles = dialog.show_plot_styles && !dialog.style_name.is_empty();
ps.flags.draw_viewports_first = dialog.paperspace_last;
ps.flags.plot_hidden = dialog.shade == "Hidden Line";
ps.shade_plot_mode = match dialog.shade.as_str() {
"2D Wireframe" | "3D Wireframe" => ShadePlotMode::Wireframe,
"Hidden Line" => ShadePlotMode::Hidden,
"As displayed" => ShadePlotMode::AsDisplayed,
_ => ShadePlotMode::Rendered,
};
ps.shade_plot_resolution = match dialog.quality.as_str() {
"Low" => ShadePlotResolutionLevel::Draft,
"High" => ShadePlotResolutionLevel::Presentation,
_ => ShadePlotResolutionLevel::Normal,
};
ps.shade_plot_dpi = 300;
/// The settings the dialog edits on top of: the named page setup it was
/// loaded from, else the active layout's own, else a fresh metric sheet.
/// Everything the dialog has no control for (viewport-border and
/// paper-update flags, the plot view handle, the paper unit that fixes
/// the layout's coordinate system) is carried over from here untouched.
fn dialog_base_settings(&self) -> acadrust::objects::PlotSettings {
use acadrust::objects::{PlotPaperUnits, PlotSettings};
let scene = &self.tabs[self.active_tab].scene;
self.plot_setup_template
.clone()
.or_else(|| scene.plot_settings_for(&scene.current_layout))
.unwrap_or_else(|| {
let mut settings = PlotSettings::new("");
settings.paper_units = PlotPaperUnits::Millimeters;
settings
})
}
self.tabs[i]
.scene
.set_layout_plot_settings(&layout_name, &ps);
for obj in self.tabs[i].scene.document.objects.values_mut() {
if let acadrust::objects::ObjectType::Layout(layout) = obj {
if layout.name == layout_name {
layout.min_limits = (0.0, 0.0);
layout.max_limits = (w, h);
layout.min_extents = (0.0, 0.0, 0.0);
layout.max_extents = (w, h, 0.0);
break;
/// The page setup the dialog describes, written the way the file format
/// expects it: the portrait medium with the orientation in `rotation`,
/// standard scales by their code, custom ones as a readable ratio in the
/// layout's own paper unit. Named page setups and the layout's embedded
/// settings both come from here so the two can never drift apart.
fn plot_settings_from_dialog(
&self,
mut ps: acadrust::objects::PlotSettings,
) -> acadrust::objects::PlotSettings {
use acadrust::objects::{
PlotPaperUnits, PlotType, ScaledType, ShadePlotMode, ShadePlotResolutionLevel,
};
let d = &self.plot_dialog;
let scene = &self.tabs[self.active_tab].scene;
// Names and margins are decided against the stored settings before
// any field is overwritten.
let paper_size = paper_name_for_settings(d, &ps);
let printer_name = device_name_for_settings(d, &ps);
let margins = margins_for_settings(d, &ps);
ps.paper_size = paper_size;
ps.printer_name = printer_name;
ps.margins = margins;
let (width, height) = plot_dialog_paper(d).portrait_mm();
ps.paper_width = width.max(1.0);
ps.paper_height = height.max(1.0);
ps.rotation = plot_dialog_rotation(d);
let layout_area = d.area == "Layout";
ps.plot_type = match d.area.as_str() {
"Window" => PlotType::Window,
"Layout" => PlotType::Layout,
"Display" => PlotType::LastScreenDisplay,
"Limits" => PlotType::Limits,
area if area.starts_with("View: ") => PlotType::View,
_ => PlotType::Extents,
};
ps.plot_view_name = d.area.strip_prefix("View: ").unwrap_or("").to_string();
if d.area == "Window" {
if let Some((x0, y0, x1, y1)) = self.plot_window {
ps.set_plot_window(x0, y0, x1, y1);
}
}
ps.flags.plot_centered = d.center && !layout_area;
ps.origin_x = if layout_area {
0.0
} else {
d.offset_x.parse::<f64>().unwrap_or(0.0)
};
ps.origin_y = if layout_area {
0.0
} else {
d.offset_y.parse::<f64>().unwrap_or(0.0)
};
if layout_area {
ps.set_standard_scale(ScaledType::OneToOne);
ps.scale_numerator = 1.0;
ps.scale_denominator = 1.0;
ps.standard_scale_factor = 1.0;
} else if d.fit_to_paper {
ps.set_scale_to_fit();
} else {
// The dialog's factor is per the drawing's scale-family unit
// (inches for an imperial drawing, else millimetres); the file
// stores it per the layout's paper unit. A named ratio is kept
// as written when no unit conversion hides inside the factor —
// a metre drawing plotting "1:100" really plots 10 mm per unit,
// which is what the file must say.
let factor = plot_dialog_scale_factor(d);
let page_unit_mm = match ps.paper_units {
PlotPaperUnits::Inches => 25.4,
_ => 1.0,
};
let conversion = scene.scale_family_unit_mm() / page_unit_mm;
let named = scene.scale_ratio(&d.scale).filter(|(paper, drawing)| {
(paper / drawing - factor).abs() <= 1e-9 * factor.abs().max(1.0)
});
let (numerator, denominator, standard) = match named {
Some((paper, drawing)) if (conversion - 1.0).abs() <= 1e-12 => {
(paper, drawing, standard_scale_for_ratio(paper, drawing))
}
_ => {
let per_unit = (factor * conversion).max(1e-9);
if per_unit >= 1.0 {
(per_unit, 1.0, None)
} else {
(1.0, 1.0 / per_unit, None)
}
}
};
match standard {
Some(scale) => ps.set_standard_scale(scale),
None => {
ps.set_custom_scale(numerator, denominator);
ps.flags.use_standard_scale = false;
}
}
self.tabs[i].dirty = true;
self.tabs[i].scene.bump_geometry_no_blocks();
self.command_line.push_info(
crate::tf!(
"Page setup: {w:.1}×{h:.1} mm area={plot_area} \
center={center} rot={rotation}°"
)
.as_ref(),
);
ps.scale_numerator = numerator;
ps.scale_denominator = denominator;
ps.standard_scale_factor = numerator / denominator;
}
ps.current_style_sheet = d.style_name.clone();
ps.flags.scale_lineweights = d.scale_lw;
ps.flags.print_lineweights = d.lineweights;
ps.flags.plot_plot_styles = d.apply_plot_styles && !d.style_name.is_empty();
ps.flags.show_plot_styles = d.show_plot_styles && !d.style_name.is_empty();
ps.flags.draw_viewports_first = d.paperspace_last;
ps.flags.plot_hidden = d.shade == "Hidden Line";
ps.shade_plot_mode = match d.shade.as_str() {
"2D Wireframe" | "3D Wireframe" => ShadePlotMode::Wireframe,
"Hidden Line" => ShadePlotMode::Hidden,
"As displayed" => ShadePlotMode::AsDisplayed,
_ => ShadePlotMode::Rendered,
};
ps.shade_plot_resolution = match d.quality.as_str() {
"Low" => ShadePlotResolutionLevel::Draft,
"High" => ShadePlotResolutionLevel::Presentation,
_ => ShadePlotResolutionLevel::Normal,
};
ps.shade_plot_dpi = 300;
ps
}
/// Write the dialog's page setup into the active layout. No-op on the
/// Model tab (which has no paper layout). Marks the tab dirty and
/// re-tessellates the sheet. Called by the Plot dialog's Set current action.
pub(super) fn apply_plot_page_settings(&mut self) {
let i = self.active_tab;
let layout_name = self.tabs[i].scene.current_layout.clone();
if layout_name == "Model" {
return;
}
let ps = self.plot_settings_from_dialog(self.dialog_base_settings());
let (w, h) = plot_dialog_sheet_mm(&self.plot_dialog);
let plot_area = self.plot_dialog.area.clone();
let center = ps.flags.plot_centered;
let rotation = ps.rotation.to_degrees() as i32;
self.tabs[i]
.scene
.set_layout_plot_settings(&layout_name, &ps);
// Layout limits and extents are the sheet in paper-space units, which
// the settings just written define (an inch layout counts in inches).
let units_per_mm = self.tabs[i].scene.paper_space_unit_factor().max(1e-9);
let (lw, lh) = (w * units_per_mm, h * units_per_mm);
for obj in self.tabs[i].scene.document.objects.values_mut() {
if let acadrust::objects::ObjectType::Layout(layout) = obj {
if layout.name == layout_name {
layout.min_limits = (0.0, 0.0);
layout.max_limits = (lw, lh);
layout.min_extents = (0.0, 0.0, 0.0);
layout.max_extents = (lw, lh, 0.0);
break;
}
}
}
self.tabs[i].dirty = true;
self.tabs[i].scene.bump_geometry_no_blocks();
self.command_line.push_info(
crate::tf!(
"Page setup: {w:.1}×{h:.1} mm area={plot_area} \
center={center} rot={rotation}°"
)
.as_ref(),
);
}
pub(in crate::app) fn on_plot_export_path_some(
@@ -4552,94 +4674,9 @@ impl OpenCADStudio {
/// Build a `PlotSettings` from the current dialog fields (for saving a named
/// page setup).
/// The dialog's values as a named page setup.
fn dialog_to_plotsettings(&self) -> acadrust::objects::PlotSettings {
use acadrust::objects::{
PlotPaperUnits, PlotRotation, PlotSettings, PlotType, ScaledType, ShadePlotMode,
ShadePlotResolutionLevel,
};
let d = &self.plot_dialog;
let (w, h) = plot_dialog_sheet_mm(d);
let mut ps = match self.plot_setup_template.clone() {
Some(settings) => settings,
None => {
let mut settings = PlotSettings::new("");
settings.paper_units = PlotPaperUnits::Millimeters;
settings
}
};
let paper_size = paper_name_for_settings(d, &ps);
let printer_name = device_name_for_settings(d, &ps);
let margins = margins_for_settings(d, &ps);
ps.paper_size = paper_size;
ps.printer_name = printer_name;
ps.margins = margins;
ps.paper_width = w;
ps.paper_height = h;
ps.plot_type = match d.area.as_str() {
"Window" => PlotType::Window,
"Layout" => PlotType::Layout,
"Display" => PlotType::LastScreenDisplay,
"Limits" => PlotType::Limits,
area if area.starts_with("View: ") => PlotType::View,
_ => PlotType::Extents,
};
ps.plot_view_name = d.area.strip_prefix("View: ").unwrap_or("").to_string();
if d.area == "Window" {
if let Some((x0, y0, x1, y1)) = self.plot_window {
ps.set_plot_window(x0, y0, x1, y1);
}
}
ps.flags.plot_centered = d.center && d.area != "Layout";
ps.origin_x = if d.area == "Layout" {
0.0
} else {
d.offset_x.parse::<f64>().unwrap_or(0.0)
};
ps.origin_y = if d.area == "Layout" {
0.0
} else {
d.offset_y.parse::<f64>().unwrap_or(0.0)
};
let rot: i16 = if d.upside_down { 180 } else { 0 };
ps.rotation = match rot {
90 => PlotRotation::Degrees90,
180 => PlotRotation::Degrees180,
270 => PlotRotation::Degrees270,
_ => PlotRotation::None,
};
if d.area == "Layout" {
ps.set_standard_scale(ScaledType::OneToOne);
ps.standard_scale_factor = 1.0;
} else if d.fit_to_paper {
ps.set_scale_to_fit();
} else {
let factor = plot_dialog_scale_factor(d);
ps.scale_type = ScaledType::CustomScale;
ps.scale_numerator = factor;
ps.scale_denominator = 1.0;
ps.standard_scale_factor = factor;
ps.flags.use_standard_scale = false;
}
ps.current_style_sheet = d.style_name.clone();
ps.flags.scale_lineweights = d.scale_lw;
ps.flags.print_lineweights = d.lineweights;
ps.flags.plot_plot_styles = d.apply_plot_styles && !d.style_name.is_empty();
ps.flags.show_plot_styles = d.show_plot_styles && !d.style_name.is_empty();
ps.flags.draw_viewports_first = d.paperspace_last;
ps.flags.plot_hidden = d.shade == "Hidden Line";
ps.shade_plot_mode = match d.shade.as_str() {
"2D Wireframe" | "3D Wireframe" => ShadePlotMode::Wireframe,
"Hidden Line" => ShadePlotMode::Hidden,
"As displayed" => ShadePlotMode::AsDisplayed,
_ => ShadePlotMode::Rendered,
};
ps.shade_plot_resolution = match d.quality.as_str() {
"Low" => ShadePlotResolutionLevel::Draft,
"High" => ShadePlotResolutionLevel::Presentation,
_ => ShadePlotResolutionLevel::Normal,
};
ps.shade_plot_dpi = 300;
ps
self.plot_settings_from_dialog(self.dialog_base_settings())
}
/// Load a `PlotSettings` into the dialog editor fields.
@@ -4700,6 +4737,7 @@ impl OpenCADStudio {
"Portrait"
}
.to_string();
let family_unit_mm = self.tabs[self.active_tab].scene.scale_family_unit_mm();
let d = &mut self.plot_dialog;
d.paper = paper.canonical.to_string();
d.paper_width_mm = ps.paper_width.max(1.0);
@@ -4735,7 +4773,7 @@ impl OpenCADStudio {
}
d.upside_down = matches!(deg, 180 | 270);
d.fit_to_paper = d.area != "Layout" && ps.is_scale_to_fit();
let target_factor = if d.area == "Layout" {
let stored_factor = if d.area == "Layout" {
1.0
} else if ps.flags.use_standard_scale {
if ps.standard_scale_factor.is_finite() && ps.standard_scale_factor > 0.0 {
@@ -4748,11 +4786,28 @@ impl OpenCADStudio {
} else {
1.0
};
// The file's factor is per the layout's paper unit; the picker's are
// per the drawing's scale-family unit (see `plot_settings_from_dialog`).
let page_unit_mm = match ps.paper_units {
acadrust::objects::PlotPaperUnits::Inches => 25.4,
_ => 1.0,
};
let target_factor = if stored_factor.is_finite() && stored_factor > 0.0 {
stored_factor * page_unit_mm / family_unit_mm
} else {
1.0
};
if !d.fit_to_paper || !d.scales.iter().any(|(name, _)| name == &d.scale) {
d.scale = scale_name_for_factor(&d.scales, target_factor)
.or_else(|| scale_name_for_factor(&d.scales, 1.0))
.or_else(|| d.scales.first().map(|(name, _)| name.clone()))
.unwrap_or_else(|| "1:1".into());
d.scale = scale_name_for_factor(&d.scales, target_factor).unwrap_or_else(|| {
// A scale the drawing has no name for stays what it is rather
// than snapping to 1:1; the ratio entry disappears again with
// the next scale-list refresh once it is no longer selected.
let name = ratio_scale_name(target_factor);
d.scales.push((name.clone(), target_factor));
d.scales
.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
name
});
}
d.scale_lw = ps.flags.scale_lineweights && !d.fit_to_paper;
d.lineweights = ps.flags.print_lineweights;
@@ -4806,12 +4861,7 @@ impl OpenCADStudio {
fn apply_dialog_to_layout(&mut self) {
self.sync_dialog_plot_runtime();
let d = self.plot_dialog.clone();
let (sheet_w, sheet_h) = plot_dialog_sheet_mm(&d);
let rotation: i16 = if d.upside_down { 180 } else { 0 };
let off_x = d.offset_x.parse::<f64>().unwrap_or(0.0);
let off_y = d.offset_y.parse::<f64>().unwrap_or(0.0);
self.apply_plot_page_settings(sheet_w, sheet_h, &d.area, d.center, off_x, off_y, rotation);
self.apply_plot_page_settings();
}
/// Open a preview PDF, export a PDF, or send the job to the chosen printer.
@@ -5239,16 +5289,246 @@ mod plot_paper_tests {
ps.paper_size = "A4".into();
ps.paper_width = 297.0;
ps.paper_height = 210.0;
// Such drivers spell the orientation in the dimensions, not the rotation.
ps.rotation = acadrust::objects::PlotRotation::None;
assert!(app.tabs[i].scene.set_layout_plot_settings("Layout1", &ps));
app
}
fn layout_paper(app: &OpenCADStudio) -> (String, f64, f64) {
let ps = app.tabs[app.active_tab]
let ps = layout_settings(app);
(ps.paper_size, ps.paper_width, ps.paper_height)
}
fn layout_settings(app: &OpenCADStudio) -> acadrust::objects::PlotSettings {
app.tabs[app.active_tab]
.scene
.plot_settings_for("Layout1")
.unwrap();
(ps.paper_size, ps.paper_width, ps.paper_height)
.unwrap()
}
fn layout_rotation(app: &OpenCADStudio) -> acadrust::objects::PlotRotation {
layout_settings(app).rotation
}
/// Store the drawing's plot settings for `Layout1` after `edit` touched them.
fn edit_layout_settings(
app: &mut OpenCADStudio,
edit: impl FnOnce(&mut acadrust::objects::PlotSettings),
) {
let i = app.active_tab;
let mut ps = app.tabs[i].scene.plot_settings_for("Layout1").unwrap();
edit(&mut ps);
assert!(app.tabs[i].scene.set_layout_plot_settings("Layout1", &ps));
}
#[test]
fn a_new_drawing_starts_with_a_compatible_default_layout() {
use acadrust::objects::{PlotRotation, PlotType, ScaledType};
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
let _ = app.update(Message::LayoutSwitch("Layout1".into()));
let ps = layout_settings(&app);
assert_eq!(ps.paper_size, "ISO_A4_(210.00_x_297.00_MM)");
assert_eq!((ps.paper_width, ps.paper_height), (210.0, 297.0));
assert_eq!(ps.rotation, PlotRotation::Degrees90);
assert_eq!(ps.printer_name, "none_device");
assert_eq!(ps.plot_type, PlotType::Layout);
assert_eq!(ps.scale_type, ScaledType::OneToOne);
assert!(ps.flags.use_standard_scale && ps.flags.print_lineweights);
// The sheet on screen is the landscape A4 the rotation describes.
let ((x0, y0), (x1, y1)) = app.tabs[app.active_tab].scene.paper_limits().unwrap();
assert_eq!((x1 - x0, y1 - y0), (297.0, 210.0));
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
let _ = app.on_plot_dialog_open();
assert_eq!(app.plot_dialog.orientation, "Landscape");
assert!(!app.plot_dialog.upside_down);
}
#[test]
fn orientation_is_stored_as_the_rotation_of_the_portrait_medium() {
use acadrust::objects::PlotRotation as R;
use crate::ui::window::plot::PlotFlag;
let cases = [
("Portrait", false, R::None),
("Landscape", false, R::Degrees90),
("Portrait", true, R::Degrees180),
("Landscape", true, R::Degrees270),
];
for (orientation, upside_down, rotation) in cases {
let mut app = app_with_printer_named_sheet();
let _ = app.on_plot_dialog_open();
let _ = app.on_plot_dlg(PlotDlgMsg::Orientation(orientation.into()));
if upside_down {
let _ = app.on_plot_dlg(PlotDlgMsg::Flag(PlotFlag::UpsideDown));
}
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
let ps = layout_settings(&app);
assert_eq!((ps.paper_width, ps.paper_height), (210.0, 297.0), "{orientation}");
assert_eq!(ps.rotation, rotation, "{orientation} upside-down={upside_down}");
// The next dialog reads the same orientation back out of the rotation.
let _ = app.on_plot_dialog_open();
assert_eq!(app.plot_dialog.orientation, orientation);
assert_eq!(app.plot_dialog.upside_down, upside_down);
assert_eq!(
super::plot_dialog_sheet_mm(&app.plot_dialog),
if orientation == "Portrait" { (210.0, 297.0) } else { (297.0, 210.0) }
);
}
}
#[test]
fn standard_scales_are_written_by_code_and_others_as_ratios() {
use acadrust::objects::ScaledType;
let mut app = app_with_printer_named_sheet();
let _ = app.on_plot_dialog_open();
let _ = app.on_plot_dlg(PlotDlgMsg::Area("Extents".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::Flag(crate::ui::window::plot::PlotFlag::FitToPaper));
assert!(!app.plot_dialog.fit_to_paper);
let _ = app.on_plot_dlg(PlotDlgMsg::Scale("1:100".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
let ps = layout_settings(&app);
assert_eq!(ps.scale_type, ScaledType::OneToHundred);
assert!(ps.flags.use_standard_scale);
assert_eq!((ps.scale_numerator, ps.scale_denominator), (1.0, 100.0));
assert!((ps.standard_scale_factor - 0.01).abs() < 1e-12);
let _ = app.on_plot_dialog_open();
assert_eq!(app.plot_dialog.scale, "1:100");
// A ratio the format has no code for is a custom scale, spelled as the ratio.
let _ = app.on_plot_dlg(PlotDlgMsg::Scale("1:3".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
let ps = layout_settings(&app);
assert_eq!(ps.scale_type, ScaledType::CustomScale);
assert!(!ps.flags.use_standard_scale);
assert_eq!((ps.scale_numerator, ps.scale_denominator), (1.0, 3.0));
// …and survives a reopen even though the drawing's scale list lacks it.
let _ = app.on_plot_dialog_open();
assert_eq!(app.plot_dialog.scale, "1:3");
assert!((super::plot_dialog_scale_factor(&app.plot_dialog) - 1.0 / 3.0).abs() < 1e-12);
// Fit to paper is its own scale type; a Layout plot is always 1:1.
let _ = app.on_plot_dlg(PlotDlgMsg::Flag(crate::ui::window::plot::PlotFlag::FitToPaper));
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
assert_eq!(layout_settings(&app).scale_type, ScaledType::ScaleToFit);
let _ = app.on_plot_dlg(PlotDlgMsg::Area("Layout".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
let ps = layout_settings(&app);
assert_eq!(ps.scale_type, ScaledType::OneToOne);
assert_eq!((ps.scale_numerator, ps.scale_denominator), (1.0, 1.0));
}
#[test]
fn a_metre_drawing_stores_the_millimetres_it_really_plots_per_unit() {
use acadrust::objects::ScaledType;
let mut app = app_with_printer_named_sheet();
// Insertion units: metres. "1:100" then plots 10 mm per drawing unit.
app.tabs[app.active_tab].scene.document.header.insertion_units = 6;
let _ = app.on_plot_dialog_open();
let _ = app.on_plot_dlg(PlotDlgMsg::Area("Extents".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::Flag(crate::ui::window::plot::PlotFlag::FitToPaper));
let _ = app.on_plot_dlg(PlotDlgMsg::Scale("1:100".into()));
assert!((super::plot_dialog_scale_factor(&app.plot_dialog) - 10.0).abs() < 1e-9);
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
let ps = layout_settings(&app);
assert_eq!(ps.scale_type, ScaledType::CustomScale, "not the format's 1:100");
assert_eq!((ps.scale_numerator, ps.scale_denominator), (10.0, 1.0));
let _ = app.on_plot_dialog_open();
assert_eq!(app.plot_dialog.scale, "1:100");
}
#[test]
fn an_inch_layout_keeps_its_unit_and_converts_the_scale() {
use acadrust::objects::{PlotPaperUnits, ScaledType};
let mut app = app_with_printer_named_sheet();
edit_layout_settings(&mut app, |ps| ps.paper_units = PlotPaperUnits::Inches);
let _ = app.on_plot_dialog_open();
let _ = app.on_plot_dlg(PlotDlgMsg::Area("Extents".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::Flag(crate::ui::window::plot::PlotFlag::FitToPaper));
let _ = app.on_plot_dlg(PlotDlgMsg::Scale("1:100".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
let ps = layout_settings(&app);
assert_eq!(
ps.paper_units,
PlotPaperUnits::Inches,
"the layout's unit is not ours to change"
);
// 1 mm = 100 units is 1 inch = 2540 units on an inch page setup.
assert_eq!(ps.scale_type, ScaledType::CustomScale);
assert_eq!((ps.scale_numerator, ps.scale_denominator), (1.0, 2540.0));
let _ = app.on_plot_dialog_open();
assert_eq!(app.plot_dialog.scale, "1:100");
}
#[test]
fn a_named_setup_and_the_layout_settings_are_written_alike() {
let mut app = app_with_printer_named_sheet();
let _ = app.on_plot_dialog_open();
let _ = app.on_plot_dlg(PlotDlgMsg::Printer(crate::ui::window::plot::OUT_PDF.into()));
let _ = app.on_plot_dlg(PlotDlgMsg::Paper("ISO_A3_(297.00_x_420.00_MM)".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::Area("Extents".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::Flag(crate::ui::window::plot::PlotFlag::FitToPaper));
let _ = app.on_plot_dlg(PlotDlgMsg::Scale("1:50".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::Flag(crate::ui::window::plot::PlotFlag::UpsideDown));
let named = app.dialog_to_plotsettings();
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
let layout = layout_settings(&app);
assert_eq!(layout.paper_size, named.paper_size);
assert_eq!(layout.printer_name, named.printer_name);
assert_eq!(layout.margins, named.margins);
assert_eq!(
(layout.paper_width, layout.paper_height),
(named.paper_width, named.paper_height)
);
assert_eq!(layout.rotation, named.rotation);
assert_eq!(layout.plot_type, named.plot_type);
assert_eq!((layout.origin_x, layout.origin_y), (named.origin_x, named.origin_y));
assert_eq!(layout.scale_type, named.scale_type);
assert_eq!(
(layout.scale_numerator, layout.scale_denominator),
(named.scale_numerator, named.scale_denominator)
);
assert_eq!(layout.standard_scale_factor, named.standard_scale_factor);
assert_eq!(layout.flags, named.flags);
assert_eq!(layout.shade_plot_mode, named.shade_plot_mode);
assert_eq!(layout.shade_plot_resolution, named.shade_plot_resolution);
assert_eq!(named.rotation, acadrust::objects::PlotRotation::Degrees270);
assert_eq!(named.scale_type, acadrust::objects::ScaledType::OneToFifty);
}
#[test]
fn flags_the_dialog_does_not_edit_survive_a_round_trip() {
let mut app = app_with_printer_named_sheet();
edit_layout_settings(&mut app, |ps| {
ps.flags.plot_viewport_borders = true;
ps.flags.update_paper = true;
ps.flags.zoom_to_paper_on_update = true;
ps.flags.unknown_bits = 0x8000;
ps.paper_image_origin_x = 3.5;
});
let _ = app.on_plot_dialog_open();
let _ = app.on_plot_dlg(PlotDlgMsg::Paper("ISO_A3_(297.00_x_420.00_MM)".into()));
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
let ps = layout_settings(&app);
assert!(ps.flags.plot_viewport_borders);
assert!(ps.flags.update_paper && ps.flags.zoom_to_paper_on_update);
assert_eq!(ps.flags.unknown_bits, 0x8000);
assert_eq!(ps.paper_image_origin_x, 3.5);
}
#[test]
fn scale_names_parse_to_their_ratios() {
use crate::scene::Scene;
assert_eq!(Scene::parse_scale_name_ratio("1:250"), Some((1.0, 250.0)));
assert_eq!(Scene::parse_scale_name_ratio("2:1"), Some((2.0, 1.0)));
assert_eq!(Scene::parse_scale_name_ratio("1/8\" = 1'-0\""), Some((0.125, 12.0)));
assert_eq!(Scene::parse_scale_name_ratio("3/32\" = 1'-0\""), Some((0.09375, 12.0)));
assert_eq!(Scene::parse_scale_name_ratio("1'-0\" = 1'-0\""), Some((12.0, 12.0)));
assert_eq!(Scene::parse_scale_name_ratio("6\" = 1'-0\""), Some((6.0, 12.0)));
assert_eq!(Scene::parse_scale_name_ratio("Fit"), None);
assert_eq!(Scene::parse_scale_name_ratio("1:0"), None);
assert_eq!(super::ratio_scale_name(0.004), "1:250");
assert_eq!(super::ratio_scale_name(25.4), "25.4:1");
assert_eq!(super::ratio_scale_name(1.0 / 3.0), "1:3");
}
#[test]
@@ -5267,7 +5547,9 @@ mod plot_paper_tests {
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
let (name, w, h) = layout_paper(&app);
assert_eq!(name, "A4", "the driver's own spelling must survive the dialog");
assert_eq!((w, h), (297.0, 210.0));
// The landscape sheet is stored as the portrait medium turned 90°.
assert_eq!((w, h), (210.0, 297.0));
assert_eq!(layout_rotation(&app), acadrust::objects::PlotRotation::Degrees90);
}
#[test]
@@ -5279,7 +5561,8 @@ mod plot_paper_tests {
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
let (name, w, h) = layout_paper(&app);
assert_eq!(name, "ISO_A3_(297.00_x_420.00_MM)");
assert_eq!((w, h), (420.0, 297.0));
assert_eq!((w, h), (297.0, 420.0));
assert_eq!(layout_rotation(&app), acadrust::objects::PlotRotation::Degrees90);
// The next dialog remembers the sheet the user chose.
assert_eq!(app.plot_paper.canonical, "ISO_A3_(297.00_x_420.00_MM)");
}
@@ -5346,7 +5629,7 @@ mod plot_paper_tests {
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
let ps = app.tabs[i].scene.plot_settings_for("Layout1").unwrap();
assert_eq!(ps.margins, acadrust::objects::PaperMargin::new(5.0, 17.0, 6.0, 18.0));
assert_eq!((ps.paper_width, ps.paper_height), (420.0, 297.0));
assert_eq!((ps.paper_width, ps.paper_height), (297.0, 420.0));
}
#[test]
@@ -5394,7 +5677,8 @@ mod plot_paper_tests {
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
let ps = app.tabs[app.active_tab].scene.plot_settings_for("Layout1").unwrap();
assert_eq!(ps.paper_size, "Roll_24_(609.60_x_1500.00_MM)");
assert_eq!((ps.paper_width, ps.paper_height), (1500.0, 609.6));
assert_eq!((ps.paper_width, ps.paper_height), (609.6, 1500.0));
assert_eq!(ps.rotation, acadrust::objects::PlotRotation::Degrees90);
assert_eq!(ps.margins, acadrust::objects::PaperMargin::new(3.0, 17.0, 6.0, 4.0));
// Re-adding the same size replaces the definition instead of duplicating it.
let _ = app.on_plot_dlg(custom(C::Open));
@@ -5507,7 +5791,11 @@ cupsPrintQuality/Print Quality: *Normal High\n";
assert_eq!(app.plot_dialog.paper, "Roll_24_(609.60_x_1500.00_MM)");
assert_eq!(super::plot_dialog_sheet_mm(&app.plot_dialog), (1500.0, 609.6));
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
assert_eq!(layout_paper(&app).0, "Roll_24_(609.60_x_1500.00_MM)");
let (name, w, h) = layout_paper(&app);
assert_eq!(name, "Roll_24_(609.60_x_1500.00_MM)");
// Written back in canonical form: the portrait medium, turned.
assert_eq!((w, h), (609.6, 1500.0));
assert_eq!(layout_rotation(&app), acadrust::objects::PlotRotation::Degrees90);
}
}
+1 -2
View File
@@ -1508,8 +1508,7 @@ impl OpenCADStudio {
Message::ClearScene => {
let i = self.active_tab;
self.push_undo_snapshot(i, "CLEAR");
self.tabs[i].scene.clear();
crate::io::linetypes::populate_document(&mut self.tabs[i].scene.document);
self.tabs[i].scene.reset_to_new_drawing();
self.tabs[i].properties = PropertiesPanel::empty();
let doc_layers = self.tabs[i].scene.document.layers.clone();
let vp_info = self.tabs[i].scene.viewport_list();
+1 -18
View File
@@ -6031,24 +6031,7 @@ properties={:.1}ms picked={}",
if let acadrust::objects::ObjectType::Layout(l) = obj {
if l.name == new_name {
l.flags = layout_flags;
l.min_limits = (0.0, 0.0);
l.max_limits = (297.0, 210.0);
l.min_extents = (0.0, 0.0, 0.0);
l.max_extents = (297.0, 210.0, 0.0);
l.paper_width = 297.0;
l.paper_height = 210.0;
l.plot_paper_units = 1;
l.plot_scale_numerator = 1.0;
l.plot_scale_denominator = 1.0;
l.plot_scale_type = 16;
l.plot_scale_factor = 1.0;
l.plot_type = 5;
l.plot_flags.use_standard_scale = true;
l.plot_flags.print_lineweights = true;
l.plot_flags.plot_plot_styles = !plot_style.is_empty();
l.plot_flags.show_plot_styles = !plot_style.is_empty();
l.plot_style_sheet = plot_style;
l.paper_size = "ISO_A4_(297.00_x_210.00_MM)".into();
crate::scene::apply_default_page_setup(l, &plot_style);
break;
}
}
+21
View File
@@ -2709,4 +2709,25 @@ impl Scene {
self.named_parameters = crate::scene::named_parameters::ParameterTable::new();
self.bump_geometry();
}
/// Reset to the drawing File → New produces. Every path that starts a
/// fresh drawing (the New tab, CLEAR, the automation `new` op) goes
/// through here so they all agree on what a blank drawing contains.
pub fn reset_to_new_drawing(&mut self) {
self.clear();
self.populate_new_drawing_defaults();
}
/// What a blank document gets on top of the codec's defaults: the
/// standard linetypes and a compatible page setup on every paper layout.
pub fn populate_new_drawing_defaults(&mut self) {
crate::io::linetypes::populate_document(&mut self.document);
for obj in self.document.objects.values_mut() {
if let acadrust::objects::ObjectType::Layout(l) = obj {
if l.name != "Model" {
crate::scene::apply_default_page_setup(l, "");
}
}
}
}
}
+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
+33 -1
View File
@@ -5,9 +5,41 @@
//! dictionary + objects consistent so the result round-trips through DWG/DXF.
use super::Scene;
use acadrust::objects::{Dictionary, ObjectType, PlotSettings};
use acadrust::objects::{Dictionary, Layout, ObjectType, PlotSettings};
use acadrust::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 acadrust::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();
}
impl Scene {
/// Handle of the `ACAD_PLOTSETTINGS` dictionary, located robustly.
///