diff --git a/src/app/automation.rs b/src/app/automation.rs index 9ca7e8c73..ff0bc2776 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -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 diff --git a/src/app/document.rs b/src/app/document.rs index 6b90aaf54..5759e3d75 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -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, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 5d560690d..c5698be37 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -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 { + 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::().unwrap_or(0.0) + }; + ps.origin_y = if layout_area { + 0.0 + } else { + d.offset_y.parse::().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::().unwrap_or(0.0) - }; - ps.origin_y = if d.area == "Layout" { - 0.0 - } else { - d.offset_y.parse::().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::().unwrap_or(0.0); - let off_y = d.offset_y.parse::().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); } } diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index ee962d958..5d650515d 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -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(); diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index d508a0537..4c7831ab8 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -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; } } diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 3b31f90ea..2c2d105df 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -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, ""); + } + } + } + } } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 57e7e9264..67acb8380 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -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 { + let text = text.trim(); + let (feet, inches) = match text.split_once('\'') { + Some((feet, rest)) => ( + feet.trim().parse::().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::().ok()? / den + } else { + inches.parse::().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 diff --git a/src/scene/page_setup.rs b/src/scene/page_setup.rs index a09db2e6c..74e302746 100644 --- a/src/scene/page_setup.rs +++ b/src/scene/page_setup.rs @@ -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. ///