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 .document
.header .header
.paper_space_insertion_base = pt; .paper_space_insertion_base = pt;
self.tabs[i].scene.persist_current_layout_state();
} else { } else {
self.tabs[i] self.tabs[i]
.scene .scene

View file

@ -412,14 +412,7 @@ impl OpenCADStudio {
vec![] vec![]
}; };
// Blocks: everything the layout-reachability walk above did // Remove unreachable blocks but retain live xrefs and space containers.
// 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.
let block_remove: Vec<String> = if do_blocks { let block_remove: Vec<String> = if do_blocks {
self.tabs[i] self.tabs[i]
.scene .scene
@ -494,15 +487,7 @@ impl OpenCADStudio {
n_blocks += block_remove.len(); n_blocks += block_remove.len();
} }
// Draw-order cleanup. A SortEntitiesTable lists a block's // Remove draw-order tables whose owning blocks are gone.
// 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").
let mut n_sortents = 0usize; let mut n_sortents = 0usize;
if do_blocks { if do_blocks {
let live_blocks: rustc_hash::FxHashSet<acadrust::Handle> = self.tabs[i] let live_blocks: rustc_hash::FxHashSet<acadrust::Handle> = self.tabs[i]
@ -1913,6 +1898,9 @@ impl OpenCADStudio {
if name == "FILLMODE" { if name == "FILLMODE" {
self.tabs[i].scene.bump_geometry(); 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); self.command_line.push_output(&msg);
} else { } else {
// Queried with no value (`changed == false`): // Queried with no value (`changed == false`):

View file

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

View file

@ -256,12 +256,11 @@ impl DocumentTab {
.unwrap_or(glam::DVec3::ZERO) .unwrap_or(glam::DVec3::ZERO)
} }
/// True when the active pane edits **model-space** geometry: the Model tab, /// True when the active pane has a drawing coordinate plane.
/// 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]]
pub(super) fn editing_model_space(&self) -> bool { 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 /// 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 /// 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 /// own per-viewport UCS, the model header UCS, or the layout UCS. Keeps the
/// plain paper space. Keeps the ViewCube in lock-step. Call on every pane /// ViewCube in lock-step. Call on every pane
/// change (enter/exit viewport, layout / tab switch, load) so one field /// change (enter/exit viewport, layout / tab switch, load) so one field
/// drives all UCS-aware systems regardless of where editing happens. /// drives all UCS-aware systems regardless of where editing happens.
pub(super) fn refresh_active_ucs(&mut self) { pub(super) fn refresh_active_ucs(&mut self) {
@ -334,15 +375,52 @@ impl DocumentTab {
} else if self.scene.current_layout == "Model" { } else if self.scene.current_layout == "Model" {
self.model_ucs_from_header() self.model_ucs_from_header()
} else { } 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(); 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 /// 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 /// 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 /// model-space UCS in the Model tab, or the layout UCS. Call after a change.
/// any UCS change.
pub(super) fn persist_active_ucs(&mut self) { pub(super) fn persist_active_ucs(&mut self) {
use acadrust::types::Vector3; use acadrust::types::Vector3;
if let Some(index) = self.active_block_edit { if let Some(index) = self.active_block_edit {
@ -385,6 +463,48 @@ impl DocumentTab {
h.model_space_ucs_name.clear(); 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 /// Snapshot of the dialog's settings taken when it opened, restored by the
/// `<previous>` list entry. /// `<previous>` list entry.
plot_prev: Option<crate::ui::window::plot::PlotDialogState>, 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. /// Paper layouts shown by Print All, in tab order with their selection.
print_all_layouts: Vec<(String, bool)>, print_all_layouts: Vec<(String, bool)>,
/// True while the Plot dialog is editing settings for Print All. /// True while the Plot dialog is editing settings for Print All.
print_all_options: bool, 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. /// Settings restored when the Print All options dialog is cancelled.
print_all_options_prev: Option<crate::ui::window::plot::PlotDialogState>, print_all_options_prev: Option<crate::ui::window::plot::PlotDialogState>,
/// Plot style restored together with cancelled Print All options. /// Plot style restored together with cancelled Print All options.
print_all_plot_style_prev: Option<Option<crate::io::plot_style::PlotStyleTable>>, print_all_plot_style_prev: Option<Option<crate::io::plot_style::PlotStyleTable>>,
/// Plot window restored together with cancelled Print All options. /// Plot window restored together with cancelled Print All options.
print_all_plot_window_prev: Option<Option<(f64, f64, f64, f64)>>, print_all_plot_window_prev: Option<Option<(f64, f64, f64, f64)>>,
print_all_plot_setup_prev: Option<Option<acadrust::objects::PlotSettings>>,
// ── Plot Style Table ────────────────────────────────────────────────── // ── Plot Style Table ──────────────────────────────────────────────────
/// Currently loaded CTB/STB table (None = no override). /// Currently loaded CTB/STB table (None = no override).
@ -3287,11 +3292,14 @@ impl OpenCADStudio {
plot_orientation: crate::io::paper_sizes::Orientation::Landscape, plot_orientation: crate::io::paper_sizes::Orientation::Landscape,
plot_dialog: crate::ui::window::plot::PlotDialogState::default(), plot_dialog: crate::ui::window::plot::PlotDialogState::default(),
plot_prev: None, plot_prev: None,
plot_setup_template: None,
print_all_layouts: Vec::new(), print_all_layouts: Vec::new(),
print_all_options: false, print_all_options: false,
print_all_settings_override: false,
print_all_options_prev: None, print_all_options_prev: None,
print_all_plot_style_prev: None, print_all_plot_style_prev: None,
print_all_plot_window_prev: None, print_all_plot_window_prev: None,
print_all_plot_setup_prev: None,
opening: None, opening: None,
open_job_serial: 0, open_job_serial: 0,
recovery_report: None, recovery_report: None,

View file

@ -120,6 +120,32 @@ type ClippedPlotParams = (
Option<(f32, f32, f32, f32)>, 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( fn plot_content_extents(
wires: &[crate::io::pdf_export::PlotWire], wires: &[crate::io::pdf_export::PlotWire],
hatches: &[crate::scene::model::hatch_model::HatchModel], hatches: &[crate::scene::model::hatch_model::HatchModel],
@ -169,7 +195,18 @@ fn plot_scene_content(
crate::io::pdf_export::PlotGroupSplits, crate::io::pdf_export::PlotGroupSplits,
) { ) {
let (mut paper_wires, mut model_wires) = scene.plot_wire_groups(render_mode_override); 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); model_wires.retain(|wire| wire.plot_visible);
let with_depth = |wires: Vec<crate::scene::WireModel>| { let with_depth = |wires: Vec<crate::scene::WireModel>| {
let depths = scene.plot_wire_depths(&wires); let depths = scene.plot_wire_depths(&wires);
@ -1364,6 +1401,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
.nth(1) .nth(1)
.unwrap_or_else(|| "Model".to_string()), .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 // Object isolation is session-only. A newly opened drawing must
// not inherit the previous tab's filter, and persisted entity // 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() iced::exit()
} }
/// Write the given plot page settings into the active layout's Layout + /// Write the given plot page settings into the active layout.
/// PlotSettings objects (paper size, plot area, offset, rotation, scale).
/// No-op on the Model tab (which has no paper layout). Marks the tab dirty /// 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. /// and re-tessellates the sheet. Called by the Plot dialog's Set current action.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@ -2684,118 +2722,97 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
if layout_name != "Model" { if layout_name != "Model" {
let w: f64 = w.max(1.0); let w: f64 = w.max(1.0);
let h: f64 = h.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() { for obj in self.tabs[i].scene.document.objects.values_mut() {
if let acadrust::objects::ObjectType::Layout(l) = obj { if let acadrust::objects::ObjectType::Layout(layout) = obj {
if l.name == layout_name { if layout.name == layout_name {
l.min_limits = (0.0, 0.0); layout.min_limits = (0.0, 0.0);
l.max_limits = (w, h); layout.max_limits = (w, h);
l.min_extents = (0.0, 0.0, 0.0); layout.min_extents = (0.0, 0.0, 0.0);
l.max_extents = (w, h, 0.0); layout.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();
break; 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].dirty = true;
self.tabs[i].scene.bump_geometry_no_blocks(); self.tabs[i].scene.bump_geometry_no_blocks();
self.command_line.push_info(crate::tf!( self.command_line.push_info(crate::tf!(
@ -2809,6 +2826,19 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
&mut self, &mut self,
path: std::path::PathBuf, path: std::path::PathBuf,
) -> Task<Message> { ) -> 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)) = let Some((wires, hatches, wipeouts, group_splits, page_w, page_h, ox, oy, rotation, scale, clip)) =
self.direct_plot_params() self.direct_plot_params()
else { else {
@ -2911,6 +2941,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
.filter(|name| name != "Model") .filter(|name| name != "Model")
.map(|name| (name, true)) .map(|name| (name, true))
.collect(); .collect();
self.print_all_settings_override = false;
self.active_modal = Some(crate::app::ModalKind::PrintAll); self.active_modal = Some(crate::app::ModalKind::PrintAll);
self.reset_modal_geometry(); self.reset_modal_geometry();
Task::none() Task::none()
@ -2920,10 +2951,12 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
let previous = self.plot_dialog.clone(); let previous = self.plot_dialog.clone();
let previous_style = self.active_plot_style.clone(); let previous_style = self.active_plot_style.clone();
let previous_window = self.plot_window; let previous_window = self.plot_window;
let previous_setup = self.plot_setup_template.clone();
let task = self.on_plot_dialog_open(); let task = self.on_plot_dialog_open();
self.print_all_options_prev = Some(previous); self.print_all_options_prev = Some(previous);
self.print_all_plot_style_prev = Some(previous_style); self.print_all_plot_style_prev = Some(previous_style);
self.print_all_plot_window_prev = Some(previous_window); self.print_all_plot_window_prev = Some(previous_window);
self.print_all_plot_setup_prev = Some(previous_setup);
self.print_all_options = true; self.print_all_options = true;
self.plot_dialog.paper_space = true; self.plot_dialog.paper_space = true;
self.plot_dialog.area = "Layout".into(); 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 i = self.active_tab;
let original_layout = self.tabs[i].scene.current_layout.clone(); let original_layout = self.tabs[i].scene.current_layout.clone();
let original_viewport = self.tabs[i].scene.active_viewport; let original_viewport = self.tabs[i].scene.active_viewport;
let dialog = self.plot_dialog.clone(); let original_psltscale = self.tabs[i]
let mut pages = Vec::with_capacity(selected.len()); .scene
for name in selected { .document
// Plot helpers read the active layout. Swap only this transient .header
// selector so the drawing's saved active-space metadata is not .paper_space_linetype_scaling;
// touched while the owned page snapshot is collected. let original_plimcheck = self.tabs[i]
{ .scene
let scene = &mut self.tabs[i].scene; .document
scene.current_layout = name; .header
scene.active_viewport = None; .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 ( Ok(pages)
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),
});
}
self.tabs[i].scene.current_layout = original_layout; self.tabs[i].scene.current_layout = original_layout;
self.tabs[i].scene.active_viewport = original_viewport; 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( 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, path: std::path::PathBuf,
) -> Task<Message> { ) -> Task<Message> {
let dialog = self.plot_dialog.clone(); 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!( self.command_line.push_error(crate::tf!(
"Plot style table '{}' is not loaded.", "Plot style table '{}' is not loaded.",
dialog.style_name dialog.style_name
@ -3007,7 +3141,6 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
return Task::none(); return Task::none();
} }
}; };
let plot_style = self.dialog_plot_style(&dialog);
let worker_path = path.clone(); let worker_path = path.clone();
self.save_config(); self.save_config();
self.close_active_modal(); 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( crate::io::pdf_export::export_pdf_pages(
&pages, &pages,
&worker_path, &worker_path,
plot_style.as_ref(), None,
) )
.map(|_| format!("Exported {} layouts to {}", pages.len(), worker_path.display())) .map(|_| format!("Exported {} layouts to {}", pages.len(), worker_path.display()))
.map_err(|error| format!("Export failed: {error}")) .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"))] #[cfg(not(target_arch = "wasm32"))]
{ {
let dialog = self.plot_dialog.clone(); 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!( self.command_line.push_error(crate::tf!(
"Plot style table '{}' is not loaded.", "Plot style table '{}' is not loaded.",
dialog.style_name dialog.style_name
@ -3048,7 +3184,6 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
return Task::none(); return Task::none();
} }
}; };
let plot_style = self.dialog_plot_style(&dialog);
let options = self.plot_print_options(&dialog, Default::default()); let options = self.plot_print_options(&dialog, Default::default());
let temp_path = crate::io::print_to_printer::temp_pdf_path("print_all"); let temp_path = crate::io::print_to_printer::temp_pdf_path("print_all");
self.save_config(); self.save_config();
@ -3060,7 +3195,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
crate::io::pdf_export::export_pdf_pages( crate::io::pdf_export::export_pdf_pages(
&pages, &pages,
&temp_path, &temp_path,
plot_style.as_ref(), None,
) )
.and_then(|_| { .and_then(|_| {
crate::io::print_to_printer::print_existing_pdf(&temp_path, &options) 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 { 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 i = self.active_tab;
let scene = &self.tabs[i].scene; let scene = &self.tabs[i].scene;
let paper_space = scene.current_layout != "Model"; let paper_space = scene.current_layout != "Model";
let selected_paper = match self.plot_dialog.paper.as_str() { let selected_sheet = plot_dialog_sheet_mm(&self.plot_dialog);
"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 (source_wires, hatches, wipeouts, mut group_splits) = let (source_wires, hatches, wipeouts, mut group_splits) =
plot_scene_content( plot_scene_content(
scene, 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) let one_to_one = scale_name_for_factor(&scales, 1.0)
.or_else(|| scales.first().map(|(name, _)| name.clone())) .or_else(|| scales.first().map(|(name, _)| name.clone()))
.unwrap_or_else(|| "1:1".into()); .unwrap_or_else(|| "1:1".into());
// Keep the user's last print preferences (printer, copies, quality, let paper_space = self.tabs[self.active_tab].scene.current_layout != "Model";
// output options — persisted in `self.plot_dialog`); only refresh the let plot_views = self.tabs[self.active_tab]
// runtime printer list and reseed the drawing-specific fields from the .scene
// active layout. .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; let d = &mut self.plot_dialog;
d.printers = crate::io::print_to_printer::list_printers(); d.printers = crate::io::print_to_printer::list_printers();
d.plot_styles = crate::io::plot_style::available_ctb_names(); d.plot_styles = crate::io::plot_style::available_ctb_names();
d.scales = scales; d.scales = scales;
d.plot_views = plot_views;
if d.scale.eq_ignore_ascii_case("fit") { if d.scale.eq_ignore_ascii_case("fit") {
d.fit_to_paper = true; d.fit_to_paper = true;
d.scale_lw = false; 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) { } else if !d.scales.iter().any(|(name, _)| name == &d.scale) {
d.scale = one_to_one.clone(); 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.paper = self.plot_format.label().to_string();
d.orientation = match self.plot_orientation { d.orientation = match self.plot_orientation {
Orientation::Portrait => "Portrait", Orientation::Portrait => "Portrait",
@ -3510,32 +3639,24 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
.map(|t| t.name.clone()) .map(|t| t.name.clone())
.unwrap_or_default(); .unwrap_or_default();
d.style_missing = false; 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_input = None;
self.plot_dialog.name_rename = false; self.plot_dialog.name_rename = false;
if self.plot_dialog.fit_to_paper { if self.plot_dialog.fit_to_paper {
self.plot_dialog.scale_lw = false; 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.refresh_page_setups();
self.plot_prev = Some(previous); self.plot_prev = Some(previous);
self.select_page_setup(crate::ui::window::plot::SETUP_PREV); let cur = self.tabs[self.active_tab].scene.current_layout.clone();
// `<previous>` may come from another tab/drawing whose custom scale is let layout_entry = format!("*{cur}*");
// not present in this document. Validate again after restoring it; the if self.tabs[self.active_tab]
// pre-restore validation above only saw the temporarily reseeded state. .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") { if self.plot_dialog.scale.eq_ignore_ascii_case("fit") {
self.plot_dialog.fit_to_paper = true; self.plot_dialog.fit_to_paper = true;
self.plot_dialog.scale = one_to_one.clone(); 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 { if self.plot_dialog.fit_to_paper {
self.plot_dialog.scale_lw = false; self.plot_dialog.scale_lw = false;
} }
// A model-space plot cannot use the paper-only Layout area. Other // A model-space plot cannot use the paper-only Layout area.
// 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();
if cur == "Model" { if cur == "Model" {
if self.plot_dialog.area == "Layout" { if self.plot_dialog.area == "Layout" {
self.plot_dialog.area = "Window".into(); self.plot_dialog.area = "Window".into();
@ -3608,10 +3726,16 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
} }
M::Paper(s) => { M::Paper(s) => {
self.plot_dialog.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() Task::none()
} }
M::Orientation(s) => { M::Orientation(s) => {
self.plot_dialog.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() Task::none()
} }
M::Area(s) => { M::Area(s) => {
@ -3681,6 +3805,12 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
PlotFlag::ScaleLw if !d.fit_to_paper => { PlotFlag::ScaleLw if !d.fit_to_paper => {
d.scale_lw = !d.scale_lw 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 => { PlotFlag::UpsideDown => {
d.upside_down = !d.upside_down; d.upside_down = !d.upside_down;
} }
@ -3700,11 +3830,14 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
if name == STYLE_NONE { if name == STYLE_NONE {
self.active_plot_style = None; self.active_plot_style = None;
self.plot_dialog.style_name.clear(); 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; self.plot_dialog.style_missing = false;
} else { } else {
match crate::io::plot_style::PlotStyleTable::load_named(&name) { match crate::io::plot_style::PlotStyleTable::load_named(&name) {
Ok(table) => { Ok(table) => {
self.plot_dialog.style_name = table.name.clone(); self.plot_dialog.style_name = table.name.clone();
self.plot_dialog.apply_plot_styles = true;
self.plot_dialog.style_missing = false; self.plot_dialog.style_missing = false;
self.active_plot_style = Some(table); 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::Preview => self.on_plot_dlg_commit(true),
M::Commit if self.print_all_options => { 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!( self.command_line.push_error(crate::tf!(
"Plot style table '{}' is not loaded.", "Plot style table '{}' is not loaded.",
self.plot_dialog.style_name 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.plot_dialog.area = "Layout".into();
self.sync_dialog_plot_runtime(); self.sync_dialog_plot_runtime();
self.save_config(); self.save_config();
self.print_all_settings_override = true;
self.print_all_options = false; self.print_all_options = false;
self.print_all_options_prev = None; self.print_all_options_prev = None;
self.print_all_plot_style_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() { if let Some(previous) = self.print_all_plot_window_prev.take() {
self.plot_window = previous; 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}; use crate::ui::window::plot::{SETUP_NONE, SETUP_PREV};
self.plot_dialog.selected_setup = name.to_string(); self.plot_dialog.selected_setup = name.to_string();
if name == SETUP_NONE { if name == SETUP_NONE {
self.plot_setup_template = None;
// No page setup: default geometry + PDF output. // No page setup: default geometry + PDF output.
let is_model = self.tabs[self.active_tab].scene.current_layout == "Model"; let is_model = self.tabs[self.active_tab].scene.current_layout == "Model";
let d = &mut self.plot_dialog; let d = &mut self.plot_dialog;
d.to_file = true; d.to_file = true;
d.paper = "A4".into(); d.paper = "A4".into();
d.orientation = "Landscape".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.area = if is_model { "Window".into() } else { "Layout".into() };
d.center = true; d.center = true;
d.offset_x = "0.0".into(); 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, PlotPaperUnits, PlotRotation, PlotSettings, PlotType, ScaledType, ShadePlotMode,
ShadePlotResolutionLevel, ShadePlotResolutionLevel,
}; };
use crate::io::paper_sizes::{sheet_mm, Orientation, PaperSize};
let d = &self.plot_dialog; let d = &self.plot_dialog;
let paper = match d.paper.as_str() { let (w, h) = plot_dialog_sheet_mm(d);
"A3" => PaperSize::A3, let mut ps = match self.plot_setup_template.clone() {
"A2" => PaperSize::A2, Some(settings) => settings,
"A1" => PaperSize::A1, None => {
"A0" => PaperSize::A0, let mut settings = PlotSettings::new("");
_ => PaperSize::A4, 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_width = w;
ps.paper_height = h; ps.paper_height = h;
ps.paper_size = d.paper.clone(); ps.paper_size = d.paper.clone();
ps.paper_units = PlotPaperUnits::Millimeters;
ps.plot_type = match d.area.as_str() { ps.plot_type = match d.area.as_str() {
"Window" => PlotType::Window, "Window" => PlotType::Window,
"Layout" => PlotType::Layout, "Layout" => PlotType::Layout,
"Display" => PlotType::LastScreenDisplay, "Display" => PlotType::LastScreenDisplay,
"Limits" => PlotType::Limits,
area if area.starts_with("View: ") => PlotType::View,
_ => PlotType::Extents, _ => PlotType::Extents,
}; };
ps.plot_view_name = d
.area
.strip_prefix("View: ")
.unwrap_or("")
.to_string();
if d.area == "Window" { if d.area == "Window" {
if let Some((x0, y0, x1, y1)) = self.plot_window { if let Some((x0, y0, x1, y1)) = self.plot_window {
ps.set_plot_window(x0, y0, x1, y1); 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" { if d.area == "Layout" {
ps.set_standard_scale(ScaledType::OneToOne); ps.set_standard_scale(ScaledType::OneToOne);
ps.standard_scale_factor = 1.0;
} else if d.fit_to_paper { } else if d.fit_to_paper {
ps.set_scale_to_fit(); ps.set_scale_to_fit();
} else { } else {
@ -4005,6 +4144,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
ps.scale_type = ScaledType::CustomScale; ps.scale_type = ScaledType::CustomScale;
ps.scale_numerator = factor; ps.scale_numerator = factor;
ps.scale_denominator = 1.0; ps.scale_denominator = 1.0;
ps.standard_scale_factor = factor;
ps.flags.use_standard_scale = false;
} }
ps.printer_name = if d.to_file { ps.printer_name = if d.to_file {
crate::ui::window::plot::OUT_PDF.into() 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.current_style_sheet = d.style_name.clone();
ps.flags.scale_lineweights = d.scale_lw; ps.flags.scale_lineweights = d.scale_lw;
ps.flags.print_lineweights = d.lineweights; 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.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() { ps.shade_plot_mode = match d.shade.as_str() {
"2D Wireframe" | "3D Wireframe" => ShadePlotMode::Wireframe, "2D Wireframe" | "3D Wireframe" => ShadePlotMode::Wireframe,
"Hidden Line" => ShadePlotMode::Hidden, "Hidden Line" => ShadePlotMode::Hidden,
@ -4037,6 +4179,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
use acadrust::objects::{ use acadrust::objects::{
PlotType, ShadePlotMode, ShadePlotResolutionLevel, PlotType, ShadePlotMode, ShadePlotResolutionLevel,
}; };
self.plot_setup_template = Some(ps.clone());
if matches!(ps.plot_type, PlotType::Window) && !ps.plot_window.is_empty() { if matches!(ps.plot_type, PlotType::Window) && !ps.plot_window.is_empty() {
self.plot_window = Some(( self.plot_window = Some((
ps.plot_window.lower_left_x, 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 active_style_name = self.active_plot_style.as_ref().map(|table| table.name.clone());
let style_name = if ps.current_style_sheet.is_empty() { let style_name = if ps.current_style_sheet.is_empty() {
active_style_name.clone().unwrap_or_default() String::new()
} else if active_style_name } else if active_style_name
.as_deref() .as_deref()
.is_some_and(|name| name.eq_ignore_ascii_case(&ps.current_style_sheet)) .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 && active_style_name
.as_deref() .as_deref()
.is_some_and(|name| name.eq_ignore_ascii_case(&style_name)); .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; let d = &mut self.plot_dialog;
d.paper = paper; 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.orientation = orient;
d.area = match ps.plot_type { d.area = match ps.plot_type {
PlotType::Window => "Window", PlotType::Window => "Window".to_string(),
PlotType::Layout => "Layout", PlotType::Layout => "Layout".to_string(),
PlotType::LastScreenDisplay => "Display", PlotType::LastScreenDisplay => "Display".to_string(),
_ => "Extents", PlotType::Limits => "Limits".to_string(),
} PlotType::View if !ps.plot_view_name.is_empty() => {
.to_string(); 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" { if !d.paper_space && d.area == "Layout" {
d.area = "Extents".into(); 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(); d.fit_to_paper = d.area != "Layout" && ps.is_scale_to_fit();
let target_factor = if d.area == "Layout" { let target_factor = if d.area == "Layout" {
1.0 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 { } else if ps.scale_denominator.abs() > 1e-9 {
ps.scale_numerator / ps.scale_denominator ps.scale_numerator / ps.scale_denominator
} else { } 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.scale_lw = ps.flags.scale_lineweights && !d.fit_to_paper;
d.lineweights = ps.flags.print_lineweights; d.lineweights = ps.flags.print_lineweights;
d.paperspace_last = ps.flags.draw_viewports_first; d.paperspace_last = ps.flags.draw_viewports_first;
d.shade = match ps.shade_plot_mode { d.shade = if ps.flags.plot_hidden {
ShadePlotMode::Wireframe => "2D Wireframe", "Hidden Line"
ShadePlotMode::Hidden => "Hidden Line", } else {
ShadePlotMode::Rendered => "Gouraud Shaded", match ps.shade_plot_mode {
ShadePlotMode::AsDisplayed => "As displayed", ShadePlotMode::Wireframe => "2D Wireframe",
ShadePlotMode::Hidden => "Hidden Line",
ShadePlotMode::Rendered => "Gouraud Shaded",
ShadePlotMode::AsDisplayed => "As displayed",
}
} }
.into(); .into();
d.quality = match ps.shade_plot_resolution { 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.printer = (!ps.printer_name.is_empty()).then(|| ps.printer_name.clone());
} }
d.style_name = style_name; 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; 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) { fn apply_dialog_to_layout(&mut self) {
use crate::io::paper_sizes::sheet_mm;
self.sync_dialog_plot_runtime(); self.sync_dialog_plot_runtime();
let d = self.plot_dialog.clone(); 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 rotation: i16 = if d.upside_down { 180 } else { 0 };
let off_x = d.offset_x.parse::<f64>().unwrap_or(0.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); 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. /// 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> { fn on_plot_dlg_commit(&mut self, preview: bool) -> Task<Message> {
let d = self.plot_dialog.clone(); 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!( self.command_line.push_error(crate::tf!(
"Plot style table '{}' is not loaded.", "Plot style table '{}' is not loaded.",
d.style_name 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); let plot_style = self.dialog_plot_style(&d);
// Extents, Window and Display use one plot path in both spaces. Only // Extents, Window and Display use one plot path in both spaces. Only
// Paper-space Layout is special: it uses the physical sheet bounds. // 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() { let job = match d.area.as_str() {
"Display" => self.display_plot_job(), "Display" => self.display_plot_job(),
"Extents" => self.extents_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(( let Some((
w_wires, w_wires,
@ -4341,7 +4513,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
&self, &self,
d: &crate::ui::window::plot::PlotDialogState, d: &crate::ui::window::plot::PlotDialogState,
) -> Option<crate::io::plot_style::PlotStyleTable> { ) -> 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; return None;
} }
self.active_plot_style 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()?) 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> { fn extents_plot_job(&self) -> Option<ClippedPlotParams> {
let scene = &self.tabs[self.active_tab].scene; let scene = &self.tabs[self.active_tab].scene;
if scene.current_layout == "Model" { 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 /// Render a selected rectangle through one shared Model/Paper path. The
/// window may lie partly or wholly outside a paper sheet. /// window may lie partly or wholly outside a paper sheet.
fn area_plot_job(&self, window: (f64, f64, f64, f64)) -> Option<ClippedPlotParams> { 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 i = self.active_tab;
let (x0, y0, x1, y1) = window; let (x0, y0, x1, y1) = window;
if (x1 - x0) < 1e-6 || (y1 - y0) < 1e-6 { if (x1 - x0) < 1e-6 || (y1 - y0) < 1e-6 {
return None; 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_w = (x1 - x0).max(1e-9);
let win_h = (y1 - y0).max(1e-9); let win_h = (y1 - y0).max(1e-9);
let scale_sel = if self.plot_dialog.fit_to_paper { 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() { if let Some(previous) = self.print_all_plot_window_prev.take() {
self.plot_window = previous; 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.print_all_options = false;
self.active_modal = Some(PrintAll); self.active_modal = Some(PrintAll);
self.reset_modal_geometry(); self.reset_modal_geometry();
@ -6917,6 +6920,9 @@ impl OpenCADStudio {
self.plot_dialog.style_missing = false; self.plot_dialog.style_missing = false;
self.plot_dialog.plot_styles = self.plot_dialog.plot_styles =
crate::io::plot_style::available_ctb_names(); crate::io::plot_style::available_ctb_names();
self.tabs[self.active_tab]
.scene
.invalidate_display_plot_style();
self.command_line.push_output( self.command_line.push_output(
crate::tf!( crate::tf!(
@ -6968,6 +6974,9 @@ impl OpenCADStudio {
self.plot_dialog.style_missing = false; self.plot_dialog.style_missing = false;
self.plot_dialog.plot_styles = self.plot_dialog.plot_styles =
crate::io::plot_style::available_ctb_names(); crate::io::plot_style::available_ctb_names();
self.tabs[self.active_tab]
.scene
.invalidate_display_plot_style();
self.command_line.push_output(crate::tf!( self.command_line.push_output(crate::tf!(
"Plot style table saved to \"{}\".", "Plot style table saved to \"{}\".",
path.display() path.display()

View file

@ -62,8 +62,7 @@ pub(super) fn layout_entry_name(s: &str) -> &str {
s.trim_start_matches('*').trim_end_matches('*') s.trim_start_matches('*').trim_end_matches('*')
} }
/// Infer the closest A-series paper label and orientation from sheet /// Infer an A-series label when dimensions match; otherwise retain the size.
/// dimensions (mm). Falls back to A4 when nothing is close.
pub(super) fn paper_label_from_dims(w: f64, h: f64) -> (String, String) { pub(super) fn paper_label_from_dims(w: f64, h: f64) -> (String, String) {
use crate::io::paper_sizes::PaperSize; use crate::io::paper_sizes::PaperSize;
let orient = if w >= h { "Landscape" } else { "Portrait" }; 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 = (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) { 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.set_current_layout(name.clone());
self.tabs[i].scene.deselect_all(); self.tabs[i].scene.deselect_all();
let switch_ms = perf_phase.elapsed().as_secs_f64() * 1000.0; let switch_ms = perf_phase.elapsed().as_secs_f64() * 1000.0;
// UCS follows the pane: model header UCS in the Model tab, none // UCS follows the active model, layout, or floating viewport pane.
// in plain paper space (a viewport's UCS is adopted on entry).
let perf_phase = Instant::now(); let perf_phase = Instant::now();
self.tabs[i].refresh_active_ucs(); self.tabs[i].refresh_active_ucs();
self.tabs[i].scene.restore_saved_camera(); self.tabs[i].scene.restore_saved_camera();
@ -5358,10 +5357,28 @@ impl OpenCADStudio {
self.push_undo_snapshot(i, "LAYOUT"); self.push_undo_snapshot(i, "LAYOUT");
match self.tabs[i].scene.document.add_layout(&new_name) { match self.tabs[i].scene.document.add_layout(&new_name) {
Ok(_) => { 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() { for obj in self.tabs[i].scene.document.objects.values_mut() {
if let acadrust::objects::ObjectType::Layout(l) = obj { if let acadrust::objects::ObjectType::Layout(l) = obj {
if l.name == new_name { if l.name == new_name {
l.flags = layout_flags;
l.min_limits = (0.0, 0.0); l.min_limits = (0.0, 0.0);
l.max_limits = (297.0, 210.0); l.max_limits = (297.0, 210.0);
l.min_extents = (0.0, 0.0, 0.0); l.min_extents = (0.0, 0.0, 0.0);
@ -5371,6 +5388,14 @@ impl OpenCADStudio {
l.plot_paper_units = 1; l.plot_paper_units = 1;
l.plot_scale_numerator = 1.0; l.plot_scale_numerator = 1.0;
l.plot_scale_denominator = 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(); l.paper_size = "ISO_A4_(297.00_x_210.00_MM)".into();
break; break;
} }

View file

@ -25,6 +25,7 @@ use std::io::Write;
use std::path::Path; use std::path::Path;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub struct PlotWire { pub struct PlotWire {
pub wire: WireModel, pub wire: WireModel,
pub draw_depth: f32, pub draw_depth: f32,
@ -120,6 +121,7 @@ pub struct PdfPageInput {
pub scale: f32, pub scale: f32,
pub clip: Option<(f32, f32, f32, f32)>, pub clip: Option<(f32, f32, f32, f32)>,
pub options: PdfPlotOptions, pub options: PdfPlotOptions,
pub plot_style: Option<PlotStyleTable>,
} }
impl Default for PdfPlotOptions { impl Default for PdfPlotOptions {
@ -270,7 +272,7 @@ fn build_pdf_pages(pages: &[PdfPageInput], plot_style: Option<&PlotStyleTable>)
page.rotation_deg, page.rotation_deg,
page.scale, page.scale,
page.clip, page.clip,
plot_style, page.plot_style.as_ref().or(plot_style),
page.options, page.options,
); );
} }
@ -440,6 +442,8 @@ fn append_pdf_page(
let mut last_color: Option<[f32; 3]> = None; let mut last_color: Option<[f32; 3]> = None;
let mut last_lw: Option<f32> = 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 // Current PDF dash array (empty = solid). Tracked so the dash op is only
// re-emitted when it actually changes between wires. // re-emitted when it actually changes between wires.
let mut last_dash: Option<Vec<i64>> = None; let mut last_dash: Option<Vec<i64>> = None;
@ -453,7 +457,9 @@ fn append_pdf_page(
ox, ox,
oy, oy,
plot_style, plot_style,
scale,
options, options,
normal_blend.as_ref(),
); );
last_color = None; last_color = None;
last_lw = None; last_lw = None;
@ -506,6 +512,8 @@ fn append_pdf_page(
let mut lw_override: Option<f32> = None; let mut lw_override: Option<f32> = None;
let mut screening = 1.0; let mut screening = 1.0;
let mut color_overridden = false; let mut color_overridden = false;
let mut cap = None;
let mut join = None;
if let Some(ctb) = plot_style { if let Some(ctb) = plot_style {
if wire.aci > 0 { if wire.aci > 0 {
if let Some([cr, cg, cb]) = ctb.resolve_color(wire.aci) { if let Some([cr, cg, cb]) = ctb.resolve_color(wire.aci) {
@ -518,6 +526,20 @@ fn append_pdf_page(
.resolve_lineweight(wire.aci) .resolve_lineweight(wire.aci)
.map(|mm| (mm * MM_TO_PT).max(0.1)); .map(|mm| (mm * MM_TO_PT).max(0.1));
screening = ctb.resolve_screening(wire.aci); 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 // 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); [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 if last_color
.map(|c| (c[0] - r).abs() > 0.01 || (c[1] - g).abs() > 0.01 || (c[2] - b).abs() > 0.01) .map(|c| (c[0] - r).abs() > 0.01 || (c[1] - g).abs() > 0.01 || (c[2] - b).abs() > 0.01)
.unwrap_or(true) .unwrap_or(true)
@ -576,13 +609,11 @@ fn append_pdf_page(
let lw_pt = if wire.world_width > 0.0 { let lw_pt = if wire.world_width > 0.0 {
wire.world_width * MM_TO_PT wire.world_width * MM_TO_PT
} else { } else {
let physical = lw_override.unwrap_or_else(|| { let physical = if options.object_lineweights {
if options.object_lineweights { lw_override.unwrap_or_else(|| (wire.line_weight_px * LW_PX_TO_PT).max(0.1))
(wire.line_weight_px * LW_PX_TO_PT).max(0.1) } else {
} else { 0.1
0.1 };
}
});
physical / pen_divisor physical / pen_divisor
}; };
if last_lw.map(|l| (l - lw_pt).abs() > 0.01).unwrap_or(true) { if last_lw.map(|l| (l - lw_pt).abs() > 0.01).unwrap_or(true) {
@ -852,12 +883,71 @@ fn emit_wire_fills(
ox: f64, ox: f64,
oy: f64, oy: f64,
plot_style: Option<&PlotStyleTable>, plot_style: Option<&PlotStyleTable>,
scale: f32,
options: PdfPlotOptions, options: PdfPlotOptions,
normal_blend: Option<&ExtendedGraphicsStateId>,
) { ) {
for wire in wires { for wire in wires {
if wire.fill_tris.is_empty() { if wire.fill_tris.is_empty() {
continue; 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; let [mut r, mut g, mut b, a] = wire.color;
if a < 0.01 { if a < 0.01 {
continue; continue;
@ -964,6 +1054,26 @@ fn emit_hatch(
if hatch.boundary.is_empty() { if hatch.boundary.is_empty() {
return; 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; let [mut r, mut g, mut b, a] = hatch.color;
if a < 0.01 { if a < 0.01 {
return; return;
@ -1082,13 +1192,11 @@ fn emit_hatch(
// Pattern hatches: rasterise the family lines clipped to the boundary // Pattern hatches: rasterise the family lines clipped to the boundary
// and emit each as a stroked line. Skips the polygon outline entirely. // and emit each as a stroked line. Skips the polygon outline entirely.
if matches!(hatch.pattern, HatchPattern::Pattern(_)) { if matches!(hatch.pattern, HatchPattern::Pattern(_)) {
let physical = lw_override.unwrap_or_else(|| { let physical = if options.object_lineweights {
if options.object_lineweights { lw_override.unwrap_or_else(|| (hatch.line_weight_px * LW_PX_TO_PT).max(0.1))
(hatch.line_weight_px * LW_PX_TO_PT).max(0.1) } else {
} else { 0.1
0.1 };
}
});
let divisor = if options.scale_lineweights { let divisor = if options.scale_lineweights {
1.0 1.0
} else { } else {
@ -1248,16 +1356,16 @@ fn emit_text(
if let Some(ctb) = plot_style { if let Some(ctb) = plot_style {
if wire.aci > 0 { if wire.aci > 0 {
ctb_color = ctb.resolve_color(wire.aci); ctb_color = ctb.resolve_color(wire.aci);
lw_override = ctb lw_override = options.object_lineweights.then(|| {
.resolve_lineweight(wire.aci) ctb.resolve_lineweight(wire.aci).map(|mm| {
.map(|mm| {
let divisor = if options.scale_lineweights { let divisor = if options.scale_lineweights {
1.0 1.0
} else { } else {
scale.max(1e-6) scale.max(1e-6)
}; };
(mm * MM_TO_PT).max(0.1) / divisor (mm * MM_TO_PT).max(0.1) / divisor
}); })
}).flatten();
screening = ctb.resolve_screening(wire.aci); screening = ctb.resolve_screening(wire.aci);
} }
} }

View file

@ -1,7 +1,7 @@
//! Plot Style Table — CTB (color-based) and STB (named) file support. //! Plot Style Table — CTB (color-based) and STB (named) file support.
//! //!
//! CTB files map indexed drawing colors (ACI, 1-255) to pen properties: //! 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. //! 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 DEFAULT_PLOT_STYLE: &str = "ocad.ctb";
pub const MONOCHROME_PLOT_STYLE: &str = "monochrome.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"))] #[cfg(not(target_arch = "wasm32"))]
pub fn plot_styles_dir() -> Result<PathBuf, String> { pub fn plot_styles_dir() -> Result<PathBuf, String> {
@ -52,10 +67,14 @@ pub fn available_ctb_names() -> Vec<String> {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
{ {
let Ok(dir) = ensure_plot_styles_dir() else { 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 { 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 let mut names: Vec<String> = entries
.filter_map(Result::ok) .filter_map(Result::ok)
@ -67,13 +86,16 @@ pub fn available_ctb_names() -> Vec<String> {
.then(|| entry.file_name().to_string_lossy().into_owned()) .then(|| entry.file_name().to_string_lossy().into_owned())
}) })
.collect(); .collect();
names.extend(STANDARD_PLOT_STYLES.iter().map(|name| (*name).to_string()));
names.sort_by_key(|name| name.to_ascii_lowercase()); names.sort_by_key(|name| name.to_ascii_lowercase());
names.dedup_by(|left, right| left.eq_ignore_ascii_case(right)); names.dedup_by(|left, right| left.eq_ignore_ascii_case(right));
names names
} }
#[cfg(target_arch = "wasm32")] #[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, MONOCHROME_PLOT_STYLE,
include_bytes!("../../assets/plotstyles/monochrome.ctb"), 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}")), _ => 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. /// Load one CTB by file name from the per-user plot styles folder.
pub fn load_named(name: &str) -> Result<Self, String> { pub fn load_named(name: &str) -> Result<Self, String> {
let path = Path::new(name); let path = Path::new(name);
@ -227,9 +283,11 @@ impl PlotStyleTable {
.to_string_lossy() .to_string_lossy()
.eq_ignore_ascii_case(name) .eq_ignore_ascii_case(name)
}) })
.map(|entry| entry.path()) .map(|entry| entry.path());
.ok_or_else(|| format!("Plot style not found: {name}"))?; return match matched {
return Self::load(&matched); Some(path) => Self::load(&path),
None => Self::builtin(name),
};
} }
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
@ -248,9 +306,23 @@ impl PlotStyleTable {
/// Returns None if no override (use object color). /// Returns None if no override (use object color).
pub fn resolve_color(&self, aci: u8) -> Option<[f32; 3]> { pub fn resolve_color(&self, aci: u8) -> Option<[f32; 3]> {
let entry = self.aci_entries.get(aci as usize)?; let entry = self.aci_entries.get(aci as usize)?;
entry let mut color = entry
.color .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. /// Resolve the effective lineweight in mm for the given ACI index.
@ -260,7 +332,13 @@ impl PlotStyleTable {
if matches!(entry.lineweight, 0 | 255) { if matches!(entry.lineweight, 0 | 255) {
None None
} else { } 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` /// 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 /// flags / `-o` options. On Windows the generated PDF already carries render
/// options. Windows queues repeated jobs when more than one copy is requested; /// 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. // Remove the Layout object itself.
self.document.objects.remove(&layout_handle); self.document.objects.remove(&layout_handle);
// Drop the layout's entry from the ACAD_LAYOUT dictionary so it does not // Drop the layout's dictionary entry so it does not dangle.
// dangle (and so AutoCAD doesn't try to recover a now-missing layout).
let dict_handle = self.document.header.acad_layout_dict_handle; let dict_handle = self.document.header.acad_layout_dict_handle;
if let Some(ObjectType::Dictionary(d)) = self.document.objects.get_mut(&dict_handle) { if let Some(ObjectType::Dictionary(d)) = self.document.objects.get_mut(&dict_handle) {
d.entries.retain(|(k, _)| k != name); d.entries.retain(|(k, _)| k != name);
@ -90,20 +89,6 @@ impl Scene {
self.document.block_records.remove(&bn); 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 the deleted layout was active, fall back to Model space.
if self.current_layout == name { if self.current_layout == name {
self.current_layout = "Model".to_string(); self.current_layout = "Model".to_string();
@ -498,9 +483,7 @@ impl Scene {
/// Reconstruct a `pane_grid` configuration from a set of (tile-index, rect) /// Reconstruct a `pane_grid` configuration from a set of (tile-index, rect)
/// items covering `region`, by recursively finding a full vertical or /// items covering `region`, by recursively finding a full vertical or
/// horizontal guillotine cut. AutoCAD tiled configs (and anything pane_grid /// horizontal guillotine cut. Non-guillotine input falls back to chained panes.
/// produces) are guillotine layouts, so this round-trips them. A non-guillotine
/// set falls back to chaining panes so nothing is lost.
fn config_from_rects( fn config_from_rects(
items: &[(usize, iced::Rectangle)], items: &[(usize, iced::Rectangle)],
region: iced::Rectangle, region: iced::Rectangle,

View file

@ -91,6 +91,7 @@ impl Scene {
self.document.header.limit_check = enabled; self.document.header.limit_check = enabled;
} else { } else {
self.document.header.paper_space_limit_check = enabled; 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 /// movement scanned the whole document for wipeouts and recreated the same
/// Arcs, making Paper frame construction CPU-bound on large drawings. /// Arcs, making Paper frame construction CPU-bound on large drawings.
paper_sheet_render_cache: RefCell<HashMap<String, PaperSheetRenderCache>>, 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. /// Per-viewport projected wire cache for paper-space content viewports.
/// Stores projected + clipped wires in paper-space coordinates. /// Stores projected + clipped wires in paper-space coordinates.
/// Maps vp_handle → (geometry_epoch, Vec<WireModel>). /// Maps vp_handle → (geometry_epoch, Vec<WireModel>).
@ -1880,6 +1889,10 @@ impl Scene {
paper_sheet_cache: RefCell::new(HashMap::default()), paper_sheet_cache: RefCell::new(HashMap::default()),
paper_viewport_cache: RefCell::new(HashMap::default()), paper_viewport_cache: RefCell::new(HashMap::default()),
paper_sheet_render_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()), paper_projected_cache: RefCell::new(HashMap::default()),
current_layout: "Model".to_string(), current_layout: "Model".to_string(),
block_edit_block: None, block_edit_block: None,
@ -2878,7 +2891,9 @@ impl Scene {
/// active layout or its background are dropped. /// active layout or its background are dropped.
pub fn set_current_layout(&mut self, name: String) { pub fn set_current_layout(&mut self, name: String) {
if self.current_layout != name { if self.current_layout != name {
self.persist_current_layout_state();
self.current_layout = name; self.current_layout = name;
self.load_current_layout_state();
self.sync_active_space_to_document(); self.sync_active_space_to_document();
self.recolor_meshes(); self.recolor_meshes();
*self.wire_cache.borrow_mut() = None; *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 /// 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` /// document's persisted settings so it round-trips on save: the `$TILEMODE`
/// header (`show_model_space`) and the `CTAB` current-tab variable. The /// header (`show_model_space`) and the `CTAB` current-tab variable. The
@ -3305,32 +3405,17 @@ impl Scene {
Some(wire) Some(wire)
} }
/// The effective plot settings for the current layout: a standalone /// The effective plot settings embedded in the current layout.
/// 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.
pub fn effective_plot_settings(&self) -> Option<acadrust::objects::PlotSettings> { pub fn effective_plot_settings(&self) -> Option<acadrust::objects::PlotSettings> {
self.plot_settings_for(&self.current_layout) self.plot_settings_for(&self.current_layout)
} }
/// Plot settings for a specific layout by name: its standalone /// Plot settings embedded in a specific layout.
/// `PlotSettings` object if one exists, else synthesized from the `Layout`
/// object's embedded fields.
pub fn plot_settings_for(&self, name: &str) -> Option<acadrust::objects::PlotSettings> { pub fn plot_settings_for(&self, name: &str) -> Option<acadrust::objects::PlotSettings> {
use acadrust::objects::{ use acadrust::objects::{
ObjectType, PaperMargin, PlotPaperUnits, PlotRotation, PlotSettings, PlotType, 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| { self.document.objects.values().find_map(|o| {
let ObjectType::Layout(l) = o else { let ObjectType::Layout(l) = o else {
return None; return None;
@ -3338,10 +3423,13 @@ impl Scene {
if l.name.as_str() != name { if l.name.as_str() != name {
return None; 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_width = l.paper_width;
ps.paper_height = l.paper_height; ps.paper_height = l.paper_height;
ps.paper_size = l.paper_size.clone(); 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( ps.margins = PaperMargin::new(
l.plot_margin_left, l.plot_margin_left,
l.plot_margin_bottom, l.plot_margin_bottom,
@ -3362,10 +3450,84 @@ impl Scene {
ps.scale_type = ScaledType::from_code(l.plot_scale_type); ps.scale_type = ScaledType::from_code(l.plot_scale_type);
ps.scale_numerator = l.plot_scale_numerator; ps.scale_numerator = l.plot_scale_numerator;
ps.scale_denominator = l.plot_scale_denominator; 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) 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 /// 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, /// 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, /// 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. /// Hatch fill pattern.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum HatchPattern { pub enum HatchPattern {

View file

@ -22,6 +22,8 @@ use crate::scene::{
SceneLight, Uniforms, ViewportInstance, WireModel, SceneLight, Uniforms, ViewportInstance, WireModel,
}; };
const DISPLAY_STYLE_CACHE_LIMIT: usize = if cfg!(target_arch = "wasm32") { 4 } else { 24 };
// ── Camera hover state (shader::Program::State) ─────────────────────────── // ── Camera hover state (shader::Program::State) ───────────────────────────
#[derive(Clone, Default)] #[derive(Clone, Default)]
@ -2496,6 +2498,238 @@ impl Scene {
}; };
(final_color, pl, pat, lw, aci) (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). /// Whether an entity sits on a locked layer (via the document's layer table).
@ -3403,11 +3637,15 @@ impl Scene {
} else { } else {
self.model_wires_for_viewport_arc(inst.handle, full.height) 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 // 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 // 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 // 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. // 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); 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 // 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 // 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), width: full.width.max(1.0),
height: full.height.max(1.0), height: full.height.max(1.0),
}; };
let mut uniforms = let display_plot_lineweights = self.current_layout != "Model"
Uniforms::new(&inst.camera, full_bounds, self.document.header.lineweight_display); && 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 if self.document.header.paper_space_linetype_scaling
&& !inst.paper_sheet && !inst.paper_sheet
&& inst.tile_idx.is_none() && inst.tile_idx.is_none()
@ -3642,6 +3887,29 @@ impl Scene {
None, 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 { let images = if let Some(images) = paper_images {
images images
} else { } else {

View file

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