forked from 1b-agent/opencadstudio-upstream
fix(plot): follow the fixture's page-setup conventions
A drawing saved by a commercial CAD application (tests/fixtures/plot,
provided by Panos: one 10 m line, three layouts) corrects several
assumptions of the previous commit:
- rotation is relative to the medium's own orientation: an unchanged sheet
keeps the driver's exact medium (its PDF driver lists landscape media of
its own, written with rotation 0; a system printer's portrait A3 plotted
landscape carries 90); only a newly picked sheet is written as the
catalogue's portrait medium
- the stored scale factor (DXF 147) is the ratio × 25.4 on millimetre page
setups and the plain ratio on inch ones; the "use standard scale" bit
stays clear for standard and custom scales and is set for fit-to-paper;
a standard code is kept across a paper-unit conversion ("1:1" on an
inch page setup over a millimetre paper space is 1 in = 25.4 units)
- layout plots take the same scale path as every other area, so a foreign
layout's paper-space unit survives
- "plot with plot styles" / "display plot styles" round-trip as the
dialog's checkboxes, independent of a style table being named
- layout limits place the sheet at minus its displayed margins, the way the
source application does (the printable corner is the paper-space origin);
the margin ↔ rotation mapping shared by the printable-area guide now
turns the medium's top margin into the layout's left one at 90°
The fixture round-trips through the dialog with every stored field
unchanged; picking a new sheet on it writes the canonical form.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
276e7c7bf1
commit
8e09bd8463
6 files changed
+16235
-112
No files matched your search
+302
-104
@@ -152,20 +152,48 @@ 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.
|
||||
/// The stored rotation for the dialog's sheet on a medium that is itself
|
||||
/// `natural`-oriented: a quarter turn when the requested orientation differs
|
||||
/// from the medium's own, and "upside-down" adds a half turn on top. This is
|
||||
/// how the commercial application writes it — a system printer's portrait
|
||||
/// `A3` plotted landscape carries 90°, while its PDF driver's landscape
|
||||
/// `ISO_A4_(297.00_x_210.00_MM)` medium plotted landscape carries 0°.
|
||||
fn plot_dialog_rotation(
|
||||
d: &crate::ui::window::plot::PlotDialogState,
|
||||
natural: crate::io::paper_catalog::Orientation,
|
||||
) -> 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,
|
||||
match (plot_dialog_orientation(d) != natural, d.upside_down) {
|
||||
(false, false) => PlotRotation::None,
|
||||
(true, false) => PlotRotation::Degrees90,
|
||||
(false, true) => PlotRotation::Degrees180,
|
||||
(true, true) => PlotRotation::Degrees270,
|
||||
}
|
||||
}
|
||||
|
||||
/// Millimetres in the paper unit a page setup counts in.
|
||||
fn plot_paper_unit_mm(units: acadrust::objects::PlotPaperUnits) -> f64 {
|
||||
match units {
|
||||
acadrust::objects::PlotPaperUnits::Inches => 25.4,
|
||||
_ => 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// The factor the commercial application stores next to the scale (DXF 147)
|
||||
/// is the `paper : drawing` ratio times 25.4 on millimetre page setups and
|
||||
/// the plain ratio on inch ones — observed in its files, whatever the
|
||||
/// published description says. Both directions of that convention live here.
|
||||
fn stored_scale_factor(ratio: f64, units: acadrust::objects::PlotPaperUnits) -> f64 {
|
||||
match units {
|
||||
acadrust::objects::PlotPaperUnits::Millimeters => ratio * 25.4,
|
||||
_ => ratio,
|
||||
}
|
||||
}
|
||||
|
||||
fn ratio_from_stored_scale_factor(factor: f64, units: acadrust::objects::PlotPaperUnits) -> f64 {
|
||||
match units {
|
||||
acadrust::objects::PlotPaperUnits::Millimeters => factor / 25.4,
|
||||
_ => factor,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3093,9 +3121,8 @@ impl OpenCADStudio {
|
||||
&self,
|
||||
mut ps: acadrust::objects::PlotSettings,
|
||||
) -> acadrust::objects::PlotSettings {
|
||||
use acadrust::objects::{
|
||||
PlotPaperUnits, PlotType, ScaledType, ShadePlotMode, ShadePlotResolutionLevel,
|
||||
};
|
||||
use crate::io::paper_catalog::Orientation;
|
||||
use acadrust::objects::{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
|
||||
@@ -3103,13 +3130,28 @@ impl OpenCADStudio {
|
||||
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);
|
||||
let same_medium = paper_unchanged(d, &ps) && ps.paper_width > 0.0 && ps.paper_height > 0.0;
|
||||
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);
|
||||
// An unchanged sheet keeps the medium exactly as the drawing stores
|
||||
// it — its driver's precise dimensions in the orientation the driver
|
||||
// defines it (a PDF driver lists landscape media of its own) — and
|
||||
// only the rotation says how the sheet is plotted on it. A new
|
||||
// selection is written as the catalogue's portrait medium.
|
||||
let natural = if same_medium {
|
||||
if ps.paper_width > ps.paper_height {
|
||||
Orientation::Landscape
|
||||
} else {
|
||||
Orientation::Portrait
|
||||
}
|
||||
} else {
|
||||
let (width, height) = plot_dialog_paper(d).portrait_mm();
|
||||
ps.paper_width = width.max(1.0);
|
||||
ps.paper_height = height.max(1.0);
|
||||
Orientation::Portrait
|
||||
};
|
||||
ps.rotation = plot_dialog_rotation(d, natural);
|
||||
let layout_area = d.area == "Layout";
|
||||
ps.plot_type = match d.area.as_str() {
|
||||
"Window" => PlotType::Window,
|
||||
@@ -3136,58 +3178,58 @@ impl OpenCADStudio {
|
||||
} 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 {
|
||||
if d.fit_to_paper && !layout_area {
|
||||
ps.set_scale_to_fit();
|
||||
// The commercial application marks fitted plots with the
|
||||
// "standard scale" bit and leaves the ratio it computed.
|
||||
ps.flags.use_standard_scale = true;
|
||||
} 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.
|
||||
// (inches for an imperial drawing, else millimetres) and already
|
||||
// includes the drawing-unit factor; the file stores it per the
|
||||
// layout's paper unit. A named ratio is kept as written when no
|
||||
// drawing-unit factor hides inside it — a metre drawing plotting
|
||||
// "1:100" really plots 10 mm per unit, which is what the file
|
||||
// must say — and its standard code is kept even across a
|
||||
// paper-unit conversion, the way the commercial application
|
||||
// writes "1:1" on an inch page setup over a millimetre paper
|
||||
// space as `1 in = 25.4 units`. A layout plot goes through the
|
||||
// same path: the dialog holds it at 1:1, which the conversion
|
||||
// turns into whatever keeps the paper space's unit intact.
|
||||
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 conversion = scene.scale_family_unit_mm() / plot_paper_unit_mm(ps.paper_units);
|
||||
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 standard =
|
||||
named.and_then(|(paper, drawing)| standard_scale_for_ratio(paper, drawing));
|
||||
let (numerator, denominator) = match named {
|
||||
Some((paper, drawing)) if (conversion - 1.0).abs() <= 1e-12 => (paper, drawing),
|
||||
_ => {
|
||||
let per_unit = (factor * conversion).max(1e-9);
|
||||
if per_unit >= 1.0 {
|
||||
(per_unit, 1.0, None)
|
||||
(per_unit, 1.0)
|
||||
} else {
|
||||
(1.0, 1.0 / per_unit, None)
|
||||
(1.0, 1.0 / per_unit)
|
||||
}
|
||||
}
|
||||
};
|
||||
match standard {
|
||||
Some(scale) => ps.set_standard_scale(scale),
|
||||
None => {
|
||||
ps.set_custom_scale(numerator, denominator);
|
||||
ps.flags.use_standard_scale = false;
|
||||
}
|
||||
}
|
||||
// The commercial application leaves the "standard scale" bit
|
||||
// clear even for a standard code and keeps the ratio in the
|
||||
// numerator/denominator pair; readers that trust the bit would
|
||||
// otherwise take the stored factor as the ratio itself.
|
||||
ps.scale_type = standard.unwrap_or(ScaledType::CustomScale);
|
||||
ps.flags.use_standard_scale = false;
|
||||
ps.scale_numerator = numerator;
|
||||
ps.scale_denominator = denominator;
|
||||
ps.standard_scale_factor = numerator / denominator;
|
||||
ps.standard_scale_factor =
|
||||
stored_scale_factor(numerator / denominator, ps.paper_units);
|
||||
}
|
||||
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.plot_plot_styles = d.apply_plot_styles;
|
||||
ps.flags.show_plot_styles = d.show_plot_styles;
|
||||
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() {
|
||||
@@ -3222,17 +3264,25 @@ impl OpenCADStudio {
|
||||
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).
|
||||
// Layout limits are the sheet in paper-space units, which the
|
||||
// settings just written define, placed the way the commercial
|
||||
// application places it: the printable area's corner is the paper
|
||||
// space origin, so the sheet starts at minus its displayed margins.
|
||||
let units_per_mm = self.tabs[i].scene.paper_space_unit_factor().max(1e-9);
|
||||
let m = ps.margins;
|
||||
let (left, bottom, ..) = crate::scene::rotated_margins(
|
||||
(m.left, m.bottom, m.right, m.top),
|
||||
ps.rotation.to_code(),
|
||||
);
|
||||
let (x0, y0) = (-left * units_per_mm, -bottom * units_per_mm);
|
||||
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);
|
||||
layout.min_limits = (x0, y0);
|
||||
layout.max_limits = (x0 + lw, y0 + lh);
|
||||
layout.min_extents = (x0, y0, 0.0);
|
||||
layout.max_extents = (x0 + lw, y0 + lh, 0.0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -4773,31 +4823,25 @@ impl OpenCADStudio {
|
||||
}
|
||||
d.upside_down = matches!(deg, 180 | 270);
|
||||
d.fit_to_paper = d.area != "Layout" && ps.is_scale_to_fit();
|
||||
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 {
|
||||
ps.standard_scale_factor
|
||||
} else {
|
||||
ps.scale_type.scale_factor()
|
||||
}
|
||||
} else if ps.scale_denominator.abs() > 1e-9 {
|
||||
ps.scale_numerator / ps.scale_denominator
|
||||
let ratio = ps.scale_numerator / ps.scale_denominator;
|
||||
let stored_factor = if ratio.is_finite() && ratio > 0.0 {
|
||||
ratio
|
||||
} else if ps.flags.use_standard_scale
|
||||
&& ps.standard_scale_factor.is_finite()
|
||||
&& ps.standard_scale_factor > 0.0
|
||||
{
|
||||
ratio_from_stored_scale_factor(ps.standard_scale_factor, ps.paper_units)
|
||||
} else {
|
||||
1.0
|
||||
ps.scale_type.scale_factor()
|
||||
};
|
||||
// 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
|
||||
stored_factor * plot_paper_unit_mm(ps.paper_units) / family_unit_mm
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
if !d.fit_to_paper || !d.scales.iter().any(|(name, _)| name == &d.scale) {
|
||||
if !d.fit_to_paper {
|
||||
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
|
||||
@@ -4808,6 +4852,12 @@ impl OpenCADStudio {
|
||||
.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
name
|
||||
});
|
||||
} else if !d.scales.iter().any(|(name, _)| name == &d.scale) {
|
||||
// A fitted plot's stored ratio is what the fit computed, not a
|
||||
// choice; the picker just needs a valid entry behind it.
|
||||
d.scale = 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_lw = ps.flags.scale_lineweights && !d.fit_to_paper;
|
||||
d.lineweights = ps.flags.print_lineweights;
|
||||
@@ -5347,34 +5397,50 @@ mod plot_paper_tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn orientation_is_stored_as_the_rotation_of_the_portrait_medium() {
|
||||
fn orientation_is_stored_as_a_rotation_of_the_medium() {
|
||||
use acadrust::objects::PlotRotation as R;
|
||||
use crate::ui::window::plot::PlotFlag;
|
||||
let cases = [
|
||||
// A newly picked sheet is the catalogue's portrait medium…
|
||||
let portrait_medium = [
|
||||
("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));
|
||||
// …while the drawing's own landscape `A4` medium is kept, so the
|
||||
// same choices rotate relative to it.
|
||||
let landscape_medium = [
|
||||
("Landscape", false, R::None),
|
||||
("Portrait", false, R::Degrees90),
|
||||
("Landscape", true, R::Degrees180),
|
||||
("Portrait", true, R::Degrees270),
|
||||
];
|
||||
for (pick_a3, cases) in [(true, portrait_medium), (false, landscape_medium)] {
|
||||
for (orientation, upside_down, rotation) in cases {
|
||||
let mut app = app_with_printer_named_sheet();
|
||||
let _ = app.on_plot_dialog_open();
|
||||
if pick_a3 {
|
||||
let a3 = "ISO_A3_(297.00_x_420.00_MM)";
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::Paper(a3.into()));
|
||||
}
|
||||
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);
|
||||
let medium = if pick_a3 { (297.0, 420.0) } else { (297.0, 210.0) };
|
||||
assert_eq!((ps.paper_width, ps.paper_height), medium, "{orientation}");
|
||||
assert_eq!(ps.rotation, rotation, "{orientation} upside-down={upside_down}");
|
||||
// The next dialog reads the same choices 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);
|
||||
let sheet = super::plot_dialog_sheet_mm(&app.plot_dialog);
|
||||
let long_side = medium.0.max(medium.1);
|
||||
assert_eq!(sheet.0 > sheet.1, orientation == "Landscape", "{sheet:?}");
|
||||
assert_eq!(sheet.0.max(sheet.1), long_side);
|
||||
}
|
||||
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) }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5390,9 +5456,13 @@ mod plot_paper_tests {
|
||||
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!(
|
||||
!ps.flags.use_standard_scale,
|
||||
"the bit stays clear, as the source application writes it"
|
||||
);
|
||||
assert_eq!((ps.scale_numerator, ps.scale_denominator), (1.0, 100.0));
|
||||
assert!((ps.standard_scale_factor - 0.01).abs() < 1e-12);
|
||||
// The stored factor follows the millimetre convention: ratio × 25.4.
|
||||
assert!((ps.standard_scale_factor - 0.254).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.
|
||||
@@ -5406,15 +5476,20 @@ mod plot_paper_tests {
|
||||
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.
|
||||
// Fit to paper is its own scale type and carries the bit; a Layout
|
||||
// plot goes back to the 1:1 the dialog holds it at.
|
||||
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 ps = layout_settings(&app);
|
||||
assert_eq!(ps.scale_type, ScaledType::ScaleToFit);
|
||||
assert!(ps.flags.use_standard_scale);
|
||||
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!(!ps.flags.use_standard_scale);
|
||||
assert_eq!((ps.scale_numerator, ps.scale_denominator), (1.0, 1.0));
|
||||
assert!((ps.standard_scale_factor - 25.4).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -5432,6 +5507,7 @@ mod plot_paper_tests {
|
||||
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));
|
||||
assert!((ps.standard_scale_factor - 254.0).abs() < 1e-9);
|
||||
let _ = app.on_plot_dialog_open();
|
||||
assert_eq!(app.plot_dialog.scale, "1:100");
|
||||
}
|
||||
@@ -5452,11 +5528,21 @@ mod plot_paper_tests {
|
||||
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);
|
||||
// 1 mm = 100 units is 1 inch = 2540 units on an inch page setup; the
|
||||
// standard code still names the choice, as the source application does.
|
||||
assert_eq!(ps.scale_type, ScaledType::OneToHundred);
|
||||
assert_eq!((ps.scale_numerator, ps.scale_denominator), (1.0, 2540.0));
|
||||
assert!((ps.standard_scale_factor - 1.0 / 2540.0).abs() < 1e-15);
|
||||
let _ = app.on_plot_dialog_open();
|
||||
assert_eq!(app.plot_dialog.scale, "1:100");
|
||||
// A layout plot keeps the paper space in millimetres: "1:1" is
|
||||
// `1 in = 25.4 units`.
|
||||
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, 25.4));
|
||||
assert!((ps.standard_scale_factor - 1.0 / 25.4).abs() < 1e-15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -5515,6 +5601,118 @@ mod plot_paper_tests {
|
||||
assert_eq!(ps.paper_image_origin_x, 3.5);
|
||||
}
|
||||
|
||||
/// Open the metric page-setup fixture written by the commercial
|
||||
/// application (see `tests/fixtures/plot/README.md`).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn app_with_fixture() -> OpenCADStudio {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let path = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/fixtures/plot/page-setups-metric.dxf"
|
||||
);
|
||||
let reply = app.automation_op(&format!(r#"{{"op":"open","path":"{path}"}}"#));
|
||||
assert_eq!(reply["ok"], true, "{reply}");
|
||||
app
|
||||
}
|
||||
|
||||
/// The page-setup fields the file format stores, minus handles.
|
||||
fn stored_fields(ps: &acadrust::objects::PlotSettings) -> String {
|
||||
format!(
|
||||
"{} | {} | {:?} | {:.6} {:.6} | {:.6} {:.6} {:.6} {:.6} | {:.6} {:.6} | {} {} \
|
||||
| {:.6} / {:.6} | {:.9} | {:?} | {:?} | {:?} {:?} {}",
|
||||
ps.printer_name,
|
||||
ps.paper_size,
|
||||
ps.paper_units,
|
||||
ps.paper_width,
|
||||
ps.paper_height,
|
||||
ps.margins.left,
|
||||
ps.margins.bottom,
|
||||
ps.margins.right,
|
||||
ps.margins.top,
|
||||
ps.origin_x,
|
||||
ps.origin_y,
|
||||
ps.rotation.to_code(),
|
||||
ps.plot_type.to_code(),
|
||||
ps.scale_numerator,
|
||||
ps.scale_denominator,
|
||||
ps.standard_scale_factor,
|
||||
ps.scale_type,
|
||||
ps.flags,
|
||||
ps.shade_plot_mode,
|
||||
ps.shade_plot_resolution,
|
||||
ps.shade_plot_dpi
|
||||
)
|
||||
}
|
||||
|
||||
/// Every layout of the fixture survives "open the dialog, apply" without
|
||||
/// a single stored field changing — the dialog reads the source
|
||||
/// application's conventions and writes them back the same way.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn fixture_page_setups_round_trip_through_the_dialog_unchanged() {
|
||||
use acadrust::objects::{PlotRotation, ScaledType};
|
||||
let mut app = app_with_fixture();
|
||||
let a3_medium = (297.0106506347656, 419.9889831542968);
|
||||
let arch_d_medium = (914.4000244140626, 609.5999755859375);
|
||||
let expectations = [
|
||||
("A4 1-100", PlotRotation::None, (297.0, 210.0), true),
|
||||
("A3 - plotter", PlotRotation::Degrees90, a3_medium, false),
|
||||
("ARCH D - PLOTTER", PlotRotation::None, arch_d_medium, true),
|
||||
];
|
||||
for (layout, rotation, medium, pdf) in expectations {
|
||||
let orientation = "Landscape";
|
||||
let _ = app.update(Message::LayoutSwitch(layout.into()));
|
||||
let i = app.active_tab;
|
||||
let before = app.tabs[i].scene.plot_settings_for(layout).unwrap();
|
||||
assert_eq!(before.rotation, rotation, "{layout}");
|
||||
assert_eq!((before.paper_width, before.paper_height), medium, "{layout}");
|
||||
assert_eq!(before.scale_type, ScaledType::OneToOne, "{layout}");
|
||||
let _ = app.on_plot_dialog_open();
|
||||
let d = &app.plot_dialog;
|
||||
assert_eq!(d.orientation, orientation, "{layout}");
|
||||
assert!(!d.upside_down);
|
||||
assert_eq!(d.area, "Layout");
|
||||
assert_eq!(d.scale, "1:1", "{layout}: a 1:1 plot in either paper unit");
|
||||
assert_eq!(d.to_file, pdf, "{layout}");
|
||||
assert_eq!(super::plot_dialog_sheet_mm(d).0 > super::plot_dialog_sheet_mm(d).1, true);
|
||||
assert!(d.lineweights && d.apply_plot_styles && d.paperspace_last, "{layout}");
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
|
||||
let after = app.tabs[i].scene.plot_settings_for(layout).unwrap();
|
||||
assert_eq!(stored_fields(&after), stored_fields(&before), "{layout}");
|
||||
}
|
||||
// The system printer's sheet keeps its driver's margins and lands on
|
||||
// the layout the way the source application places it: the medium's
|
||||
// top margin on the left, its left margin at the bottom.
|
||||
let _ = app.update(Message::LayoutSwitch("A3 - plotter".into()));
|
||||
let ((x0, y0), (x1, y1)) = app.tabs[app.active_tab].scene.paper_limits().unwrap();
|
||||
assert!((x0 + 20.02365112304687).abs() < 1e-9 && (y0 + 4.995333194732666).abs() < 1e-9);
|
||||
assert!((x1 - 399.96533203125).abs() < 1e-6 && (y1 - 292.015317440033).abs() < 1e-6);
|
||||
}
|
||||
|
||||
/// Picking a new sheet on the fixture's PDF layout writes our canonical
|
||||
/// portrait medium with the orientation in the rotation and the PDF
|
||||
/// driver's margins for that medium.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn fixture_pdf_layout_takes_a_new_sheet_in_canonical_form() {
|
||||
use acadrust::objects::{PaperMargin, PlotRotation};
|
||||
let mut app = app_with_fixture();
|
||||
let _ = app.update(Message::LayoutSwitch("A4 1-100".into()));
|
||||
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 = app.tabs[app.active_tab].scene.plot_settings_for("A4 1-100").unwrap();
|
||||
assert_eq!(ps.printer_name, "DWG To PDF.pc3");
|
||||
assert_eq!(ps.paper_size, "ISO_A3_(297.00_x_420.00_MM)");
|
||||
assert_eq!((ps.paper_width, ps.paper_height), (297.0, 420.0));
|
||||
assert_eq!(ps.rotation, PlotRotation::Degrees90);
|
||||
assert_eq!(ps.margins, PaperMargin::new(5.0, 17.0, 6.0, 18.0));
|
||||
// The sheet sits at minus its displayed margins: top on the left,
|
||||
// left at the bottom.
|
||||
let ((x0, y0), _) = app.tabs[app.active_tab].scene.paper_limits().unwrap();
|
||||
assert!((x0 + 18.0).abs() < 1e-9 && (y0 + 5.0).abs() < 1e-9, "{x0} {y0}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scale_names_parse_to_their_ratios() {
|
||||
use crate::scene::Scene;
|
||||
@@ -5547,9 +5745,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");
|
||||
// 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);
|
||||
// …and so must the driver's landscape medium: unrotated, as stored.
|
||||
assert_eq!((w, h), (297.0, 210.0));
|
||||
assert_eq!(layout_rotation(&app), acadrust::objects::PlotRotation::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -5793,9 +5991,9 @@ cupsPrintQuality/Print Quality: *Normal High\n";
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
|
||||
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);
|
||||
// The drawing's own landscape medium stays as it was stored.
|
||||
assert_eq!((w, h), (1500.0, 609.6));
|
||||
assert_eq!(layout_rotation(&app), acadrust::objects::PlotRotation::None);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-8
@@ -33,7 +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;
|
||||
pub use page_setup::{apply_default_page_setup, rotated_margins};
|
||||
mod dwg_native_constraints;
|
||||
mod entity;
|
||||
#[cfg(test)]
|
||||
@@ -4362,13 +4362,8 @@ impl Scene {
|
||||
None
|
||||
})?;
|
||||
// `paper_limits()` already swaps the sheet for a 90°/270° rotation, so the
|
||||
// margins must rotate to the same edges: a margin on a physical side moves
|
||||
// to the displayed side that side rotates onto.
|
||||
let (ml, mb, mr, mt) = match rot {
|
||||
1 | 3 => (bottom, left, top, right),
|
||||
2 => (right, top, left, bottom),
|
||||
_ => (left, bottom, right, top),
|
||||
};
|
||||
// margins must rotate to the same edges.
|
||||
let (ml, mb, mr, mt) = page_setup::rotated_margins((left, bottom, right, top), rot);
|
||||
// Plot margins are millimetres like the paper size; scale them into the
|
||||
// layout's paper-space units so the inset matches the (already scaled)
|
||||
// sheet rect. Without this an inch paper space insets an ~8-inch sheet
|
||||
|
||||
@@ -40,6 +40,25 @@ pub fn apply_default_page_setup(layout: &mut Layout, plot_style: &str) {
|
||||
layout.plot_printer_name = crate::io::plot_device::PlotDevice::None.canonical_name();
|
||||
}
|
||||
|
||||
/// The unprintable margins of a sheet as they appear on the rotated layout,
|
||||
/// `(left, bottom, right, top)`. Stored margins belong to the medium's own
|
||||
/// edges; a plot rotated by a quarter turn (`rotation` is the file's 0–3
|
||||
/// code) shows the medium turned on the layout, so each margin moves to the
|
||||
/// edge its side lands on — verified against limits written by the
|
||||
/// commercial application: at 90° the medium's top margin is the layout's
|
||||
/// left one and its left margin the layout's bottom one.
|
||||
pub fn rotated_margins(
|
||||
(left, bottom, right, top): (f64, f64, f64, f64),
|
||||
rotation: i16,
|
||||
) -> (f64, f64, f64, f64) {
|
||||
match rotation {
|
||||
1 => (top, left, bottom, right),
|
||||
2 => (right, top, left, bottom),
|
||||
3 => (bottom, right, top, left),
|
||||
_ => (left, bottom, right, top),
|
||||
}
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
/// Handle of the `ACAD_PLOTSETTINGS` dictionary, located robustly.
|
||||
///
|
||||
|
||||
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
Page-setup fixtures saved by a commercial CAD application (2016 release, DXF
|
||||
AC1027 and the matching DWG). Provided by Panos for the plotting parity track;
|
||||
the drawing holds one 10 m line (`$INSUNITS` = 6, metres) and three paper
|
||||
layouts, each with a viewport of that line and its dimension:
|
||||
|
||||
| Layout | Device | Media (`4`) | `44`×`45` | `73` | `72` | `142`/`143` | `147` | `75` |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| `A4 1-100` | `DWG To PDF.pc3` | `ISO_A4_(297.00_x_210.00_MM)` | 297 × 210 | 0 | mm | 1 / 1 | 25.4 | 16 |
|
||||
| `A3 - plotter` | system printer `HPB602DE (HP OfficeJet Pro 7740 series)` | `A3` | 297.01 × 419.99 | 1 | mm | 1 / 1 | 25.4 | 16 |
|
||||
| `ARCH D - PLOTTER` | the application's second PDF plotter configuration (`… PDF (High Quality Print).pc3`) | `ARCH_D_(36.00_x_24.00_Inches)` | 914.4 × 609.6 | 0 | in | 1 / 25.4 | 0.03937 | 16 |
|
||||
|
||||
What the file establishes (see `src/app/update/file.rs`, `plot_settings_from_dialog`):
|
||||
|
||||
- A landscape sheet is written either as the driver's own landscape medium
|
||||
with rotation 0 (the PDF driver lists both orientations) or as a portrait
|
||||
medium with rotation 90 (system printers) — rotation is relative to the
|
||||
medium's orientation, and the medium's exact driver dimensions are kept.
|
||||
- Margins (`40`–`43`, mm) belong to the medium's own edges: the PDF driver's
|
||||
A4 has 5.79375 left/right and 17.79375 bottom/top. Layout limits (`10`/`20`)
|
||||
put the sheet at minus the *displayed* margins: at rotation 90 the medium's
|
||||
top margin (20.02) becomes the layout's left one, its left margin (5.00) the
|
||||
layout's bottom one.
|
||||
- Plot flags `70` = 672 on every layout: draw viewports first, print
|
||||
lineweights, plot with plot styles — the "use standard scale" bit stays clear
|
||||
even with a standard code in `75`; the model tab (fit to paper) sets it.
|
||||
- `147` is `142/143` × 25.4 on millimetre page setups and `142/143` itself on
|
||||
inch ones.
|
||||
- An inch page setup over a millimetre paper space stores "1:1" as
|
||||
`1 in = 25.4 units` (`75` = 16 kept).
|
||||
BIN
Binary file not shown.
+15882
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user