forked from 1b-agent/opencadstudio-upstream
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:
1 parent
ef0c6bf403
commit
276e7c7bf1
8 files changed
+641
-263
No files matched your search
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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.
|
||||
///
|
||||
|
||||
Reference in new issue
Block a user