fix(layout): complete paperspace integration

Apply each layout's embedded page setup and plot style consistently across display, PDF, printing, and multi-layout output.\n\nRefs #844
This commit is contained in:
Hakan Seven 2026-08-26 08:58:38 +03:00
commit 8c61d89b1e
18 changed files with 1389 additions and 369 deletions

View file

@ -30,6 +30,7 @@ impl OpenCADStudio {
.document
.header
.paper_space_insertion_base = pt;
self.tabs[i].scene.persist_current_layout_state();
} else {
self.tabs[i]
.scene

View file

@ -412,14 +412,7 @@ impl OpenCADStudio {
vec![]
};
// Blocks: everything the layout-reachability walk above did
// NOT mark live. That covers unreferenced named blocks,
// leftover anonymous blocks (*U hatch/unnamed, orphaned *D
// dimension, *T table, dynamic-block variants — AutoCAD drops
// these too), AND whole dead subgraphs a detached xref leaves
// behind. Live xrefs stay (an attached xref's blocks are
// reached from the layout that inserts them); the *Model_Space
// / *Paper_Space containers are never removed.
// Remove unreachable blocks but retain live xrefs and space containers.
let block_remove: Vec<String> = if do_blocks {
self.tabs[i]
.scene
@ -494,15 +487,7 @@ impl OpenCADStudio {
n_blocks += block_remove.len();
}
// Draw-order cleanup. A SortEntitiesTable lists a block's
// entities in draw order; purging the block drops the entities
// and the block record, but this object lingers, still holding
// the (now dangling) handle of every deleted entity. On a
// detached-xref purge that is hundreds of thousands of stale
// handles — megabytes of dead weight the save re-emits, which is
// why an OCS-purged file stayed far larger than AutoCAD's.
// Drop every SortEntitiesTable whose owning block is gone
// (AutoCAD's "orphaned data").
// Remove draw-order tables whose owning blocks are gone.
let mut n_sortents = 0usize;
if do_blocks {
let live_blocks: rustc_hash::FxHashSet<acadrust::Handle> = self.tabs[i]
@ -1913,6 +1898,9 @@ impl OpenCADStudio {
if name == "FILLMODE" {
self.tabs[i].scene.bump_geometry();
}
if matches!(name.as_str(), "PSLTSCALE" | "PLIMCHECK") {
self.tabs[i].scene.persist_current_layout_state();
}
self.command_line.push_output(&msg);
} else {
// Queried with no value (`changed == false`):

View file

@ -568,63 +568,49 @@ impl OpenCADStudio {
}
} else {
// ── Preset viewport layout ───────────────────────────
// Determine paper dimensions from PlotSettings (fallback A4 landscape).
let layout_name = scene.current_layout.clone();
let (paper_w, paper_h) = {
use acadrust::objects::ObjectType;
let mut pw = 297.0_f64;
let mut ph = 210.0_f64;
for (_, obj) in &scene.document.objects {
if let ObjectType::PlotSettings(ps) = obj {
if ps.page_name == layout_name && ps.paper_width > 0.0 {
pw = ps.paper_width;
ph = ps.paper_height;
break;
}
}
}
(pw, ph)
};
let margin = 5.0_f64; // mm margin around the usable area
let uw = paper_w - 2.0 * margin; // usable width
let uh = paper_h - 2.0 * margin; // usable height
// Collect rectangle specs: (cx, cz, w, h) in mm
let ((x0, y0), (x1, y1)) = scene
.printable_area_limits()
.unwrap_or(((0.0, 0.0), (297.0, 210.0)));
let uw = (x1 - x0).max(1.0);
let uh = (y1 - y0).max(1.0);
let gap = 2.0 * scene.paper_space_unit_factor();
let rects: Vec<(f64, f64, f64, f64)> = match sub.as_str() {
"2H" => {
// Two viewports side by side (horizontal split)
let vw = (uw - 2.0) / 2.0;
let vw = (uw - gap) / 2.0;
vec![
(margin + vw / 2.0, margin + uh / 2.0, vw, uh),
(margin + vw + 2.0 + vw / 2.0, margin + uh / 2.0, vw, uh),
(x0 + vw / 2.0, y0 + uh / 2.0, vw, uh),
(x0 + vw + gap + vw / 2.0, y0 + uh / 2.0, vw, uh),
]
}
"2V" => {
// Two viewports stacked (vertical split)
let vh = (uh - 2.0) / 2.0;
let vh = (uh - gap) / 2.0;
vec![
(margin + uw / 2.0, margin + vh + 2.0 + vh / 2.0, uw, vh),
(margin + uw / 2.0, margin + vh / 2.0, uw, vh),
(x0 + uw / 2.0, y0 + vh + gap + vh / 2.0, uw, vh),
(x0 + uw / 2.0, y0 + vh / 2.0, uw, vh),
]
}
"4" => {
// Four equal viewports (2×2 grid)
let vw = (uw - 2.0) / 2.0;
let vh = (uh - 2.0) / 2.0;
let vw = (uw - gap) / 2.0;
let vh = (uh - gap) / 2.0;
vec![
(margin + vw / 2.0, margin + vh + 2.0 + vh / 2.0, vw, vh),
(x0 + vw / 2.0, y0 + vh + gap + vh / 2.0, vw, vh),
(
margin + vw + 2.0 + vw / 2.0,
margin + vh + 2.0 + vh / 2.0,
x0 + vw + gap + vw / 2.0,
y0 + vh + gap + vh / 2.0,
vw,
vh,
),
(margin + vw / 2.0, margin + vh / 2.0, vw, vh),
(margin + vw + 2.0 + vw / 2.0, margin + vh / 2.0, vw, vh),
(x0 + vw / 2.0, y0 + vh / 2.0, vw, vh),
(x0 + vw + gap + vw / 2.0, y0 + vh / 2.0, vw, vh),
]
}
"SINGLE" | "1" => {
// Single full-page viewport
vec![(margin + uw / 2.0, margin + uh / 2.0, uw, uh)]
vec![(x0 + uw / 2.0, y0 + uh / 2.0, uw, uh)]
}
_ => {
self.command_line.push_error(

View file

@ -256,12 +256,11 @@ impl DocumentTab {
.unwrap_or(glam::DVec3::ZERO)
}
/// True when the active pane edits **model-space** geometry: the Model tab,
/// or inside a floating viewport (MSPACE). The UCS applies in both; plain
/// paper space (no active viewport) is excluded. Single predicate so every
/// UCS-aware system shares one rule. [[feedback_shared_infra]]
/// True when the active pane has a drawing coordinate plane.
pub(super) fn editing_model_space(&self) -> bool {
self.scene.current_layout == "Model" || self.scene.active_viewport.is_some()
self.scene.current_layout == "Model"
|| self.scene.active_viewport.is_some()
|| self.active_ucs.is_some()
}
/// The document's saved model-space UCS (header), as a `Ucs`. `None` when it
@ -321,9 +320,51 @@ impl DocumentTab {
}
}
pub(super) fn ucs_from_layout(&self) -> Option<Ucs> {
let layout = self.scene.document.objects.values().find_map(|object| {
let acadrust::objects::ObjectType::Layout(layout) = object else {
return None;
};
(layout.name == self.scene.current_layout).then_some(layout)
})?;
let mut ucs = self
.scene
.document
.ucss
.iter()
.find(|ucs| ucs.handle == layout.named_ucs)
.cloned()
.unwrap_or_else(|| Ucs::new("*PAPERUCS*"));
ucs.origin = acadrust::types::Vector3::new(
layout.ucs_origin.0,
layout.ucs_origin.1,
layout.ucs_origin.2,
);
ucs.x_axis = acadrust::types::Vector3::new(
layout.ucs_x_axis.0,
layout.ucs_x_axis.1,
layout.ucs_x_axis.2,
);
ucs.y_axis = acadrust::types::Vector3::new(
layout.ucs_y_axis.0,
layout.ucs_y_axis.1,
layout.ucs_y_axis.2,
);
ucs.elevation = layout.elevation;
ucs.ortho_type = layout.ucs_ortho_type;
ucs.named_ucs_handle = layout.named_ucs;
ucs.base_ucs_handle = layout.base_ucs;
let is_world = layout.named_ucs.is_null()
&& layout.base_ucs.is_null()
&& layout.ucs_ortho_type == 0
&& layout.elevation.abs() <= f64::EPSILON
&& super::helpers::UcsXform::from_ucs(&ucs).is_identity();
(!is_world).then_some(ucs)
}
/// Set `active_ucs` to the UCS of the *current pane*: the entered viewport's
/// own per-viewport UCS, the model header UCS in the Model tab, or none in
/// plain paper space. Keeps the ViewCube in lock-step. Call on every pane
/// own per-viewport UCS, the model header UCS, or the layout UCS. Keeps the
/// ViewCube in lock-step. Call on every pane
/// change (enter/exit viewport, layout / tab switch, load) so one field
/// drives all UCS-aware systems regardless of where editing happens.
pub(super) fn refresh_active_ucs(&mut self) {
@ -334,15 +375,52 @@ impl DocumentTab {
} else if self.scene.current_layout == "Model" {
self.model_ucs_from_header()
} else {
None
self.ucs_from_layout()
};
if self.active_block_edit.is_none()
&& self.scene.active_viewport.is_none()
&& self.scene.current_layout != "Model"
{
self.sync_paper_ucs_header();
}
self.sync_ucs_to_scene();
}
fn sync_paper_ucs_header(&mut self) {
use acadrust::types::Vector3;
let header = &mut self.scene.document.header;
match &self.active_ucs {
Some(ucs) => {
header.paper_space_ucs_origin = ucs.origin;
header.paper_space_ucs_x_axis = ucs.x_axis;
header.paper_space_ucs_y_axis = ucs.y_axis;
header.paper_elevation = ucs.elevation;
header.paper_space_ucs_name = if (ucs.named_ucs_handle.is_valid()
|| ucs.handle.is_valid())
&& !ucs.name.starts_with('*')
{
ucs.name.clone()
} else {
String::new()
};
header.paper_ucs_ortho_ref = ucs.base_ucs_handle;
header.paper_ucs_ortho_view = ucs.ortho_type;
}
None => {
header.paper_space_ucs_origin = Vector3::ZERO;
header.paper_space_ucs_x_axis = Vector3::UNIT_X;
header.paper_space_ucs_y_axis = Vector3::UNIT_Y;
header.paper_elevation = 0.0;
header.paper_space_ucs_name.clear();
header.paper_ucs_ortho_ref = Handle::NULL;
header.paper_ucs_ortho_view = 0;
}
}
}
/// Persist `active_ucs` back to its pane's storage so it round-trips: the
/// entered viewport's per-viewport UCS fields, or the document header's
/// model-space UCS in the Model tab. No-op in plain paper space. Call after
/// any UCS change.
/// model-space UCS in the Model tab, or the layout UCS. Call after a change.
pub(super) fn persist_active_ucs(&mut self) {
use acadrust::types::Vector3;
if let Some(index) = self.active_block_edit {
@ -385,6 +463,48 @@ impl DocumentTab {
h.model_space_ucs_name.clear();
}
}
} else {
let (origin, x_axis, y_axis, elevation, ortho, named, base) =
match &self.active_ucs {
Some(ucs) => (
ucs.origin,
ucs.x_axis,
ucs.y_axis,
ucs.elevation,
ucs.ortho_type,
if ucs.named_ucs_handle.is_valid() {
ucs.named_ucs_handle
} else {
ucs.handle
},
ucs.base_ucs_handle,
),
None => (
Vector3::ZERO,
Vector3::UNIT_X,
Vector3::UNIT_Y,
0.0,
0,
Handle::NULL,
Handle::NULL,
),
};
for object in self.scene.document.objects.values_mut() {
let acadrust::objects::ObjectType::Layout(layout) = object else {
continue;
};
if layout.name == self.scene.current_layout {
layout.ucs_origin = (origin.x, origin.y, origin.z);
layout.ucs_x_axis = (x_axis.x, x_axis.y, x_axis.z);
layout.ucs_y_axis = (y_axis.x, y_axis.y, y_axis.z);
layout.elevation = elevation;
layout.ucs_ortho_type = ortho;
layout.named_ucs = named;
layout.base_ucs = base;
break;
}
}
self.sync_paper_ucs_header();
}
}

View file

@ -810,16 +810,21 @@ pub(super) struct OpenCADStudio {
/// Snapshot of the dialog's settings taken when it opened, restored by the
/// `<previous>` list entry.
plot_prev: Option<crate::ui::window::plot::PlotDialogState>,
/// Full source settings behind the fields currently shown in Plot.
plot_setup_template: Option<acadrust::objects::PlotSettings>,
/// Paper layouts shown by Print All, in tab order with their selection.
print_all_layouts: Vec<(String, bool)>,
/// True while the Plot dialog is editing settings for Print All.
print_all_options: bool,
/// True when Print All should override each layout's page setup.
print_all_settings_override: bool,
/// Settings restored when the Print All options dialog is cancelled.
print_all_options_prev: Option<crate::ui::window::plot::PlotDialogState>,
/// Plot style restored together with cancelled Print All options.
print_all_plot_style_prev: Option<Option<crate::io::plot_style::PlotStyleTable>>,
/// Plot window restored together with cancelled Print All options.
print_all_plot_window_prev: Option<Option<(f64, f64, f64, f64)>>,
print_all_plot_setup_prev: Option<Option<acadrust::objects::PlotSettings>>,
// ── Plot Style Table ──────────────────────────────────────────────────
/// Currently loaded CTB/STB table (None = no override).
@ -3287,11 +3292,14 @@ impl OpenCADStudio {
plot_orientation: crate::io::paper_sizes::Orientation::Landscape,
plot_dialog: crate::ui::window::plot::PlotDialogState::default(),
plot_prev: None,
plot_setup_template: None,
print_all_layouts: Vec::new(),
print_all_options: false,
print_all_settings_override: false,
print_all_options_prev: None,
print_all_plot_style_prev: None,
print_all_plot_window_prev: None,
print_all_plot_setup_prev: None,
opening: None,
open_job_serial: 0,
recovery_report: None,

View file

@ -120,6 +120,32 @@ type ClippedPlotParams = (
Option<(f32, f32, f32, f32)>,
);
fn plot_dialog_sheet_mm(d: &crate::ui::window::plot::PlotDialogState) -> (f64, f64) {
use crate::io::paper_sizes::{sheet_mm, Orientation, PaperSize};
let orientation = if d.orientation == "Portrait" {
Orientation::Portrait
} else {
Orientation::Landscape
};
let standard = match d.paper.as_str() {
"A3" => Some(PaperSize::A3),
"A2" => Some(PaperSize::A2),
"A1" => Some(PaperSize::A1),
"A0" => Some(PaperSize::A0),
"A4" => Some(PaperSize::A4),
_ => None,
};
if let Some(paper) = standard {
return sheet_mm(paper, orientation);
}
let short = d.paper_width_mm.min(d.paper_height_mm).max(1.0);
let long = d.paper_width_mm.max(d.paper_height_mm).max(1.0);
match orientation {
Orientation::Portrait => (short, long),
Orientation::Landscape => (long, short),
}
}
fn plot_content_extents(
wires: &[crate::io::pdf_export::PlotWire],
hatches: &[crate::scene::model::hatch_model::HatchModel],
@ -169,7 +195,18 @@ fn plot_scene_content(
crate::io::pdf_export::PlotGroupSplits,
) {
let (mut paper_wires, mut model_wires) = scene.plot_wire_groups(render_mode_override);
paper_wires.retain(|wire| wire.plot_visible);
let plot_viewport_borders = scene
.effective_plot_settings()
.is_none_or(|settings| settings.flags.plot_viewport_borders);
paper_wires.retain(|wire| {
wire.plot_visible
&& (plot_viewport_borders
|| !crate::scene::Scene::handle_from_wire_name(&wire.name)
.and_then(|handle| scene.document.get_entity(handle))
.is_some_and(|entity| {
matches!(entity, acadrust::EntityType::Viewport(viewport) if crate::scene::Scene::is_content_viewport(viewport))
}))
});
model_wires.retain(|wire| wire.plot_visible);
let with_depth = |wires: Vec<crate::scene::WireModel>| {
let depths = scene.plot_wire_depths(&wires);
@ -1364,6 +1401,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
.nth(1)
.unwrap_or_else(|| "Model".to_string()),
};
self.tabs[i].scene.load_current_layout_state();
self.tabs[i].refresh_active_ucs();
}
// Object isolation is session-only. A newly opened drawing must
// not inherit the previous tab's filter, and persisted entity
@ -2662,8 +2701,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
iced::exit()
}
/// Write the given plot page settings into the active layout's Layout +
/// PlotSettings objects (paper size, plot area, offset, rotation, scale).
/// 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)]
@ -2684,118 +2722,97 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
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(""));
ps.paper_width = w;
ps.paper_height = h;
ps.paper_size = dialog.paper.clone();
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.printer_name = if dialog.to_file {
crate::ui::window::plot::OUT_PDF.into()
} else {
dialog.printer.clone().unwrap_or_default()
};
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;
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(l) = obj {
if l.name == layout_name {
l.min_limits = (0.0, 0.0);
l.max_limits = (w, h);
l.min_extents = (0.0, 0.0, 0.0);
l.max_extents = (w, h, 0.0);
l.paper_width = w;
l.paper_height = h;
l.plot_rotation = 0;
l.plot_paper_units = 1;
l.plot_origin_x = offset_x;
l.plot_origin_y = offset_y;
l.paper_size = String::new();
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;
}
}
}
use acadrust::objects::{
ObjectType, PlotPaperUnits, PlotRotation, PlotSettings, PlotType, ScaledType,
ShadePlotMode, ShadePlotResolutionLevel,
};
let plot_handle = self.tabs[i]
.scene
.document
.objects
.iter()
.find_map(|(h, obj)| {
if let ObjectType::PlotSettings(ps) = obj {
(ps.page_name == layout_name).then_some(*h)
} else {
None
}
});
let ps_entry = if let Some(h) = plot_handle {
self.tabs[i].scene.document.objects.get_mut(&h)
} else {
let mut ps = PlotSettings::new(layout_name.clone());
ps.handle = self.tabs[i].scene.document.allocate_handle();
let h = ps.handle;
self.tabs[i]
.scene
.document
.objects
.insert(h, ObjectType::PlotSettings(ps));
self.tabs[i].scene.document.objects.get_mut(&h)
};
if let Some(ObjectType::PlotSettings(ps)) = ps_entry {
ps.paper_width = w;
ps.paper_height = h;
ps.paper_size = dialog.paper.clone();
ps.paper_units = PlotPaperUnits::Millimeters;
ps.plot_type = match plot_area {
"Window" => PlotType::Window,
"Display" => PlotType::LastScreenDisplay,
"Extents" => PlotType::Extents,
_ => PlotType::Layout,
};
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 {
let factor = if plot_area == "Layout" {
1.0
} else {
plot_dialog_scale_factor(&dialog)
};
ps.scale_type = ScaledType::CustomScale;
ps.scale_numerator = factor;
ps.scale_denominator = 1.0;
}
ps.printer_name = if dialog.to_file {
crate::ui::window::plot::OUT_PDF.into()
} else {
dialog.printer.clone().unwrap_or_default()
};
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.style_name.is_empty();
ps.flags.draw_viewports_first = dialog.paperspace_last;
ps.flags.plot_hidden = false;
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;
}
self.tabs[i].dirty = true;
self.tabs[i].scene.bump_geometry_no_blocks();
self.command_line.push_info(crate::tf!(
@ -2809,6 +2826,19 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
&mut self,
path: std::path::PathBuf,
) -> Task<Message> {
let i = self.active_tab;
if self.tabs[i].scene.current_layout != "Model" {
self.plot_dialog.paper_space = true;
self.plot_dialog.scales = self.tabs[i]
.scene
.scale_list()
.into_iter()
.map(|(name, _, factor)| (name, factor))
.collect();
if let Some(settings) = self.tabs[i].scene.effective_plot_settings() {
self.load_plotsettings_into_dialog(&settings);
}
}
let Some((wires, hatches, wipeouts, group_splits, page_w, page_h, ox, oy, rotation, scale, clip)) =
self.direct_plot_params()
else {
@ -2911,6 +2941,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
.filter(|name| name != "Model")
.map(|name| (name, true))
.collect();
self.print_all_settings_override = false;
self.active_modal = Some(crate::app::ModalKind::PrintAll);
self.reset_modal_geometry();
Task::none()
@ -2920,10 +2951,12 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
let previous = self.plot_dialog.clone();
let previous_style = self.active_plot_style.clone();
let previous_window = self.plot_window;
let previous_setup = self.plot_setup_template.clone();
let task = self.on_plot_dialog_open();
self.print_all_options_prev = Some(previous);
self.print_all_plot_style_prev = Some(previous_style);
self.print_all_plot_window_prev = Some(previous_window);
self.print_all_plot_setup_prev = Some(previous_setup);
self.print_all_options = true;
self.plot_dialog.paper_space = true;
self.plot_dialog.area = "Layout".into();
@ -2945,47 +2978,145 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
let i = self.active_tab;
let original_layout = self.tabs[i].scene.current_layout.clone();
let original_viewport = self.tabs[i].scene.active_viewport;
let dialog = self.plot_dialog.clone();
let mut pages = Vec::with_capacity(selected.len());
for name in selected {
// Plot helpers read the active layout. Swap only this transient
// selector so the drawing's saved active-space metadata is not
// touched while the owned page snapshot is collected.
{
let scene = &mut self.tabs[i].scene;
scene.current_layout = name;
scene.active_viewport = None;
let original_psltscale = self.tabs[i]
.scene
.document
.header
.paper_space_linetype_scaling;
let original_plimcheck = self.tabs[i]
.scene
.document
.header
.paper_space_limit_check;
let original_dialog = self.plot_dialog.clone();
let original_style = self.active_plot_style.clone();
let original_window = self.plot_window;
let original_setup = self.plot_setup_template.clone();
let original_camera = self.tabs[i].scene.camera.borrow().clone();
let original_camera_generation = self.tabs[i].scene.camera_generation;
let override_dialog = self.print_all_settings_override.then(|| original_dialog.clone());
let override_style = self.print_all_settings_override.then(|| original_style.clone());
let result = (|| {
let mut pages = Vec::with_capacity(selected.len());
for name in selected {
let page_setup = self.tabs[i]
.scene
.plot_settings_for(&name)
.ok_or_else(|| format!("Layout '{name}' has no page setup."))?;
{
let scene = &mut self.tabs[i].scene;
scene.current_layout = name.clone();
scene.active_viewport = None;
scene.load_current_layout_state();
}
if let Some(dialog) = &override_dialog {
self.plot_dialog = dialog.clone();
self.active_plot_style = override_style.clone().flatten();
self.plot_window = original_window;
} else {
self.plot_dialog.paper_space = true;
self.plot_window = None;
self.load_plotsettings_into_dialog(&page_setup);
}
let dialog = self.plot_dialog.clone();
if dialog.area == "Display" {
self.tabs[i].scene.restore_saved_camera();
}
if dialog.style_missing && dialog.apply_plot_styles {
return Err(format!(
"Layout '{name}' plot style table '{}' is not loaded.",
dialog.style_name
));
}
let plot_style = self.dialog_plot_style(&dialog);
let params = match dialog.area.as_str() {
"Display" => self.display_plot_job(),
"Extents" => self.extents_plot_job(),
"Limits" => self.limits_plot_job(),
"Window" => self.window_plot_job(),
area if area.starts_with("View: ") => {
self.named_view_plot_job(area.trim_start_matches("View: "))
}
_ => None,
};
let (
wires,
hatches,
wipeouts,
group_splits,
paper_w,
paper_h,
offset_x,
offset_y,
rotation_deg,
scale,
clip,
) = if dialog.area == "Layout" {
self.layout_plot_params_for("Layout")
} else {
let (
wires,
hatches,
wipeouts,
group_splits,
paper_w,
paper_h,
offset_x,
offset_y,
rotation_deg,
scale,
clip,
) = params.ok_or_else(|| format!("Layout '{name}' plot area is empty."))?;
(
std::sync::Arc::new(wires),
hatches,
wipeouts,
group_splits,
paper_w,
paper_h,
offset_x,
offset_y,
rotation_deg,
scale,
clip,
)
};
pages.push(crate::io::pdf_export::PdfPageInput {
wires,
hatches,
wipeouts,
paper_w,
paper_h,
offset_x,
offset_y,
rotation_deg,
scale,
clip,
options: Self::pdf_plot_options(&dialog, group_splits),
plot_style,
});
}
let (
wires,
hatches,
wipeouts,
group_splits,
paper_w,
paper_h,
offset_x,
offset_y,
rotation_deg,
scale,
clip,
) = self.layout_plot_params_for("Layout");
pages.push(crate::io::pdf_export::PdfPageInput {
wires,
hatches,
wipeouts,
paper_w,
paper_h,
offset_x,
offset_y,
rotation_deg,
scale,
clip,
options: Self::pdf_plot_options(&dialog, group_splits),
});
}
Ok(pages)
})();
self.tabs[i].scene.current_layout = original_layout;
self.tabs[i].scene.active_viewport = original_viewport;
Ok(pages)
self.tabs[i]
.scene
.document
.header
.paper_space_linetype_scaling = original_psltscale;
self.tabs[i]
.scene
.document
.header
.paper_space_limit_check = original_plimcheck;
self.plot_dialog = original_dialog;
self.active_plot_style = original_style;
self.plot_window = original_window;
self.plot_setup_template = original_setup;
*self.tabs[i].scene.camera.borrow_mut() = original_camera;
self.tabs[i].scene.camera_generation = original_camera_generation;
result
}
pub(super) fn on_print_all_pdf_path_some(
@ -2993,7 +3124,10 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
path: std::path::PathBuf,
) -> Task<Message> {
let dialog = self.plot_dialog.clone();
if dialog.style_missing {
if self.print_all_settings_override
&& dialog.style_missing
&& dialog.apply_plot_styles
{
self.command_line.push_error(crate::tf!(
"Plot style table '{}' is not loaded.",
dialog.style_name
@ -3007,7 +3141,6 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
return Task::none();
}
};
let plot_style = self.dialog_plot_style(&dialog);
let worker_path = path.clone();
self.save_config();
self.close_active_modal();
@ -3015,7 +3148,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
crate::io::pdf_export::export_pdf_pages(
&pages,
&worker_path,
plot_style.as_ref(),
None,
)
.map(|_| format!("Exported {} layouts to {}", pages.len(), worker_path.display()))
.map_err(|error| format!("Export failed: {error}"))
@ -3034,7 +3167,10 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
#[cfg(not(target_arch = "wasm32"))]
{
let dialog = self.plot_dialog.clone();
if dialog.style_missing {
if self.print_all_settings_override
&& dialog.style_missing
&& dialog.apply_plot_styles
{
self.command_line.push_error(crate::tf!(
"Plot style table '{}' is not loaded.",
dialog.style_name
@ -3048,7 +3184,6 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
return Task::none();
}
};
let plot_style = self.dialog_plot_style(&dialog);
let options = self.plot_print_options(&dialog, Default::default());
let temp_path = crate::io::print_to_printer::temp_pdf_path("print_all");
self.save_config();
@ -3060,7 +3195,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
crate::io::pdf_export::export_pdf_pages(
&pages,
&temp_path,
plot_style.as_ref(),
None,
)
.and_then(|_| {
crate::io::print_to_printer::print_existing_pdf(&temp_path, &options)
@ -3112,23 +3247,10 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
}
fn layout_plot_params_for(&self, plot_area: &str) -> LayoutPlotParams {
use crate::io::paper_sizes::{sheet_mm, Orientation, PaperSize};
let i = self.active_tab;
let scene = &self.tabs[i].scene;
let paper_space = scene.current_layout != "Model";
let selected_paper = match self.plot_dialog.paper.as_str() {
"A3" => PaperSize::A3,
"A2" => PaperSize::A2,
"A1" => PaperSize::A1,
"A0" => PaperSize::A0,
_ => PaperSize::A4,
};
let selected_orientation = if self.plot_dialog.orientation == "Portrait" {
Orientation::Portrait
} else {
Orientation::Landscape
};
let selected_sheet = sheet_mm(selected_paper, selected_orientation);
let selected_sheet = plot_dialog_sheet_mm(&self.plot_dialog);
let (source_wires, hatches, wipeouts, mut group_splits) =
plot_scene_content(
scene,
@ -3463,14 +3585,21 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
let one_to_one = scale_name_for_factor(&scales, 1.0)
.or_else(|| scales.first().map(|(name, _)| name.clone()))
.unwrap_or_else(|| "1:1".into());
// Keep the user's last print preferences (printer, copies, quality,
// output options — persisted in `self.plot_dialog`); only refresh the
// runtime printer list and reseed the drawing-specific fields from the
// active layout.
let paper_space = self.tabs[self.active_tab].scene.current_layout != "Model";
let plot_views = self.tabs[self.active_tab]
.scene
.document
.views
.iter()
.filter(|view| view.paper_space == paper_space)
.map(|view| view.name.clone())
.collect();
// Keep session-only choices while loading drawing fields from the layout.
let d = &mut self.plot_dialog;
d.printers = crate::io::print_to_printer::list_printers();
d.plot_styles = crate::io::plot_style::available_ctb_names();
d.scales = scales;
d.plot_views = plot_views;
if d.scale.eq_ignore_ascii_case("fit") {
d.fit_to_paper = true;
d.scale_lw = false;
@ -3478,7 +3607,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
} else if !d.scales.iter().any(|(name, _)| name == &d.scale) {
d.scale = one_to_one.clone();
}
d.paper_space = self.tabs[self.active_tab].scene.current_layout != "Model";
d.paper_space = paper_space;
d.paper = self.plot_format.label().to_string();
d.orientation = match self.plot_orientation {
Orientation::Portrait => "Portrait",
@ -3510,32 +3639,24 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
.map(|t| t.name.clone())
.unwrap_or_default();
d.style_missing = false;
// `area`, scale and fit are remembered user choices,
// not reseeded from the layout. Offset / center / rotation ARE layout
// properties, so reflect them.
if let Some(ps) = self.tabs[self.active_tab].scene.effective_plot_settings() {
let d = &mut self.plot_dialog;
d.center = ps.flags.plot_centered;
d.offset_x = format!("{:.2}", ps.origin_x);
d.offset_y = format!("{:.2}", ps.origin_y);
let deg = ps.rotation.to_degrees() as i32;
d.upside_down = matches!(deg, 180 | 270);
}
self.plot_dialog.name_input = None;
self.plot_dialog.name_rename = false;
if self.plot_dialog.fit_to_paper {
self.plot_dialog.scale_lw = false;
}
// Refresh document/runtime lists, then restore the live choices from
// the preceding dialog session. Previously the snapshot happened
// after the layout values above were reloaded, so opening Plot itself
// destroyed the user's last paper, area, scale and output choices.
self.refresh_page_setups();
self.plot_prev = Some(previous);
self.select_page_setup(crate::ui::window::plot::SETUP_PREV);
// `<previous>` may come from another tab/drawing whose custom scale is
// not present in this document. Validate again after restoring it; the
// pre-restore validation above only saw the temporarily reseeded state.
let cur = self.tabs[self.active_tab].scene.current_layout.clone();
let layout_entry = format!("*{cur}*");
if self.tabs[self.active_tab]
.scene
.plot_settings_for(&cur)
.is_some()
{
self.select_page_setup(&layout_entry);
} else {
self.select_page_setup(crate::ui::window::plot::SETUP_PREV);
}
if self.plot_dialog.scale.eq_ignore_ascii_case("fit") {
self.plot_dialog.fit_to_paper = true;
self.plot_dialog.scale = one_to_one.clone();
@ -3550,10 +3671,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
if self.plot_dialog.fit_to_paper {
self.plot_dialog.scale_lw = false;
}
// A model-space plot cannot use the paper-only Layout area. Other
// values remain exactly as the user left them; selecting a layout or
// named setup explicitly still reloads that setup below.
let cur = self.tabs[self.active_tab].scene.current_layout.clone();
// A model-space plot cannot use the paper-only Layout area.
if cur == "Model" {
if self.plot_dialog.area == "Layout" {
self.plot_dialog.area = "Window".into();
@ -3608,10 +3726,16 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
}
M::Paper(s) => {
self.plot_dialog.paper = s;
let (w, h) = plot_dialog_sheet_mm(&self.plot_dialog);
self.plot_dialog.paper_width_mm = w;
self.plot_dialog.paper_height_mm = h;
Task::none()
}
M::Orientation(s) => {
self.plot_dialog.orientation = s;
let (w, h) = plot_dialog_sheet_mm(&self.plot_dialog);
self.plot_dialog.paper_width_mm = w;
self.plot_dialog.paper_height_mm = h;
Task::none()
}
M::Area(s) => {
@ -3681,6 +3805,12 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
PlotFlag::ScaleLw if !d.fit_to_paper => {
d.scale_lw = !d.scale_lw
}
PlotFlag::PlotStyles if !d.style_name.is_empty() => {
d.apply_plot_styles = !d.apply_plot_styles
}
PlotFlag::DisplayStyles if d.paper_space && !d.style_name.is_empty() => {
d.show_plot_styles = !d.show_plot_styles
}
PlotFlag::UpsideDown => {
d.upside_down = !d.upside_down;
}
@ -3700,11 +3830,14 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
if name == STYLE_NONE {
self.active_plot_style = None;
self.plot_dialog.style_name.clear();
self.plot_dialog.apply_plot_styles = false;
self.plot_dialog.show_plot_styles = false;
self.plot_dialog.style_missing = false;
} else {
match crate::io::plot_style::PlotStyleTable::load_named(&name) {
Ok(table) => {
self.plot_dialog.style_name = table.name.clone();
self.plot_dialog.apply_plot_styles = true;
self.plot_dialog.style_missing = false;
self.active_plot_style = Some(table);
}
@ -3852,7 +3985,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
}
M::Preview => self.on_plot_dlg_commit(true),
M::Commit if self.print_all_options => {
if self.plot_dialog.style_missing {
if self.plot_dialog.style_missing && self.plot_dialog.apply_plot_styles {
self.command_line.push_error(crate::tf!(
"Plot style table '{}' is not loaded.",
self.plot_dialog.style_name
@ -3863,9 +3996,11 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
self.plot_dialog.area = "Layout".into();
self.sync_dialog_plot_runtime();
self.save_config();
self.print_all_settings_override = true;
self.print_all_options = false;
self.print_all_options_prev = None;
self.print_all_plot_style_prev = None;
self.print_all_plot_setup_prev = None;
if let Some(previous) = self.print_all_plot_window_prev.take() {
self.plot_window = previous;
}
@ -3894,12 +4029,15 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
use crate::ui::window::plot::{SETUP_NONE, SETUP_PREV};
self.plot_dialog.selected_setup = name.to_string();
if name == SETUP_NONE {
self.plot_setup_template = None;
// No page setup: default geometry + PDF output.
let is_model = self.tabs[self.active_tab].scene.current_layout == "Model";
let d = &mut self.plot_dialog;
d.to_file = true;
d.paper = "A4".into();
d.orientation = "Landscape".into();
d.paper_width_mm = 297.0;
d.paper_height_mm = 210.0;
d.area = if is_model { "Window".into() } else { "Layout".into() };
d.center = true;
d.offset_x = "0.0".into();
@ -3947,32 +4085,32 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
PlotPaperUnits, PlotRotation, PlotSettings, PlotType, ScaledType, ShadePlotMode,
ShadePlotResolutionLevel,
};
use crate::io::paper_sizes::{sheet_mm, Orientation, PaperSize};
let d = &self.plot_dialog;
let paper = match d.paper.as_str() {
"A3" => PaperSize::A3,
"A2" => PaperSize::A2,
"A1" => PaperSize::A1,
"A0" => PaperSize::A0,
_ => PaperSize::A4,
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 orient = if d.orientation == "Portrait" {
Orientation::Portrait
} else {
Orientation::Landscape
};
let (w, h) = sheet_mm(paper, orient);
let mut ps = PlotSettings::new("");
ps.paper_width = w;
ps.paper_height = h;
ps.paper_size = d.paper.clone();
ps.paper_units = PlotPaperUnits::Millimeters;
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);
@ -3998,6 +4136,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
};
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 {
@ -4005,6 +4144,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
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.printer_name = if d.to_file {
crate::ui::window::plot::OUT_PDF.into()
@ -4014,9 +4155,10 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
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.style_name.is_empty();
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 = false;
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,
@ -4037,6 +4179,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
use acadrust::objects::{
PlotType, ShadePlotMode, ShadePlotResolutionLevel,
};
self.plot_setup_template = Some(ps.clone());
if matches!(ps.plot_type, PlotType::Window) && !ps.plot_window.is_empty() {
self.plot_window = Some((
ps.plot_window.lower_left_x,
@ -4059,7 +4202,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
}
let active_style_name = self.active_plot_style.as_ref().map(|table| table.name.clone());
let style_name = if ps.current_style_sheet.is_empty() {
active_style_name.clone().unwrap_or_default()
String::new()
} else if active_style_name
.as_deref()
.is_some_and(|name| name.eq_ignore_ascii_case(&ps.current_style_sheet))
@ -4072,17 +4215,30 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
&& active_style_name
.as_deref()
.is_some_and(|name| name.eq_ignore_ascii_case(&style_name));
let (paper, orient) = paper_label_from_dims(ps.paper_width, ps.paper_height);
let (mut paper, orient) = paper_label_from_dims(ps.paper_width, ps.paper_height);
if !matches!(paper.as_str(), "A4" | "A3" | "A2" | "A1" | "A0")
&& !ps.paper_size.is_empty()
{
paper = ps.paper_size.clone();
}
let d = &mut self.plot_dialog;
d.paper = paper;
d.paper_width_mm = ps.paper_width.max(1.0);
d.paper_height_mm = ps.paper_height.max(1.0);
d.orientation = orient;
d.area = match ps.plot_type {
PlotType::Window => "Window",
PlotType::Layout => "Layout",
PlotType::LastScreenDisplay => "Display",
_ => "Extents",
}
.to_string();
PlotType::Window => "Window".to_string(),
PlotType::Layout => "Layout".to_string(),
PlotType::LastScreenDisplay => "Display".to_string(),
PlotType::Limits => "Limits".to_string(),
PlotType::View if !ps.plot_view_name.is_empty() => {
d.plot_views.push(ps.plot_view_name.clone());
d.plot_views.sort();
d.plot_views.dedup();
format!("View: {}", ps.plot_view_name)
}
_ => "Extents".to_string(),
};
if !d.paper_space && d.area == "Layout" {
d.area = "Extents".into();
}
@ -4102,6 +4258,12 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
d.fit_to_paper = d.area != "Layout" && ps.is_scale_to_fit();
let target_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
} else {
@ -4116,11 +4278,15 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
d.scale_lw = ps.flags.scale_lineweights && !d.fit_to_paper;
d.lineweights = ps.flags.print_lineweights;
d.paperspace_last = ps.flags.draw_viewports_first;
d.shade = match ps.shade_plot_mode {
ShadePlotMode::Wireframe => "2D Wireframe",
ShadePlotMode::Hidden => "Hidden Line",
ShadePlotMode::Rendered => "Gouraud Shaded",
ShadePlotMode::AsDisplayed => "As displayed",
d.shade = if ps.flags.plot_hidden {
"Hidden Line"
} else {
match ps.shade_plot_mode {
ShadePlotMode::Wireframe => "2D Wireframe",
ShadePlotMode::Hidden => "Hidden Line",
ShadePlotMode::Rendered => "Gouraud Shaded",
ShadePlotMode::AsDisplayed => "As displayed",
}
}
.into();
d.quality = match ps.shade_plot_resolution {
@ -4139,6 +4305,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
d.printer = (!ps.printer_name.is_empty()).then(|| ps.printer_name.clone());
}
d.style_name = style_name;
d.apply_plot_styles = ps.flags.plot_plot_styles;
d.show_plot_styles = ps.flags.show_plot_styles;
d.style_missing = !d.style_name.is_empty() && !style_loaded;
}
@ -4164,10 +4332,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
}
fn apply_dialog_to_layout(&mut self) {
use crate::io::paper_sizes::sheet_mm;
self.sync_dialog_plot_runtime();
let d = self.plot_dialog.clone();
let (sheet_w, sheet_h) = sheet_mm(self.plot_format, self.plot_orientation);
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);
@ -4185,7 +4352,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
/// Open a preview PDF, export a PDF, or send the job to the chosen printer.
fn on_plot_dlg_commit(&mut self, preview: bool) -> Task<Message> {
let d = self.plot_dialog.clone();
if d.style_missing {
if d.style_missing && d.apply_plot_styles {
self.command_line.push_error(crate::tf!(
"Plot style table '{}' is not loaded.",
d.style_name
@ -4203,11 +4370,16 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
let plot_style = self.dialog_plot_style(&d);
// Extents, Window and Display use one plot path in both spaces. Only
// Paper-space Layout is special: it uses the physical sheet bounds.
if matches!(d.area.as_str(), "Extents" | "Window" | "Display") {
if d.area != "Layout" {
let job = match d.area.as_str() {
"Display" => self.display_plot_job(),
"Extents" => self.extents_plot_job(),
_ => self.window_plot_job(),
"Limits" => self.limits_plot_job(),
"Window" => self.window_plot_job(),
area if area.starts_with("View: ") => {
self.named_view_plot_job(area.trim_start_matches("View: "))
}
_ => None,
};
let Some((
w_wires,
@ -4341,7 +4513,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
&self,
d: &crate::ui::window::plot::PlotDialogState,
) -> Option<crate::io::plot_style::PlotStyleTable> {
if d.style_name.is_empty() || d.style_missing {
if d.style_name.is_empty() || d.style_missing || !d.apply_plot_styles {
return None;
}
self.active_plot_style
@ -4358,6 +4530,34 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
self.area_plot_job(self.display_plot_window()?)
}
fn limits_plot_job(&self) -> Option<ClippedPlotParams> {
let (min, max) = self.tabs[self.active_tab]
.scene
.current_drawing_limits()?;
self.area_plot_job((min.x, min.y, max.x, max.y))
}
fn named_view_plot_job(&self, name: &str) -> Option<ClippedPlotParams> {
let view = self.tabs[self.active_tab]
.scene
.document
.views
.iter()
.find(|view| {
view.name.eq_ignore_ascii_case(name)
&& view.paper_space == (self.tabs[self.active_tab].scene.current_layout != "Model")
})?;
let half_w = view.width.abs() * 0.5;
let half_h = view.height.abs() * 0.5;
(half_w > 1e-9 && half_h > 1e-9).then_some(())?;
self.area_plot_job((
view.center.x - half_w,
view.center.y - half_h,
view.center.x + half_w,
view.center.y + half_h,
))
}
fn extents_plot_job(&self) -> Option<ClippedPlotParams> {
let scene = &self.tabs[self.active_tab].scene;
if scene.current_layout == "Model" {
@ -4446,13 +4646,13 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
/// Render a selected rectangle through one shared Model/Paper path. The
/// window may lie partly or wholly outside a paper sheet.
fn area_plot_job(&self, window: (f64, f64, f64, f64)) -> Option<ClippedPlotParams> {
use crate::io::paper_sizes::{sheet_mm, window_to_sheet, PlotScale};
use crate::io::paper_sizes::{window_to_sheet, PlotScale};
let i = self.active_tab;
let (x0, y0, x1, y1) = window;
if (x1 - x0) < 1e-6 || (y1 - y0) < 1e-6 {
return None;
}
let (sheet_w, sheet_h) = sheet_mm(self.plot_format, self.plot_orientation);
let (sheet_w, sheet_h) = plot_dialog_sheet_mm(&self.plot_dialog);
let win_w = (x1 - x0).max(1e-9);
let win_h = (y1 - y0).max(1e-9);
let scale_sel = if self.plot_dialog.fit_to_paper {

View file

@ -151,6 +151,9 @@ impl OpenCADStudio {
if let Some(previous) = self.print_all_plot_window_prev.take() {
self.plot_window = previous;
}
if let Some(previous) = self.print_all_plot_setup_prev.take() {
self.plot_setup_template = previous;
}
self.print_all_options = false;
self.active_modal = Some(PrintAll);
self.reset_modal_geometry();
@ -6917,6 +6920,9 @@ impl OpenCADStudio {
self.plot_dialog.style_missing = false;
self.plot_dialog.plot_styles =
crate::io::plot_style::available_ctb_names();
self.tabs[self.active_tab]
.scene
.invalidate_display_plot_style();
self.command_line.push_output(
crate::tf!(
@ -6968,6 +6974,9 @@ impl OpenCADStudio {
self.plot_dialog.style_missing = false;
self.plot_dialog.plot_styles =
crate::io::plot_style::available_ctb_names();
self.tabs[self.active_tab]
.scene
.invalidate_display_plot_style();
self.command_line.push_output(crate::tf!(
"Plot style table saved to \"{}\".",
path.display()

View file

@ -62,8 +62,7 @@ pub(super) fn layout_entry_name(s: &str) -> &str {
s.trim_start_matches('*').trim_end_matches('*')
}
/// Infer the closest A-series paper label and orientation from sheet
/// dimensions (mm). Falls back to A4 when nothing is close.
/// Infer an A-series label when dimensions match; otherwise retain the size.
pub(super) fn paper_label_from_dims(w: f64, h: f64) -> (String, String) {
use crate::io::paper_sizes::PaperSize;
let orient = if w >= h { "Landscape" } else { "Portrait" };
@ -76,7 +75,12 @@ pub(super) fn paper_label_from_dims(w: f64, h: f64) -> (String, String) {
best = (p.label().to_string(), err);
}
}
(best.0, orient.to_string())
let label = if best.1 <= 2.0 {
best.0
} else {
format!("{w:.2} × {h:.2} mm")
};
(label, orient.to_string())
}
pub(super) fn parse_plot_scale(s: &str) -> (f64, f64) {

View file

@ -5293,8 +5293,7 @@ impl OpenCADStudio {
self.tabs[i].scene.set_current_layout(name.clone());
self.tabs[i].scene.deselect_all();
let switch_ms = perf_phase.elapsed().as_secs_f64() * 1000.0;
// UCS follows the pane: model header UCS in the Model tab, none
// in plain paper space (a viewport's UCS is adopted on entry).
// UCS follows the active model, layout, or floating viewport pane.
let perf_phase = Instant::now();
self.tabs[i].refresh_active_ucs();
self.tabs[i].scene.restore_saved_camera();
@ -5358,10 +5357,28 @@ impl OpenCADStudio {
self.push_undo_snapshot(i, "LAYOUT");
match self.tabs[i].scene.document.add_layout(&new_name) {
Ok(_) => {
// Override the acadrust default limits (12×9 imperial) with A4 landscape.
let layout_flags = i16::from(
self.tabs[i]
.scene
.document
.header
.paper_space_linetype_scaling,
) | (i16::from(
self.tabs[i]
.scene
.document
.header
.paper_space_limit_check,
) << 1);
let plot_style = self
.active_plot_style
.as_ref()
.map(|style| style.name.clone())
.unwrap_or_default();
for obj in self.tabs[i].scene.document.objects.values_mut() {
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);
@ -5371,6 +5388,14 @@ impl OpenCADStudio {
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();
break;
}

View file

@ -25,6 +25,7 @@ use std::io::Write;
use std::path::Path;
#[derive(Clone, Debug)]
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub struct PlotWire {
pub wire: WireModel,
pub draw_depth: f32,
@ -120,6 +121,7 @@ pub struct PdfPageInput {
pub scale: f32,
pub clip: Option<(f32, f32, f32, f32)>,
pub options: PdfPlotOptions,
pub plot_style: Option<PlotStyleTable>,
}
impl Default for PdfPlotOptions {
@ -270,7 +272,7 @@ fn build_pdf_pages(pages: &[PdfPageInput], plot_style: Option<&PlotStyleTable>)
page.rotation_deg,
page.scale,
page.clip,
plot_style,
page.plot_style.as_ref().or(plot_style),
page.options,
);
}
@ -440,6 +442,8 @@ fn append_pdf_page(
let mut last_color: Option<[f32; 3]> = None;
let mut last_lw: Option<f32> = None;
let mut last_cap = Some(LineCapStyle::Round);
let mut last_join = Some(LineJoinStyle::Round);
// Current PDF dash array (empty = solid). Tracked so the dash op is only
// re-emitted when it actually changes between wires.
let mut last_dash: Option<Vec<i64>> = None;
@ -453,7 +457,9 @@ fn append_pdf_page(
ox,
oy,
plot_style,
scale,
options,
normal_blend.as_ref(),
);
last_color = None;
last_lw = None;
@ -506,6 +512,8 @@ fn append_pdf_page(
let mut lw_override: Option<f32> = None;
let mut screening = 1.0;
let mut color_overridden = false;
let mut cap = None;
let mut join = None;
if let Some(ctb) = plot_style {
if wire.aci > 0 {
if let Some([cr, cg, cb]) = ctb.resolve_color(wire.aci) {
@ -518,6 +526,20 @@ fn append_pdf_page(
.resolve_lineweight(wire.aci)
.map(|mm| (mm * MM_TO_PT).max(0.1));
screening = ctb.resolve_screening(wire.aci);
if let Some(entry) = ctb.aci_entries.get(wire.aci as usize) {
cap = match entry.end_style {
0 => Some(LineCapStyle::Butt),
1 | 3 => Some(LineCapStyle::ProjectingSquare),
2 => Some(LineCapStyle::Round),
_ => None,
};
join = match entry.join_style {
0 => Some(LineJoinStyle::Miter),
1 | 3 => Some(LineJoinStyle::Bevel),
2 => Some(LineJoinStyle::Round),
_ => None,
};
}
}
}
// Near-white and near-yellow (viewport active border) → dark grey for print
@ -539,6 +561,17 @@ fn append_pdf_page(
}
[r, g, b] = plotted_color([r, g, b], a, screening, options);
let cap = cap.unwrap_or(LineCapStyle::Round);
if last_cap != Some(cap) {
ops.push(Op::SetLineCapStyle { cap });
last_cap = Some(cap);
}
let join = join.unwrap_or(LineJoinStyle::Round);
if last_join != Some(join) {
ops.push(Op::SetLineJoinStyle { join });
last_join = Some(join);
}
if last_color
.map(|c| (c[0] - r).abs() > 0.01 || (c[1] - g).abs() > 0.01 || (c[2] - b).abs() > 0.01)
.unwrap_or(true)
@ -576,13 +609,11 @@ fn append_pdf_page(
let lw_pt = if wire.world_width > 0.0 {
wire.world_width * MM_TO_PT
} else {
let physical = lw_override.unwrap_or_else(|| {
if options.object_lineweights {
(wire.line_weight_px * LW_PX_TO_PT).max(0.1)
} else {
0.1
}
});
let physical = if options.object_lineweights {
lw_override.unwrap_or_else(|| (wire.line_weight_px * LW_PX_TO_PT).max(0.1))
} else {
0.1
};
physical / pen_divisor
};
if last_lw.map(|l| (l - lw_pt).abs() > 0.01).unwrap_or(true) {
@ -852,12 +883,71 @@ fn emit_wire_fills(
ox: f64,
oy: f64,
plot_style: Option<&PlotStyleTable>,
scale: f32,
options: PdfPlotOptions,
normal_blend: Option<&ExtendedGraphicsStateId>,
) {
for wire in wires {
if wire.fill_tris.is_empty() {
continue;
}
let styled_pattern = plot_style.and_then(|table| {
(wire.aci > 0)
.then(|| table.aci_entries.get(wire.aci as usize))
.flatten()
.and_then(|entry| {
(65..=72)
.contains(&entry.fill_style)
.then(|| {
crate::scene::model::hatch_model::plot_style_fill_pattern(
entry.fill_style,
)
})
.flatten()
})
});
if let Some(pattern) = styled_pattern {
for (triangle_index, triangle) in wire.fill_tris.chunks_exact(3).enumerate() {
let mut boundary = Vec::with_capacity(4);
for (point_index, point) in triangle.iter().enumerate() {
let index = triangle_index * 3 + point_index;
let low = wire.fill_tris_low.get(index).copied().unwrap_or([0.0; 3]);
boundary.push([point[0] + low[0], point[1] + low[1]]);
}
boundary.push(boundary[0]);
let hatch = HatchModel {
render_instance: wire.render_instance.clone(),
world_origin: [0.0, 0.0],
boundary: std::sync::Arc::new(boundary),
boundary_wcs: None,
fill_plane: None,
fill_plane_boundary: None,
boundary_exterior: None,
boundary_sources: None,
boundary_paths: None,
style: acadrust::entities::HatchStyleType::Normal,
pattern: pattern.clone(),
name: "PLOTSTYLE".to_string(),
color: wire.color,
aci: wire.aci,
line_weight_px: wire.line_weight_px,
angle_offset: 0.0,
scale: 1.0 / scale.max(1.0e-6),
draw_depth: wire.depth_override.unwrap_or(0.0),
};
emit_hatch(
ops,
&hatch,
ox,
oy,
plot_style,
scale,
options,
normal_blend,
);
}
continue;
}
let [mut r, mut g, mut b, a] = wire.color;
if a < 0.01 {
continue;
@ -964,6 +1054,26 @@ fn emit_hatch(
if hatch.boundary.is_empty() {
return;
}
let mut styled_hatch = None;
if let Some(table) = plot_style {
if hatch.aci > 0 && matches!(hatch.pattern, HatchPattern::Solid) {
if let Some(pattern) = table
.aci_entries
.get(hatch.aci as usize)
.and_then(|entry| {
crate::scene::model::hatch_model::plot_style_fill_pattern(
entry.fill_style,
)
})
{
let mut model = hatch.clone();
model.pattern = pattern;
model.scale = 1.0 / scale.max(1.0e-6);
styled_hatch = Some(model);
}
}
}
let hatch = styled_hatch.as_ref().unwrap_or(hatch);
let [mut r, mut g, mut b, a] = hatch.color;
if a < 0.01 {
return;
@ -1082,13 +1192,11 @@ fn emit_hatch(
// Pattern hatches: rasterise the family lines clipped to the boundary
// and emit each as a stroked line. Skips the polygon outline entirely.
if matches!(hatch.pattern, HatchPattern::Pattern(_)) {
let physical = lw_override.unwrap_or_else(|| {
if options.object_lineweights {
(hatch.line_weight_px * LW_PX_TO_PT).max(0.1)
} else {
0.1
}
});
let physical = if options.object_lineweights {
lw_override.unwrap_or_else(|| (hatch.line_weight_px * LW_PX_TO_PT).max(0.1))
} else {
0.1
};
let divisor = if options.scale_lineweights {
1.0
} else {
@ -1248,16 +1356,16 @@ fn emit_text(
if let Some(ctb) = plot_style {
if wire.aci > 0 {
ctb_color = ctb.resolve_color(wire.aci);
lw_override = ctb
.resolve_lineweight(wire.aci)
.map(|mm| {
lw_override = options.object_lineweights.then(|| {
ctb.resolve_lineweight(wire.aci).map(|mm| {
let divisor = if options.scale_lineweights {
1.0
} else {
scale.max(1e-6)
};
(mm * MM_TO_PT).max(0.1) / divisor
});
})
}).flatten();
screening = ctb.resolve_screening(wire.aci);
}
}

View file

@ -1,7 +1,7 @@
//! Plot Style Table — CTB (color-based) and STB (named) file support.
//!
//! CTB files map indexed drawing colors (ACI, 1-255) to pen properties:
//! RGB color override, lineweight, and screeing percentage.
//! RGB color override, lineweight, and screening percentage.
//!
//! File format: a fixed 60-byte header followed by zlib-compressed text.
//!
@ -17,6 +17,21 @@ use std::path::PathBuf;
pub const DEFAULT_PLOT_STYLE: &str = "ocad.ctb";
pub const MONOCHROME_PLOT_STYLE: &str = "monochrome.ctb";
pub const GRAYSCALE_PLOT_STYLE: &str = "Grayscale.ctb";
pub const FILL_PATTERNS_PLOT_STYLE: &str = "Fill Patterns.ctb";
pub const SCREENING_100_PLOT_STYLE: &str = "Screening 100%.ctb";
pub const SCREENING_75_PLOT_STYLE: &str = "Screening 75%.ctb";
pub const SCREENING_50_PLOT_STYLE: &str = "Screening 50%.ctb";
pub const SCREENING_25_PLOT_STYLE: &str = "Screening 25%.ctb";
const STANDARD_PLOT_STYLES: &[&str] = &[
GRAYSCALE_PLOT_STYLE,
FILL_PATTERNS_PLOT_STYLE,
SCREENING_100_PLOT_STYLE,
SCREENING_75_PLOT_STYLE,
SCREENING_50_PLOT_STYLE,
SCREENING_25_PLOT_STYLE,
];
#[cfg(not(target_arch = "wasm32"))]
pub fn plot_styles_dir() -> Result<PathBuf, String> {
@ -52,10 +67,14 @@ pub fn available_ctb_names() -> Vec<String> {
#[cfg(not(target_arch = "wasm32"))]
{
let Ok(dir) = ensure_plot_styles_dir() else {
return vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()];
let mut names = vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()];
names.extend(STANDARD_PLOT_STYLES.iter().map(|name| (*name).to_string()));
return names;
};
let Ok(entries) = std::fs::read_dir(dir) else {
return vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()];
let mut names = vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()];
names.extend(STANDARD_PLOT_STYLES.iter().map(|name| (*name).to_string()));
return names;
};
let mut names: Vec<String> = entries
.filter_map(Result::ok)
@ -67,13 +86,16 @@ pub fn available_ctb_names() -> Vec<String> {
.then(|| entry.file_name().to_string_lossy().into_owned())
})
.collect();
names.extend(STANDARD_PLOT_STYLES.iter().map(|name| (*name).to_string()));
names.sort_by_key(|name| name.to_ascii_lowercase());
names.dedup_by(|left, right| left.eq_ignore_ascii_case(right));
names
}
#[cfg(target_arch = "wasm32")]
{
vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()]
let mut names = vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()];
names.extend(STANDARD_PLOT_STYLES.iter().map(|name| (*name).to_string()));
names
}
}
@ -199,10 +221,44 @@ impl PlotStyleTable {
MONOCHROME_PLOT_STYLE,
include_bytes!("../../assets/plotstyles/monochrome.ctb"),
),
"grayscale.ctb" => {
let mut table = Self::identity(GRAYSCALE_PLOT_STYLE);
for aci in 1..=255u8 {
if let Some((r, g, b)) = acadrust::types::aci_to_rgb(aci) {
let gray = if aci == 7 {
0
} else {
(0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32)
.round() as u8
};
table.aci_entries[aci as usize].color = Some([gray, gray, gray]);
}
}
Ok(table)
}
"fill patterns.ctb" => {
let mut table = Self::identity(FILL_PATTERNS_PLOT_STYLE);
for aci in 1..=9usize {
table.aci_entries[aci].fill_style = 63 + aci as u8;
}
Ok(table)
}
"screening 100%.ctb" => Ok(Self::screening(SCREENING_100_PLOT_STYLE, 100)),
"screening 75%.ctb" => Ok(Self::screening(SCREENING_75_PLOT_STYLE, 75)),
"screening 50%.ctb" => Ok(Self::screening(SCREENING_50_PLOT_STYLE, 50)),
"screening 25%.ctb" => Ok(Self::screening(SCREENING_25_PLOT_STYLE, 25)),
_ => Err(format!("Unknown built-in plot style: {name}")),
}
}
fn screening(name: &str, percent: u8) -> Self {
let mut table = Self::identity(name);
for entry in table.aci_entries.iter_mut().skip(1) {
entry.screening = percent;
}
table
}
/// Load one CTB by file name from the per-user plot styles folder.
pub fn load_named(name: &str) -> Result<Self, String> {
let path = Path::new(name);
@ -227,9 +283,11 @@ impl PlotStyleTable {
.to_string_lossy()
.eq_ignore_ascii_case(name)
})
.map(|entry| entry.path())
.ok_or_else(|| format!("Plot style not found: {name}"))?;
return Self::load(&matched);
.map(|entry| entry.path());
return match matched {
Some(path) => Self::load(&path),
None => Self::builtin(name),
};
}
#[cfg(target_arch = "wasm32")]
@ -248,9 +306,23 @@ impl PlotStyleTable {
/// Returns None if no override (use object color).
pub fn resolve_color(&self, aci: u8) -> Option<[f32; 3]> {
let entry = self.aci_entries.get(aci as usize)?;
entry
let mut color = entry
.color
.map(|[r, g, b]| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0])
.map(|[r, g, b]| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0]);
if entry.color_policy & 2 != 0 {
let rgb = color.or_else(|| {
if aci == 7 {
Some([0.0; 3])
} else {
acadrust::types::aci_to_rgb(aci).map(|(r, g, b)| {
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0]
})
}
})?;
let gray = 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2];
color = Some([gray; 3]);
}
color
}
/// Resolve the effective lineweight in mm for the given ACI index.
@ -260,7 +332,13 @@ impl PlotStyleTable {
if matches!(entry.lineweight, 0 | 255) {
None
} else {
self.lineweights.get(entry.lineweight as usize).copied()
self.lineweights.get(entry.lineweight as usize).copied().map(|weight| {
if self.apply_factor {
weight * self.scale_factor.max(0.0)
} else {
weight
}
})
}
}

View file

@ -29,6 +29,11 @@ pub(crate) fn temp_pdf_path(kind: &str) -> std::path::PathBuf {
))
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn temp_pdf_path(kind: &str) -> std::path::PathBuf {
std::path::PathBuf::from(format!("{kind}.pdf"))
}
/// Extra options for a print job. On CUPS (Linux/macOS) these map to `lp`
/// flags / `-o` options. On Windows the generated PDF already carries render
/// options. Windows queues repeated jobs when more than one copy is requested;

View file

@ -72,8 +72,7 @@ impl Scene {
// Remove the Layout object itself.
self.document.objects.remove(&layout_handle);
// Drop the layout's entry from the ACAD_LAYOUT dictionary so it does not
// dangle (and so AutoCAD doesn't try to recover a now-missing layout).
// Drop the layout's dictionary entry so it does not dangle.
let dict_handle = self.document.header.acad_layout_dict_handle;
if let Some(ObjectType::Dictionary(d)) = self.document.objects.get_mut(&dict_handle) {
d.entries.retain(|(k, _)| k != name);
@ -90,20 +89,6 @@ impl Scene {
self.document.block_records.remove(&bn);
}
// Drop any standalone PlotSettings page setup tied to this layout.
let ps_handles: Vec<Handle> = self
.document
.objects
.iter()
.filter_map(|(h, o)| match o {
ObjectType::PlotSettings(ps) if ps.page_name == name => Some(*h),
_ => None,
})
.collect();
for h in ps_handles {
self.document.objects.remove(&h);
}
// If the deleted layout was active, fall back to Model space.
if self.current_layout == name {
self.current_layout = "Model".to_string();
@ -498,9 +483,7 @@ impl Scene {
/// Reconstruct a `pane_grid` configuration from a set of (tile-index, rect)
/// items covering `region`, by recursively finding a full vertical or
/// horizontal guillotine cut. AutoCAD tiled configs (and anything pane_grid
/// produces) are guillotine layouts, so this round-trips them. A non-guillotine
/// set falls back to chaining panes so nothing is lost.
/// horizontal guillotine cut. Non-guillotine input falls back to chained panes.
fn config_from_rects(
items: &[(usize, iced::Rectangle)],
region: iced::Rectangle,

View file

@ -91,6 +91,7 @@ impl Scene {
self.document.header.limit_check = enabled;
} else {
self.document.header.paper_space_limit_check = enabled;
self.persist_current_layout_state();
}
}

View file

@ -1614,6 +1614,15 @@ pub struct Scene {
/// movement scanned the whole document for wipeouts and recreated the same
/// Arcs, making Paper frame construction CPU-bound on large drawings.
paper_sheet_render_cache: RefCell<HashMap<String, PaperSheetRenderCache>>,
display_plot_style_cache:
RefCell<HashMap<(String, String), Option<Arc<crate::io::plot_style::PlotStyleTable>>>>,
styled_wire_cache: RefCell<
HashMap<(u64, String), (u64, Arc<Vec<WireModel>>)>,
>,
styled_hatch_cache:
RefCell<HashMap<(u64, usize, usize, String, u32), Arc<Vec<HatchModel>>>>,
styled_wire_fill_cache:
RefCell<HashMap<(u64, String, u32), Arc<Vec<HatchModel>>>>,
/// Per-viewport projected wire cache for paper-space content viewports.
/// Stores projected + clipped wires in paper-space coordinates.
/// Maps vp_handle → (geometry_epoch, Vec<WireModel>).
@ -1880,6 +1889,10 @@ impl Scene {
paper_sheet_cache: RefCell::new(HashMap::default()),
paper_viewport_cache: RefCell::new(HashMap::default()),
paper_sheet_render_cache: RefCell::new(HashMap::default()),
display_plot_style_cache: RefCell::new(HashMap::default()),
styled_wire_cache: RefCell::new(HashMap::default()),
styled_hatch_cache: RefCell::new(HashMap::default()),
styled_wire_fill_cache: RefCell::new(HashMap::default()),
paper_projected_cache: RefCell::new(HashMap::default()),
current_layout: "Model".to_string(),
block_edit_block: None,
@ -2878,7 +2891,9 @@ impl Scene {
/// active layout or its background are dropped.
pub fn set_current_layout(&mut self, name: String) {
if self.current_layout != name {
self.persist_current_layout_state();
self.current_layout = name;
self.load_current_layout_state();
self.sync_active_space_to_document();
self.recolor_meshes();
*self.wire_cache.borrow_mut() = None;
@ -2888,6 +2903,91 @@ impl Scene {
}
}
pub fn load_current_layout_state(&mut self) {
self.display_plot_style_cache
.borrow_mut()
.retain(|(layout, _), _| layout != &self.current_layout);
self.styled_wire_cache.borrow_mut().clear();
self.styled_hatch_cache.borrow_mut().clear();
self.styled_wire_fill_cache.borrow_mut().clear();
if self.current_layout == "Model" {
return;
}
if let Some((flags, insertion_base, min_extents, max_extents, min_limits, max_limits)) =
self.document.objects.values().find_map(|object| {
let ObjectType::Layout(layout) = object else {
return None;
};
(layout.name == self.current_layout).then_some((
layout.flags,
layout.insertion_base,
layout.min_extents,
layout.max_extents,
layout.min_limits,
layout.max_limits,
))
}) {
self.document.header.paper_space_linetype_scaling = flags & 1 != 0;
self.document.header.paper_space_limit_check = flags & 2 != 0;
self.document.header.paper_space_insertion_base = acadrust::types::Vector3::new(
insertion_base.0,
insertion_base.1,
insertion_base.2,
);
self.document.header.paper_space_extents_min = acadrust::types::Vector3::new(
min_extents.0,
min_extents.1,
min_extents.2,
);
self.document.header.paper_space_extents_max = acadrust::types::Vector3::new(
max_extents.0,
max_extents.1,
max_extents.2,
);
self.document.header.paper_space_limits_min =
acadrust::types::Vector2::new(min_limits.0, min_limits.1);
self.document.header.paper_space_limits_max =
acadrust::types::Vector2::new(max_limits.0, max_limits.1);
}
}
pub fn persist_current_layout_state(&mut self) {
if self.current_layout == "Model" {
return;
}
let header = &self.document.header;
let psltscale = header.paper_space_linetype_scaling;
let plimcheck = header.paper_space_limit_check;
let insertion_base = header.paper_space_insertion_base;
let min_extents = header.paper_space_extents_min;
let max_extents = header.paper_space_extents_max;
let min_limits = header.paper_space_limits_min;
let max_limits = header.paper_space_limits_max;
for object in self.document.objects.values_mut() {
let ObjectType::Layout(layout) = object else {
continue;
};
if layout.name == self.current_layout {
layout.flags = if psltscale {
layout.flags | 1
} else {
layout.flags & !1
};
layout.flags = if plimcheck {
layout.flags | 2
} else {
layout.flags & !2
};
layout.insertion_base = (insertion_base.x, insertion_base.y, insertion_base.z);
layout.min_extents = (min_extents.x, min_extents.y, min_extents.z);
layout.max_extents = (max_extents.x, max_extents.y, max_extents.z);
layout.min_limits = (min_limits.x, min_limits.y);
layout.max_limits = (max_limits.x, max_limits.y);
break;
}
}
}
/// Mirror the active space (Model tile-mode vs a paper layout) into the
/// document's persisted settings so it round-trips on save: the `$TILEMODE`
/// header (`show_model_space`) and the `CTAB` current-tab variable. The
@ -3305,32 +3405,17 @@ impl Scene {
Some(wire)
}
/// The effective plot settings for the current layout: a standalone
/// PlotSettings page setup if one exists, otherwise the settings embedded in
/// the LAYOUT object (paper size, margins, origin, rotation, scale). Loaded
/// The fallback preserves rotation, origin and scale from loaded files.
/// The effective plot settings embedded in the current layout.
pub fn effective_plot_settings(&self) -> Option<acadrust::objects::PlotSettings> {
self.plot_settings_for(&self.current_layout)
}
/// Plot settings for a specific layout by name: its standalone
/// `PlotSettings` object if one exists, else synthesized from the `Layout`
/// object's embedded fields.
/// Plot settings embedded in a specific layout.
pub fn plot_settings_for(&self, name: &str) -> Option<acadrust::objects::PlotSettings> {
use acadrust::objects::{
ObjectType, PaperMargin, PlotPaperUnits, PlotRotation, PlotSettings, PlotType,
PlotWindow, ScaledType,
PlotWindow, ScaledType, ShadePlotMode, ShadePlotResolutionLevel,
};
if let Some(ps) = self.document.objects.values().find_map(|o| {
if let ObjectType::PlotSettings(ps) = o {
if ps.page_name.as_str() == name {
return Some(ps.clone());
}
}
None
}) {
return Some(ps);
}
self.document.objects.values().find_map(|o| {
let ObjectType::Layout(l) = o else {
return None;
@ -3338,10 +3423,13 @@ impl Scene {
if l.name.as_str() != name {
return None;
}
let mut ps = PlotSettings::new(l.name.clone());
let mut ps = PlotSettings::new(l.plot_page_name.clone());
ps.printer_name = l.plot_printer_name.clone();
ps.paper_width = l.paper_width;
ps.paper_height = l.paper_height;
ps.paper_size = l.paper_size.clone();
ps.plot_view_name = l.plot_view_name.clone();
ps.current_style_sheet = l.plot_style_sheet.clone();
ps.margins = PaperMargin::new(
l.plot_margin_left,
l.plot_margin_bottom,
@ -3362,10 +3450,84 @@ impl Scene {
ps.scale_type = ScaledType::from_code(l.plot_scale_type);
ps.scale_numerator = l.plot_scale_numerator;
ps.scale_denominator = l.plot_scale_denominator;
ps.flags = l.plot_flags;
ps.standard_scale_factor = l.plot_scale_factor;
ps.paper_image_origin_x = l.paper_image_origin_x;
ps.paper_image_origin_y = l.paper_image_origin_y;
ps.shade_plot_mode = ShadePlotMode::from_code(l.shade_plot_mode);
ps.shade_plot_resolution =
ShadePlotResolutionLevel::from_code(l.shade_plot_resolution);
ps.shade_plot_dpi = l.shade_plot_dpi;
ps.plot_view_handle = l.plot_view_handle;
ps.visual_style_handle = l.visual_style_handle;
Some(ps)
})
}
/// Replace the plot-settings portion embedded in one layout.
pub fn set_layout_plot_settings(
&mut self,
name: &str,
ps: &acadrust::objects::PlotSettings,
) -> bool {
let Some(layout) = self.document.objects.values_mut().find_map(|object| {
let ObjectType::Layout(layout) = object else {
return None;
};
(layout.name == name).then_some(layout)
}) else {
return false;
};
layout.plot_page_name = ps.page_name.clone();
layout.plot_printer_name = ps.printer_name.clone();
layout.paper_size = ps.paper_size.clone();
layout.plot_view_name = ps.plot_view_name.clone();
layout.plot_style_sheet = ps.current_style_sheet.clone();
layout.plot_margin_left = ps.margins.left;
layout.plot_margin_bottom = ps.margins.bottom;
layout.plot_margin_right = ps.margins.right;
layout.plot_margin_top = ps.margins.top;
layout.paper_width = ps.paper_width;
layout.paper_height = ps.paper_height;
layout.plot_origin_x = ps.origin_x;
layout.plot_origin_y = ps.origin_y;
layout.plot_window_min_x = ps.plot_window.lower_left_x;
layout.plot_window_min_y = ps.plot_window.lower_left_y;
layout.plot_window_max_x = ps.plot_window.upper_right_x;
layout.plot_window_max_y = ps.plot_window.upper_right_y;
layout.plot_scale_numerator = ps.scale_numerator;
layout.plot_scale_denominator = ps.scale_denominator;
layout.plot_paper_units = ps.paper_units.to_code();
layout.plot_rotation = ps.rotation.to_code();
layout.plot_type = ps.plot_type.to_code();
layout.plot_scale_type = ps.scale_type.to_code();
layout.shade_plot_mode = ps.shade_plot_mode.to_code();
layout.shade_plot_resolution = ps.shade_plot_resolution.to_code();
layout.shade_plot_dpi = ps.shade_plot_dpi;
layout.plot_flags = ps.flags;
layout.plot_scale_factor = ps.standard_scale_factor;
layout.paper_image_origin_x = ps.paper_image_origin_x;
layout.paper_image_origin_y = ps.paper_image_origin_y;
layout.plot_view_handle = ps.plot_view_handle;
layout.visual_style_handle = ps.visual_style_handle;
layout.raw_plot_settings_codes = None;
self.display_plot_style_cache
.borrow_mut()
.retain(|(layout_name, _), _| layout_name != name);
self.styled_wire_cache.borrow_mut().clear();
self.styled_hatch_cache.borrow_mut().clear();
self.styled_wire_fill_cache.borrow_mut().clear();
true
}
pub fn invalidate_display_plot_style(&self) {
self.display_plot_style_cache.borrow_mut().clear();
self.styled_wire_cache.borrow_mut().clear();
self.styled_hatch_cache.borrow_mut().clear();
self.styled_wire_fill_cache.borrow_mut().clear();
}
/// PlotSettings store the paper size and plot margins in millimetres, but a
/// layout's paper space is laid out in its own units — its viewports,
/// drawing limits and the sheet the user sees can be in inches, millimetres,

View file

@ -139,6 +139,39 @@ impl GradientKind {
}
}
pub(crate) fn plot_style_fill_pattern(style: u8) -> Option<HatchPattern> {
let family = |angle_deg, dx, dy, dashes: &[f32]| PatFamily {
angle_deg,
x0: 0.0,
y0: 0.0,
dx,
dy,
dashes: dashes.to_vec(),
};
let pattern = match style {
64 => HatchPattern::Solid,
65 => HatchPattern::Pattern(vec![
family(0.0, 0.0, 2.0, &[2.0, -2.0]),
family(90.0, 2.0, 0.0, &[2.0, -2.0]),
]),
66 => HatchPattern::Pattern(vec![
family(0.0, 0.0, 2.0, &[]),
family(90.0, 2.0, 0.0, &[]),
]),
67 => HatchPattern::Pattern(vec![
family(45.0, 0.0, 2.0, &[]),
family(135.0, 0.0, 2.0, &[]),
]),
68 => HatchPattern::Pattern(vec![family(0.0, 0.0, 2.0, &[])]),
69 => HatchPattern::Pattern(vec![family(135.0, 0.0, 2.0, &[])]),
70 => HatchPattern::Pattern(vec![family(45.0, 0.0, 2.0, &[])]),
71 => HatchPattern::Pattern(vec![family(0.0, 0.0, 2.0, &[0.0, -2.0])]),
72 => HatchPattern::Pattern(vec![family(90.0, 2.0, 0.0, &[])]),
_ => return None,
};
Some(pattern)
}
/// Hatch fill pattern.
#[derive(Clone, Debug)]
pub enum HatchPattern {

View file

@ -22,6 +22,8 @@ use crate::scene::{
SceneLight, Uniforms, ViewportInstance, WireModel,
};
const DISPLAY_STYLE_CACHE_LIMIT: usize = if cfg!(target_arch = "wasm32") { 4 } else { 24 };
// ── Camera hover state (shader::Program::State) ───────────────────────────
#[derive(Clone, Default)]
@ -2496,6 +2498,238 @@ impl Scene {
};
(final_color, pl, pat, lw, aci)
}
fn display_plot_style(&self) -> Option<Arc<crate::io::plot_style::PlotStyleTable>> {
if self.current_layout == "Model" {
return None;
}
let settings = self.effective_plot_settings()?;
if !settings.flags.show_plot_styles || settings.current_style_sheet.is_empty() {
return None;
}
let key = (
self.current_layout.clone(),
settings.current_style_sheet.to_ascii_lowercase(),
);
if let Some(style) = self.display_plot_style_cache.borrow().get(&key) {
return style.clone();
}
let style = crate::io::plot_style::PlotStyleTable::load_named(
&settings.current_style_sheet,
)
.ok()
.map(Arc::new);
self.display_plot_style_cache
.borrow_mut()
.insert(key, style.clone());
style
}
fn apply_display_plot_style(
&self,
color: &mut [f32; 4],
aci: u8,
style: &crate::io::plot_style::PlotStyleTable,
) {
if aci == 0 {
return;
}
if let Some(rgb) = style.resolve_color(aci) {
color[..3].copy_from_slice(&rgb);
}
let screening = style.resolve_screening(aci);
for (channel, paper) in color[..3]
.iter_mut()
.zip(self.paper_bg_color[..3].iter())
{
*channel = *channel * screening + *paper * (1.0 - screening);
}
}
fn display_styled_wires(
&self,
source: Arc<Vec<WireModel>>,
source_gen: u64,
) -> (Arc<Vec<WireModel>>, u64) {
let Some(style) = self.display_plot_style() else {
return (source, source_gen);
};
let key = (source_gen, style.name.to_ascii_lowercase());
if let Some((gen, wires)) = self.styled_wire_cache.borrow().get(&key) {
return (Arc::clone(wires), *gen);
}
let mut wires = source.as_ref().clone();
for wire in &mut wires {
self.apply_display_plot_style(&mut wire.color, wire.aci, &style);
if wire.aci > 0 {
if wire.fill_is_2d_solid
&& style
.aci_entries
.get(wire.aci as usize)
.is_some_and(|entry| (65..=72).contains(&entry.fill_style))
{
wire.fill_tris.clear();
wire.fill_tris_low.clear();
}
if let Some(mm) = style.resolve_lineweight(wire.aci) {
wire.line_weight_px = (mm * (96.0 / 25.4) * 2.0).max(1.0);
}
for vertex in &mut wire.text_verts {
self.apply_display_plot_style(&mut vertex.color, wire.aci, &style);
}
}
}
let wires = Arc::new(wires);
let gen = crate::scene::WIRE_CONTENT_GEN
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let mut cache = self.styled_wire_cache.borrow_mut();
if cache.len() >= DISPLAY_STYLE_CACHE_LIMIT {
cache.clear();
}
cache.insert(key, (gen, Arc::clone(&wires)));
(wires, gen)
}
fn display_styled_hatches(
&self,
source: Arc<Vec<HatchModel>>,
wire_fills: Option<&Arc<Vec<HatchModel>>>,
pattern_scale: f32,
) -> Arc<Vec<HatchModel>> {
let Some(style) = self.display_plot_style() else {
return source;
};
let key = (
self.geometry_epoch,
Arc::as_ptr(&source) as usize,
wire_fills.map_or(0, |fills| Arc::as_ptr(fills) as usize),
style.name.to_ascii_lowercase(),
pattern_scale.to_bits(),
);
if let Some(hatches) = self.styled_hatch_cache.borrow().get(&key) {
return Arc::clone(hatches);
}
let mut hatches = source.as_ref().clone();
for hatch in &mut hatches {
self.apply_display_plot_style(&mut hatch.color, hatch.aci, &style);
if hatch.aci > 0 {
if matches!(hatch.pattern, crate::scene::model::hatch_model::HatchPattern::Solid) {
if let Some(fill_style) = style
.aci_entries
.get(hatch.aci as usize)
.and_then(|entry| {
crate::scene::model::hatch_model::plot_style_fill_pattern(
entry.fill_style,
)
})
{
hatch.pattern = fill_style;
hatch.scale = pattern_scale;
}
}
if let Some(mm) = style.resolve_lineweight(hatch.aci) {
hatch.line_weight_px = (mm * (96.0 / 25.4) * 2.0).max(1.0);
}
if let crate::scene::model::hatch_model::HatchPattern::Gradient {
color2, ..
} = &mut hatch.pattern
{
self.apply_display_plot_style(color2, hatch.aci, &style);
}
}
}
if let Some(wire_fills) = wire_fills {
hatches.extend(wire_fills.iter().cloned());
}
let hatches = Arc::new(hatches);
let mut cache = self.styled_hatch_cache.borrow_mut();
cache.retain(|(epoch, _, _, _, _), _| *epoch == self.geometry_epoch);
if cache.len() >= DISPLAY_STYLE_CACHE_LIMIT * 2 {
cache.clear();
}
cache.insert(key, Arc::clone(&hatches));
hatches
}
fn display_styled_wire_fills(
&self,
wires: &Arc<Vec<WireModel>>,
source_gen: u64,
pattern_scale: f32,
) -> Option<Arc<Vec<HatchModel>>> {
let Some(style) = self.display_plot_style() else {
return None;
};
let key = (
source_gen,
style.name.to_ascii_lowercase(),
pattern_scale.to_bits(),
);
if let Some(hatches) = self.styled_wire_fill_cache.borrow().get(&key) {
return Some(Arc::clone(hatches));
}
let mut hatches = Vec::new();
for wire in wires.iter().filter(|wire| wire.fill_is_2d_solid && wire.aci > 0) {
let Some(pattern) = style
.aci_entries
.get(wire.aci as usize)
.and_then(|entry| {
(65..=72)
.contains(&entry.fill_style)
.then(|| {
crate::scene::model::hatch_model::plot_style_fill_pattern(
entry.fill_style,
)
})
.flatten()
})
else {
continue;
};
let mut color = wire.color;
self.apply_display_plot_style(&mut color, wire.aci, &style);
let line_weight_px = style
.resolve_lineweight(wire.aci)
.map(|mm| (mm * (96.0 / 25.4) * 2.0).max(1.0))
.unwrap_or(wire.line_weight_px);
for (triangle_index, triangle) in wire.fill_tris.chunks_exact(3).enumerate() {
let mut boundary = Vec::with_capacity(4);
for (point_index, point) in triangle.iter().enumerate() {
let index = triangle_index * 3 + point_index;
let low = wire.fill_tris_low.get(index).copied().unwrap_or([0.0; 3]);
boundary.push([point[0] + low[0], point[1] + low[1]]);
}
boundary.push(boundary[0]);
hatches.push(HatchModel {
render_instance: wire.render_instance.clone(),
world_origin: [0.0, 0.0],
boundary: Arc::new(boundary),
boundary_wcs: None,
fill_plane: None,
fill_plane_boundary: None,
boundary_exterior: None,
boundary_sources: None,
boundary_paths: None,
style: acadrust::entities::HatchStyleType::Normal,
pattern: pattern.clone(),
name: "PLOTSTYLE".to_string(),
color,
aci: 0,
line_weight_px,
angle_offset: 0.0,
scale: pattern_scale,
draw_depth: wire.depth_override.unwrap_or(0.0),
});
}
}
let hatches = Arc::new(hatches);
let mut cache = self.styled_wire_fill_cache.borrow_mut();
if cache.len() >= DISPLAY_STYLE_CACHE_LIMIT {
cache.clear();
}
cache.insert(key, Arc::clone(&hatches));
Some(hatches)
}
}
/// Whether an entity sits on a locked layer (via the document's layer table).
@ -3403,11 +3637,15 @@ impl Scene {
} else {
self.model_wires_for_viewport_arc(inst.handle, full.height)
};
let source_gen = self.last_model_wire_gen.get();
let styled_fill_source = Arc::clone(&base_arc);
let (base_arc, styled_gen) = self.display_styled_wires(base_arc, source_gen);
self.last_model_wire_gen.set(styled_gen);
// Wire-buffer content id for the upload gate. Preview / interim wires
// are NOT part of this buffer anymore (they go in a separate per-frame
// overlay buffer below), so the base id is the source's stable content
// gen — a drag or camera move never re-uploads the base wire set.
let base_wire_content_id = self.last_model_wire_gen.get();
let base_wire_content_id = styled_gen;
let base_wire_patch = self.model_wire_patch_for(base_wire_content_id);
// Split Face3D wires from the rest. The split is content-only (keyed
// by the wire-set content id), so while the geometry is unchanged it's
@ -3548,8 +3786,15 @@ impl Scene {
width: full.width.max(1.0),
height: full.height.max(1.0),
};
let mut uniforms =
Uniforms::new(&inst.camera, full_bounds, self.document.header.lineweight_display);
let display_plot_lineweights = self.current_layout != "Model"
&& self.effective_plot_settings().is_some_and(|settings| {
settings.flags.show_plot_styles && settings.flags.print_lineweights
});
let mut uniforms = Uniforms::new(
&inst.camera,
full_bounds,
self.document.header.lineweight_display || display_plot_lineweights,
);
if self.document.header.paper_space_linetype_scaling
&& !inst.paper_sheet
&& inst.tile_idx.is_none()
@ -3642,6 +3887,29 @@ impl Scene {
None,
)
};
let viewport_scale = if inst.paper_sheet {
1.0
} else {
self.document
.get_entity(inst.handle)
.and_then(|entity| match entity {
EntityType::Viewport(viewport) => Some(vp_effective_scale(
viewport.custom_scale,
viewport.view_height,
viewport.height,
)),
_ => None,
})
.unwrap_or(1.0)
};
let pattern_scale = (self.paper_space_unit_factor() / viewport_scale.max(1.0e-9)) as f32;
let styled_wire_fills =
self.display_styled_wire_fills(&styled_fill_source, source_gen, pattern_scale);
let hatches = self.display_styled_hatches(
hatches,
styled_wire_fills.as_ref().filter(|fills| !fills.is_empty()),
pattern_scale,
);
let images = if let Some(images) = paper_images {
images
} else {

View file

@ -63,6 +63,8 @@ pub enum PlotFlag {
FitToPaper,
Center,
ScaleLw,
PlotStyles,
DisplayStyles,
UpsideDown,
Lineweights,
Transparency,
@ -132,6 +134,10 @@ pub struct PlotDialogState {
/// Output goes to a PDF file instead of a printer.
pub to_file: bool,
pub paper: String,
#[serde(skip)]
pub paper_width_mm: f64,
#[serde(skip)]
pub paper_height_mm: f64,
pub orientation: String,
pub upside_down: bool,
pub copies: String,
@ -144,6 +150,8 @@ pub struct PlotDialogState {
pub fit_to_paper: bool,
#[serde(skip)]
pub scales: Vec<(String, f64)>,
#[serde(skip)]
pub plot_views: Vec<String>,
pub scale_lw: bool,
pub quality: String,
pub shade: String,
@ -155,6 +163,8 @@ pub struct PlotDialogState {
pub stamp: bool,
/// Display name of the active plot style table ("" = none).
pub style_name: String,
pub apply_plot_styles: bool,
pub show_plot_styles: bool,
/// CTB file names discovered in the per-user plot styles folder.
#[serde(skip)]
pub plot_styles: Vec<String>,
@ -185,6 +195,8 @@ impl Default for PlotDialogState {
printer: None,
to_file: false,
paper: "A4".into(),
paper_width_mm: 297.0,
paper_height_mm: 210.0,
orientation: "Landscape".into(),
upside_down: false,
copies: "1".into(),
@ -195,6 +207,7 @@ impl Default for PlotDialogState {
scale: "1:1".into(),
fit_to_paper: true,
scales: Vec::new(),
plot_views: Vec::new(),
scale_lw: false,
quality: "Normal".into(),
shade: "As displayed".into(),
@ -205,6 +218,8 @@ impl Default for PlotDialogState {
paperspace_last: false,
stamp: false,
style_name: String::new(),
apply_plot_styles: true,
show_plot_styles: false,
plot_styles: Vec::new(),
style_missing: false,
page_setups: Vec::new(),
@ -224,6 +239,8 @@ impl PlotDialogState {
self.printer = o.printer.clone();
self.to_file = o.to_file;
self.paper = o.paper.clone();
self.paper_width_mm = o.paper_width_mm;
self.paper_height_mm = o.paper_height_mm;
self.orientation = o.orientation.clone();
self.upside_down = o.upside_down;
self.copies = o.copies.clone();
@ -243,6 +260,8 @@ impl PlotDialogState {
self.paperspace_last = o.paperspace_last;
self.stamp = o.stamp;
self.style_name = o.style_name.clone();
self.apply_plot_styles = o.apply_plot_styles;
self.show_plot_styles = o.show_plot_styles;
self.style_missing = o.style_missing;
}
@ -622,7 +641,10 @@ pub fn view_window(
None => PlotChoice::localized(OUT_DEFAULT),
})
};
let paper_opts: Vec<String> = PaperSize::ALL.iter().map(|p| p.label().to_string()).collect();
let mut paper_opts: Vec<String> = PaperSize::ALL.iter().map(|p| p.label().to_string()).collect();
if !paper_opts.iter().any(|name| name == &s.paper) {
paper_opts.push(s.paper.clone());
}
let paper_note: Element<'_, Message> = if s.area == "Layout" {
text(t!("Layout plots the current sheet using the selected paper size."))
.size(10)
@ -685,11 +707,18 @@ pub fn view_window(
let mut area_options = if print_all_options {
choices(&["Layout"])
} else {
choices(&["Extents", "Display", "Window"])
choices(&["Extents", "Limits", "Display", "Window"])
};
if s.paper_space && !print_all_options {
area_options.insert(0, PlotChoice::localized("Layout"));
}
if !print_all_options {
area_options.extend(
s.plot_views
.iter()
.map(|name| PlotChoice::raw(format!("View: {name}"))),
);
}
let mut area_row = row![
text(t!("What to plot")).size(11).style(muted_style).width(92),
iced::widget::pick_list(
@ -779,6 +808,18 @@ pub fn view_window(
PlotDlgMsg::Style,
width,
),
check_enabled(
t!("Plot with plot styles"),
s.apply_plot_styles,
PlotFlag::PlotStyles,
!s.style_name.is_empty(),
),
check_enabled(
t!("Display plot styles"),
s.show_plot_styles,
PlotFlag::DisplayStyles,
s.paper_space && !s.style_name.is_empty(),
),
row![
button(text(t!("Load…")).size(11))
.on_press(Message::PlotDlg(PlotDlgMsg::LoadStyle))