diff --git a/src/app/commands/blocks.rs b/src/app/commands/blocks.rs index caeb9f05c..35e3cc205 100644 --- a/src/app/commands/blocks.rs +++ b/src/app/commands/blocks.rs @@ -30,6 +30,7 @@ impl OpenCADStudio { .document .header .paper_space_insertion_base = pt; + self.tabs[i].scene.persist_current_layout_state(); } else { self.tabs[i] .scene diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index c1d30748d..39d4cd93c 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -412,14 +412,7 @@ impl OpenCADStudio { vec![] }; - // Blocks: everything the layout-reachability walk above did - // NOT mark live. That covers unreferenced named blocks, - // leftover anonymous blocks (*U hatch/unnamed, orphaned *D - // dimension, *T table, dynamic-block variants — AutoCAD drops - // these too), AND whole dead subgraphs a detached xref leaves - // behind. Live xrefs stay (an attached xref's blocks are - // reached from the layout that inserts them); the *Model_Space - // / *Paper_Space containers are never removed. + // Remove unreachable blocks but retain live xrefs and space containers. let block_remove: Vec = if do_blocks { self.tabs[i] .scene @@ -494,15 +487,7 @@ impl OpenCADStudio { n_blocks += block_remove.len(); } - // Draw-order cleanup. A SortEntitiesTable lists a block's - // entities in draw order; purging the block drops the entities - // and the block record, but this object lingers, still holding - // the (now dangling) handle of every deleted entity. On a - // detached-xref purge that is hundreds of thousands of stale - // handles — megabytes of dead weight the save re-emits, which is - // why an OCS-purged file stayed far larger than AutoCAD's. - // Drop every SortEntitiesTable whose owning block is gone - // (AutoCAD's "orphaned data"). + // Remove draw-order tables whose owning blocks are gone. let mut n_sortents = 0usize; if do_blocks { let live_blocks: rustc_hash::FxHashSet = self.tabs[i] @@ -1913,6 +1898,9 @@ impl OpenCADStudio { if name == "FILLMODE" { self.tabs[i].scene.bump_geometry(); } + if matches!(name.as_str(), "PSLTSCALE" | "PLIMCHECK") { + self.tabs[i].scene.persist_current_layout_state(); + } self.command_line.push_output(&msg); } else { // Queried with no value (`changed == false`): diff --git a/src/app/commands/view.rs b/src/app/commands/view.rs index fa4a223b7..02dc22220 100644 --- a/src/app/commands/view.rs +++ b/src/app/commands/view.rs @@ -568,63 +568,49 @@ impl OpenCADStudio { } } else { // ── Preset viewport layout ─────────────────────────── - // Determine paper dimensions from PlotSettings (fallback A4 landscape). let layout_name = scene.current_layout.clone(); - let (paper_w, paper_h) = { - use acadrust::objects::ObjectType; - let mut pw = 297.0_f64; - let mut ph = 210.0_f64; - for (_, obj) in &scene.document.objects { - if let ObjectType::PlotSettings(ps) = obj { - if ps.page_name == layout_name && ps.paper_width > 0.0 { - pw = ps.paper_width; - ph = ps.paper_height; - break; - } - } - } - (pw, ph) - }; - let margin = 5.0_f64; // mm margin around the usable area - let uw = paper_w - 2.0 * margin; // usable width - let uh = paper_h - 2.0 * margin; // usable height - // Collect rectangle specs: (cx, cz, w, h) in mm + let ((x0, y0), (x1, y1)) = scene + .printable_area_limits() + .unwrap_or(((0.0, 0.0), (297.0, 210.0))); + let uw = (x1 - x0).max(1.0); + let uh = (y1 - y0).max(1.0); + let gap = 2.0 * scene.paper_space_unit_factor(); let rects: Vec<(f64, f64, f64, f64)> = match sub.as_str() { "2H" => { // Two viewports side by side (horizontal split) - let vw = (uw - 2.0) / 2.0; + let vw = (uw - gap) / 2.0; vec![ - (margin + vw / 2.0, margin + uh / 2.0, vw, uh), - (margin + vw + 2.0 + vw / 2.0, margin + uh / 2.0, vw, uh), + (x0 + vw / 2.0, y0 + uh / 2.0, vw, uh), + (x0 + vw + gap + vw / 2.0, y0 + uh / 2.0, vw, uh), ] } "2V" => { // Two viewports stacked (vertical split) - let vh = (uh - 2.0) / 2.0; + let vh = (uh - gap) / 2.0; vec![ - (margin + uw / 2.0, margin + vh + 2.0 + vh / 2.0, uw, vh), - (margin + uw / 2.0, margin + vh / 2.0, uw, vh), + (x0 + uw / 2.0, y0 + vh + gap + vh / 2.0, uw, vh), + (x0 + uw / 2.0, y0 + vh / 2.0, uw, vh), ] } "4" => { // Four equal viewports (2×2 grid) - let vw = (uw - 2.0) / 2.0; - let vh = (uh - 2.0) / 2.0; + let vw = (uw - gap) / 2.0; + let vh = (uh - gap) / 2.0; vec![ - (margin + vw / 2.0, margin + vh + 2.0 + vh / 2.0, vw, vh), + (x0 + vw / 2.0, y0 + vh + gap + vh / 2.0, vw, vh), ( - margin + vw + 2.0 + vw / 2.0, - margin + vh + 2.0 + vh / 2.0, + x0 + vw + gap + vw / 2.0, + y0 + vh + gap + vh / 2.0, vw, vh, ), - (margin + vw / 2.0, margin + vh / 2.0, vw, vh), - (margin + vw + 2.0 + vw / 2.0, margin + vh / 2.0, vw, vh), + (x0 + vw / 2.0, y0 + vh / 2.0, vw, vh), + (x0 + vw + gap + vw / 2.0, y0 + vh / 2.0, vw, vh), ] } "SINGLE" | "1" => { // Single full-page viewport - vec![(margin + uw / 2.0, margin + uh / 2.0, uw, uh)] + vec![(x0 + uw / 2.0, y0 + uh / 2.0, uw, uh)] } _ => { self.command_line.push_error( diff --git a/src/app/document.rs b/src/app/document.rs index 5893af529..3c866fac2 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -256,12 +256,11 @@ impl DocumentTab { .unwrap_or(glam::DVec3::ZERO) } - /// True when the active pane edits **model-space** geometry: the Model tab, - /// or inside a floating viewport (MSPACE). The UCS applies in both; plain - /// paper space (no active viewport) is excluded. Single predicate so every - /// UCS-aware system shares one rule. [[feedback_shared_infra]] + /// True when the active pane has a drawing coordinate plane. pub(super) fn editing_model_space(&self) -> bool { - self.scene.current_layout == "Model" || self.scene.active_viewport.is_some() + self.scene.current_layout == "Model" + || self.scene.active_viewport.is_some() + || self.active_ucs.is_some() } /// The document's saved model-space UCS (header), as a `Ucs`. `None` when it @@ -321,9 +320,51 @@ impl DocumentTab { } } + pub(super) fn ucs_from_layout(&self) -> Option { + let layout = self.scene.document.objects.values().find_map(|object| { + let acadrust::objects::ObjectType::Layout(layout) = object else { + return None; + }; + (layout.name == self.scene.current_layout).then_some(layout) + })?; + let mut ucs = self + .scene + .document + .ucss + .iter() + .find(|ucs| ucs.handle == layout.named_ucs) + .cloned() + .unwrap_or_else(|| Ucs::new("*PAPERUCS*")); + ucs.origin = acadrust::types::Vector3::new( + layout.ucs_origin.0, + layout.ucs_origin.1, + layout.ucs_origin.2, + ); + ucs.x_axis = acadrust::types::Vector3::new( + layout.ucs_x_axis.0, + layout.ucs_x_axis.1, + layout.ucs_x_axis.2, + ); + ucs.y_axis = acadrust::types::Vector3::new( + layout.ucs_y_axis.0, + layout.ucs_y_axis.1, + layout.ucs_y_axis.2, + ); + ucs.elevation = layout.elevation; + ucs.ortho_type = layout.ucs_ortho_type; + ucs.named_ucs_handle = layout.named_ucs; + ucs.base_ucs_handle = layout.base_ucs; + let is_world = layout.named_ucs.is_null() + && layout.base_ucs.is_null() + && layout.ucs_ortho_type == 0 + && layout.elevation.abs() <= f64::EPSILON + && super::helpers::UcsXform::from_ucs(&ucs).is_identity(); + (!is_world).then_some(ucs) + } + /// Set `active_ucs` to the UCS of the *current pane*: the entered viewport's - /// own per-viewport UCS, the model header UCS in the Model tab, or none in - /// plain paper space. Keeps the ViewCube in lock-step. Call on every pane + /// own per-viewport UCS, the model header UCS, or the layout UCS. Keeps the + /// ViewCube in lock-step. Call on every pane /// change (enter/exit viewport, layout / tab switch, load) so one field /// drives all UCS-aware systems regardless of where editing happens. pub(super) fn refresh_active_ucs(&mut self) { @@ -334,15 +375,52 @@ impl DocumentTab { } else if self.scene.current_layout == "Model" { self.model_ucs_from_header() } else { - None + self.ucs_from_layout() }; + if self.active_block_edit.is_none() + && self.scene.active_viewport.is_none() + && self.scene.current_layout != "Model" + { + self.sync_paper_ucs_header(); + } self.sync_ucs_to_scene(); } + fn sync_paper_ucs_header(&mut self) { + use acadrust::types::Vector3; + let header = &mut self.scene.document.header; + match &self.active_ucs { + Some(ucs) => { + header.paper_space_ucs_origin = ucs.origin; + header.paper_space_ucs_x_axis = ucs.x_axis; + header.paper_space_ucs_y_axis = ucs.y_axis; + header.paper_elevation = ucs.elevation; + header.paper_space_ucs_name = if (ucs.named_ucs_handle.is_valid() + || ucs.handle.is_valid()) + && !ucs.name.starts_with('*') + { + ucs.name.clone() + } else { + String::new() + }; + header.paper_ucs_ortho_ref = ucs.base_ucs_handle; + header.paper_ucs_ortho_view = ucs.ortho_type; + } + None => { + header.paper_space_ucs_origin = Vector3::ZERO; + header.paper_space_ucs_x_axis = Vector3::UNIT_X; + header.paper_space_ucs_y_axis = Vector3::UNIT_Y; + header.paper_elevation = 0.0; + header.paper_space_ucs_name.clear(); + header.paper_ucs_ortho_ref = Handle::NULL; + header.paper_ucs_ortho_view = 0; + } + } + } + /// Persist `active_ucs` back to its pane's storage so it round-trips: the /// entered viewport's per-viewport UCS fields, or the document header's - /// model-space UCS in the Model tab. No-op in plain paper space. Call after - /// any UCS change. + /// model-space UCS in the Model tab, or the layout UCS. Call after a change. pub(super) fn persist_active_ucs(&mut self) { use acadrust::types::Vector3; if let Some(index) = self.active_block_edit { @@ -385,6 +463,48 @@ impl DocumentTab { h.model_space_ucs_name.clear(); } } + } else { + let (origin, x_axis, y_axis, elevation, ortho, named, base) = + match &self.active_ucs { + Some(ucs) => ( + ucs.origin, + ucs.x_axis, + ucs.y_axis, + ucs.elevation, + ucs.ortho_type, + if ucs.named_ucs_handle.is_valid() { + ucs.named_ucs_handle + } else { + ucs.handle + }, + ucs.base_ucs_handle, + ), + None => ( + Vector3::ZERO, + Vector3::UNIT_X, + Vector3::UNIT_Y, + 0.0, + 0, + Handle::NULL, + Handle::NULL, + ), + }; + for object in self.scene.document.objects.values_mut() { + let acadrust::objects::ObjectType::Layout(layout) = object else { + continue; + }; + if layout.name == self.scene.current_layout { + layout.ucs_origin = (origin.x, origin.y, origin.z); + layout.ucs_x_axis = (x_axis.x, x_axis.y, x_axis.z); + layout.ucs_y_axis = (y_axis.x, y_axis.y, y_axis.z); + layout.elevation = elevation; + layout.ucs_ortho_type = ortho; + layout.named_ucs = named; + layout.base_ucs = base; + break; + } + } + self.sync_paper_ucs_header(); } } diff --git a/src/app/mod.rs b/src/app/mod.rs index d98a3958f..8324cf55d 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -810,16 +810,21 @@ pub(super) struct OpenCADStudio { /// Snapshot of the dialog's settings taken when it opened, restored by the /// `` list entry. plot_prev: Option, + /// Full source settings behind the fields currently shown in Plot. + plot_setup_template: Option, /// Paper layouts shown by Print All, in tab order with their selection. print_all_layouts: Vec<(String, bool)>, /// True while the Plot dialog is editing settings for Print All. print_all_options: bool, + /// True when Print All should override each layout's page setup. + print_all_settings_override: bool, /// Settings restored when the Print All options dialog is cancelled. print_all_options_prev: Option, /// Plot style restored together with cancelled Print All options. print_all_plot_style_prev: Option>, /// Plot window restored together with cancelled Print All options. print_all_plot_window_prev: Option>, + print_all_plot_setup_prev: Option>, // ── Plot Style Table ────────────────────────────────────────────────── /// Currently loaded CTB/STB table (None = no override). @@ -3287,11 +3292,14 @@ impl OpenCADStudio { plot_orientation: crate::io::paper_sizes::Orientation::Landscape, plot_dialog: crate::ui::window::plot::PlotDialogState::default(), plot_prev: None, + plot_setup_template: None, print_all_layouts: Vec::new(), print_all_options: false, + print_all_settings_override: false, print_all_options_prev: None, print_all_plot_style_prev: None, print_all_plot_window_prev: None, + print_all_plot_setup_prev: None, opening: None, open_job_serial: 0, recovery_report: None, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 3092b6d90..1f77d4bf7 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -120,6 +120,32 @@ type ClippedPlotParams = ( Option<(f32, f32, f32, f32)>, ); +fn plot_dialog_sheet_mm(d: &crate::ui::window::plot::PlotDialogState) -> (f64, f64) { + use crate::io::paper_sizes::{sheet_mm, Orientation, PaperSize}; + let orientation = if d.orientation == "Portrait" { + Orientation::Portrait + } else { + Orientation::Landscape + }; + let standard = match d.paper.as_str() { + "A3" => Some(PaperSize::A3), + "A2" => Some(PaperSize::A2), + "A1" => Some(PaperSize::A1), + "A0" => Some(PaperSize::A0), + "A4" => Some(PaperSize::A4), + _ => None, + }; + if let Some(paper) = standard { + return sheet_mm(paper, orientation); + } + let short = d.paper_width_mm.min(d.paper_height_mm).max(1.0); + let long = d.paper_width_mm.max(d.paper_height_mm).max(1.0); + match orientation { + Orientation::Portrait => (short, long), + Orientation::Landscape => (long, short), + } +} + fn plot_content_extents( wires: &[crate::io::pdf_export::PlotWire], hatches: &[crate::scene::model::hatch_model::HatchModel], @@ -169,7 +195,18 @@ fn plot_scene_content( crate::io::pdf_export::PlotGroupSplits, ) { let (mut paper_wires, mut model_wires) = scene.plot_wire_groups(render_mode_override); - paper_wires.retain(|wire| wire.plot_visible); + let plot_viewport_borders = scene + .effective_plot_settings() + .is_none_or(|settings| settings.flags.plot_viewport_borders); + paper_wires.retain(|wire| { + wire.plot_visible + && (plot_viewport_borders + || !crate::scene::Scene::handle_from_wire_name(&wire.name) + .and_then(|handle| scene.document.get_entity(handle)) + .is_some_and(|entity| { + matches!(entity, acadrust::EntityType::Viewport(viewport) if crate::scene::Scene::is_content_viewport(viewport)) + })) + }); model_wires.retain(|wire| wire.plot_visible); let with_depth = |wires: Vec| { let depths = scene.plot_wire_depths(&wires); @@ -1364,6 +1401,8 @@ pub(super) fn on_open_file(&mut self) -> Task { .nth(1) .unwrap_or_else(|| "Model".to_string()), }; + self.tabs[i].scene.load_current_layout_state(); + self.tabs[i].refresh_active_ucs(); } // Object isolation is session-only. A newly opened drawing must // not inherit the previous tab's filter, and persisted entity @@ -2662,8 +2701,7 @@ pub(super) fn on_open_file(&mut self) -> Task { iced::exit() } - /// Write the given plot page settings into the active layout's Layout + - /// PlotSettings objects (paper size, plot area, offset, rotation, scale). + /// Write the given plot page settings into the active layout. /// No-op on the Model tab (which has no paper layout). Marks the tab dirty /// and re-tessellates the sheet. Called by the Plot dialog's Set current action. #[allow(clippy::too_many_arguments)] @@ -2684,118 +2722,97 @@ pub(super) fn on_open_file(&mut self) -> Task { if layout_name != "Model" { let w: f64 = w.max(1.0); let h: f64 = h.max(1.0); + use acadrust::objects::{ + PlotRotation, PlotSettings, PlotType, ScaledType, ShadePlotMode, + ShadePlotResolutionLevel, + }; + let mut ps = self + .plot_setup_template + .clone() + .or_else(|| self.tabs[i].scene.plot_settings_for(&layout_name)) + .unwrap_or_else(|| PlotSettings::new("")); + ps.paper_width = w; + ps.paper_height = h; + ps.paper_size = dialog.paper.clone(); + ps.plot_type = match plot_area { + "Window" => PlotType::Window, + "Display" => PlotType::LastScreenDisplay, + "Extents" => PlotType::Extents, + "Limits" => PlotType::Limits, + area if area.starts_with("View: ") => PlotType::View, + _ => PlotType::Layout, + }; + ps.plot_view_name = plot_area + .strip_prefix("View: ") + .unwrap_or("") + .to_string(); + if plot_area == "Window" { + if let Some((x0, y0, x1, y1)) = plot_window { + ps.set_plot_window(x0, y0, x1, y1); + } + } + ps.flags.plot_centered = center && plot_area != "Layout"; + ps.origin_x = if plot_area == "Layout" { 0.0 } else { offset_x }; + ps.origin_y = if plot_area == "Layout" { 0.0 } else { offset_y }; + ps.rotation = match rotation { + 90 => PlotRotation::Degrees90, + 180 => PlotRotation::Degrees180, + 270 => PlotRotation::Degrees270, + _ => PlotRotation::None, + }; + if dialog.fit_to_paper && plot_area != "Layout" { + ps.set_scale_to_fit(); + } else if plot_area == "Layout" { + ps.set_standard_scale(ScaledType::OneToOne); + ps.standard_scale_factor = 1.0; + } else { + let factor = plot_dialog_scale_factor(&dialog); + ps.scale_type = ScaledType::CustomScale; + ps.scale_numerator = factor; + ps.scale_denominator = 1.0; + ps.standard_scale_factor = factor; + ps.flags.use_standard_scale = false; + } + ps.printer_name = if dialog.to_file { + crate::ui::window::plot::OUT_PDF.into() + } else { + dialog.printer.clone().unwrap_or_default() + }; + ps.current_style_sheet = dialog.style_name.clone(); + ps.flags.scale_lineweights = dialog.scale_lw; + ps.flags.print_lineweights = dialog.lineweights; + ps.flags.plot_plot_styles = dialog.apply_plot_styles && !dialog.style_name.is_empty(); + ps.flags.show_plot_styles = dialog.show_plot_styles && !dialog.style_name.is_empty(); + ps.flags.draw_viewports_first = dialog.paperspace_last; + ps.flags.plot_hidden = dialog.shade == "Hidden Line"; + ps.shade_plot_mode = match dialog.shade.as_str() { + "2D Wireframe" | "3D Wireframe" => ShadePlotMode::Wireframe, + "Hidden Line" => ShadePlotMode::Hidden, + "As displayed" => ShadePlotMode::AsDisplayed, + _ => ShadePlotMode::Rendered, + }; + ps.shade_plot_resolution = match dialog.quality.as_str() { + "Low" => ShadePlotResolutionLevel::Draft, + "High" => ShadePlotResolutionLevel::Presentation, + _ => ShadePlotResolutionLevel::Normal, + }; + ps.shade_plot_dpi = 300; + + self.tabs[i] + .scene + .set_layout_plot_settings(&layout_name, &ps); for obj in self.tabs[i].scene.document.objects.values_mut() { - if let acadrust::objects::ObjectType::Layout(l) = obj { - if l.name == layout_name { - l.min_limits = (0.0, 0.0); - l.max_limits = (w, h); - l.min_extents = (0.0, 0.0, 0.0); - l.max_extents = (w, h, 0.0); - l.paper_width = w; - l.paper_height = h; - l.plot_rotation = 0; - l.plot_paper_units = 1; - l.plot_origin_x = offset_x; - l.plot_origin_y = offset_y; - l.paper_size = String::new(); + if let acadrust::objects::ObjectType::Layout(layout) = obj { + if layout.name == layout_name { + layout.min_limits = (0.0, 0.0); + layout.max_limits = (w, h); + layout.min_extents = (0.0, 0.0, 0.0); + layout.max_extents = (w, h, 0.0); break; } } } - use acadrust::objects::{ - ObjectType, PlotPaperUnits, PlotRotation, PlotSettings, PlotType, ScaledType, - ShadePlotMode, ShadePlotResolutionLevel, - }; - let plot_handle = self.tabs[i] - .scene - .document - .objects - .iter() - .find_map(|(h, obj)| { - if let ObjectType::PlotSettings(ps) = obj { - (ps.page_name == layout_name).then_some(*h) - } else { - None - } - }); - - let ps_entry = if let Some(h) = plot_handle { - self.tabs[i].scene.document.objects.get_mut(&h) - } else { - let mut ps = PlotSettings::new(layout_name.clone()); - ps.handle = self.tabs[i].scene.document.allocate_handle(); - let h = ps.handle; - self.tabs[i] - .scene - .document - .objects - .insert(h, ObjectType::PlotSettings(ps)); - self.tabs[i].scene.document.objects.get_mut(&h) - }; - - if let Some(ObjectType::PlotSettings(ps)) = ps_entry { - ps.paper_width = w; - ps.paper_height = h; - ps.paper_size = dialog.paper.clone(); - ps.paper_units = PlotPaperUnits::Millimeters; - ps.plot_type = match plot_area { - "Window" => PlotType::Window, - "Display" => PlotType::LastScreenDisplay, - "Extents" => PlotType::Extents, - _ => PlotType::Layout, - }; - if plot_area == "Window" { - if let Some((x0, y0, x1, y1)) = plot_window { - ps.set_plot_window(x0, y0, x1, y1); - } - } - ps.flags.plot_centered = center && plot_area != "Layout"; - ps.origin_x = if plot_area == "Layout" { 0.0 } else { offset_x }; - ps.origin_y = if plot_area == "Layout" { 0.0 } else { offset_y }; - ps.rotation = match rotation { - 90 => PlotRotation::Degrees90, - 180 => PlotRotation::Degrees180, - 270 => PlotRotation::Degrees270, - _ => PlotRotation::None, - }; - if dialog.fit_to_paper && plot_area != "Layout" { - ps.set_scale_to_fit(); - } else { - let factor = if plot_area == "Layout" { - 1.0 - } else { - plot_dialog_scale_factor(&dialog) - }; - ps.scale_type = ScaledType::CustomScale; - ps.scale_numerator = factor; - ps.scale_denominator = 1.0; - } - ps.printer_name = if dialog.to_file { - crate::ui::window::plot::OUT_PDF.into() - } else { - dialog.printer.clone().unwrap_or_default() - }; - ps.current_style_sheet = dialog.style_name.clone(); - ps.flags.scale_lineweights = dialog.scale_lw; - ps.flags.print_lineweights = dialog.lineweights; - ps.flags.plot_plot_styles = !dialog.style_name.is_empty(); - ps.flags.draw_viewports_first = dialog.paperspace_last; - ps.flags.plot_hidden = false; - ps.shade_plot_mode = match dialog.shade.as_str() { - "2D Wireframe" | "3D Wireframe" => ShadePlotMode::Wireframe, - "Hidden Line" => ShadePlotMode::Hidden, - "As displayed" => ShadePlotMode::AsDisplayed, - _ => ShadePlotMode::Rendered, - }; - ps.shade_plot_resolution = match dialog.quality.as_str() { - "Low" => ShadePlotResolutionLevel::Draft, - "High" => ShadePlotResolutionLevel::Presentation, - _ => ShadePlotResolutionLevel::Normal, - }; - ps.shade_plot_dpi = 300; - } - self.tabs[i].dirty = true; self.tabs[i].scene.bump_geometry_no_blocks(); self.command_line.push_info(crate::tf!( @@ -2809,6 +2826,19 @@ pub(super) fn on_open_file(&mut self) -> Task { &mut self, path: std::path::PathBuf, ) -> Task { + let i = self.active_tab; + if self.tabs[i].scene.current_layout != "Model" { + self.plot_dialog.paper_space = true; + self.plot_dialog.scales = self.tabs[i] + .scene + .scale_list() + .into_iter() + .map(|(name, _, factor)| (name, factor)) + .collect(); + if let Some(settings) = self.tabs[i].scene.effective_plot_settings() { + self.load_plotsettings_into_dialog(&settings); + } + } let Some((wires, hatches, wipeouts, group_splits, page_w, page_h, ox, oy, rotation, scale, clip)) = self.direct_plot_params() else { @@ -2911,6 +2941,7 @@ pub(super) fn on_open_file(&mut self) -> Task { .filter(|name| name != "Model") .map(|name| (name, true)) .collect(); + self.print_all_settings_override = false; self.active_modal = Some(crate::app::ModalKind::PrintAll); self.reset_modal_geometry(); Task::none() @@ -2920,10 +2951,12 @@ pub(super) fn on_open_file(&mut self) -> Task { let previous = self.plot_dialog.clone(); let previous_style = self.active_plot_style.clone(); let previous_window = self.plot_window; + let previous_setup = self.plot_setup_template.clone(); let task = self.on_plot_dialog_open(); self.print_all_options_prev = Some(previous); self.print_all_plot_style_prev = Some(previous_style); self.print_all_plot_window_prev = Some(previous_window); + self.print_all_plot_setup_prev = Some(previous_setup); self.print_all_options = true; self.plot_dialog.paper_space = true; self.plot_dialog.area = "Layout".into(); @@ -2945,47 +2978,145 @@ pub(super) fn on_open_file(&mut self) -> Task { let i = self.active_tab; let original_layout = self.tabs[i].scene.current_layout.clone(); let original_viewport = self.tabs[i].scene.active_viewport; - let dialog = self.plot_dialog.clone(); - let mut pages = Vec::with_capacity(selected.len()); - for name in selected { - // Plot helpers read the active layout. Swap only this transient - // selector so the drawing's saved active-space metadata is not - // touched while the owned page snapshot is collected. - { - let scene = &mut self.tabs[i].scene; - scene.current_layout = name; - scene.active_viewport = None; + let original_psltscale = self.tabs[i] + .scene + .document + .header + .paper_space_linetype_scaling; + let original_plimcheck = self.tabs[i] + .scene + .document + .header + .paper_space_limit_check; + let original_dialog = self.plot_dialog.clone(); + let original_style = self.active_plot_style.clone(); + let original_window = self.plot_window; + let original_setup = self.plot_setup_template.clone(); + let original_camera = self.tabs[i].scene.camera.borrow().clone(); + let original_camera_generation = self.tabs[i].scene.camera_generation; + let override_dialog = self.print_all_settings_override.then(|| original_dialog.clone()); + let override_style = self.print_all_settings_override.then(|| original_style.clone()); + let result = (|| { + let mut pages = Vec::with_capacity(selected.len()); + for name in selected { + let page_setup = self.tabs[i] + .scene + .plot_settings_for(&name) + .ok_or_else(|| format!("Layout '{name}' has no page setup."))?; + { + let scene = &mut self.tabs[i].scene; + scene.current_layout = name.clone(); + scene.active_viewport = None; + scene.load_current_layout_state(); + } + if let Some(dialog) = &override_dialog { + self.plot_dialog = dialog.clone(); + self.active_plot_style = override_style.clone().flatten(); + self.plot_window = original_window; + } else { + self.plot_dialog.paper_space = true; + self.plot_window = None; + self.load_plotsettings_into_dialog(&page_setup); + } + let dialog = self.plot_dialog.clone(); + if dialog.area == "Display" { + self.tabs[i].scene.restore_saved_camera(); + } + if dialog.style_missing && dialog.apply_plot_styles { + return Err(format!( + "Layout '{name}' plot style table '{}' is not loaded.", + dialog.style_name + )); + } + let plot_style = self.dialog_plot_style(&dialog); + let params = match dialog.area.as_str() { + "Display" => self.display_plot_job(), + "Extents" => self.extents_plot_job(), + "Limits" => self.limits_plot_job(), + "Window" => self.window_plot_job(), + area if area.starts_with("View: ") => { + self.named_view_plot_job(area.trim_start_matches("View: ")) + } + _ => None, + }; + let ( + wires, + hatches, + wipeouts, + group_splits, + paper_w, + paper_h, + offset_x, + offset_y, + rotation_deg, + scale, + clip, + ) = if dialog.area == "Layout" { + self.layout_plot_params_for("Layout") + } else { + let ( + wires, + hatches, + wipeouts, + group_splits, + paper_w, + paper_h, + offset_x, + offset_y, + rotation_deg, + scale, + clip, + ) = params.ok_or_else(|| format!("Layout '{name}' plot area is empty."))?; + ( + std::sync::Arc::new(wires), + hatches, + wipeouts, + group_splits, + paper_w, + paper_h, + offset_x, + offset_y, + rotation_deg, + scale, + clip, + ) + }; + pages.push(crate::io::pdf_export::PdfPageInput { + wires, + hatches, + wipeouts, + paper_w, + paper_h, + offset_x, + offset_y, + rotation_deg, + scale, + clip, + options: Self::pdf_plot_options(&dialog, group_splits), + plot_style, + }); } - let ( - wires, - hatches, - wipeouts, - group_splits, - paper_w, - paper_h, - offset_x, - offset_y, - rotation_deg, - scale, - clip, - ) = self.layout_plot_params_for("Layout"); - pages.push(crate::io::pdf_export::PdfPageInput { - wires, - hatches, - wipeouts, - paper_w, - paper_h, - offset_x, - offset_y, - rotation_deg, - scale, - clip, - options: Self::pdf_plot_options(&dialog, group_splits), - }); - } + Ok(pages) + })(); self.tabs[i].scene.current_layout = original_layout; self.tabs[i].scene.active_viewport = original_viewport; - Ok(pages) + self.tabs[i] + .scene + .document + .header + .paper_space_linetype_scaling = original_psltscale; + self.tabs[i] + .scene + .document + .header + .paper_space_limit_check = original_plimcheck; + self.plot_dialog = original_dialog; + self.active_plot_style = original_style; + self.plot_window = original_window; + self.plot_setup_template = original_setup; + *self.tabs[i].scene.camera.borrow_mut() = original_camera; + self.tabs[i].scene.camera_generation = original_camera_generation; + result } pub(super) fn on_print_all_pdf_path_some( @@ -2993,7 +3124,10 @@ pub(super) fn on_open_file(&mut self) -> Task { path: std::path::PathBuf, ) -> Task { let dialog = self.plot_dialog.clone(); - if dialog.style_missing { + if self.print_all_settings_override + && dialog.style_missing + && dialog.apply_plot_styles + { self.command_line.push_error(crate::tf!( "Plot style table '{}' is not loaded.", dialog.style_name @@ -3007,7 +3141,6 @@ pub(super) fn on_open_file(&mut self) -> Task { return Task::none(); } }; - let plot_style = self.dialog_plot_style(&dialog); let worker_path = path.clone(); self.save_config(); self.close_active_modal(); @@ -3015,7 +3148,7 @@ pub(super) fn on_open_file(&mut self) -> Task { crate::io::pdf_export::export_pdf_pages( &pages, &worker_path, - plot_style.as_ref(), + None, ) .map(|_| format!("Exported {} layouts to {}", pages.len(), worker_path.display())) .map_err(|error| format!("Export failed: {error}")) @@ -3034,7 +3167,10 @@ pub(super) fn on_open_file(&mut self) -> Task { #[cfg(not(target_arch = "wasm32"))] { let dialog = self.plot_dialog.clone(); - if dialog.style_missing { + if self.print_all_settings_override + && dialog.style_missing + && dialog.apply_plot_styles + { self.command_line.push_error(crate::tf!( "Plot style table '{}' is not loaded.", dialog.style_name @@ -3048,7 +3184,6 @@ pub(super) fn on_open_file(&mut self) -> Task { return Task::none(); } }; - let plot_style = self.dialog_plot_style(&dialog); let options = self.plot_print_options(&dialog, Default::default()); let temp_path = crate::io::print_to_printer::temp_pdf_path("print_all"); self.save_config(); @@ -3060,7 +3195,7 @@ pub(super) fn on_open_file(&mut self) -> Task { crate::io::pdf_export::export_pdf_pages( &pages, &temp_path, - plot_style.as_ref(), + None, ) .and_then(|_| { crate::io::print_to_printer::print_existing_pdf(&temp_path, &options) @@ -3112,23 +3247,10 @@ pub(super) fn on_open_file(&mut self) -> Task { } fn layout_plot_params_for(&self, plot_area: &str) -> LayoutPlotParams { - use crate::io::paper_sizes::{sheet_mm, Orientation, PaperSize}; let i = self.active_tab; let scene = &self.tabs[i].scene; let paper_space = scene.current_layout != "Model"; - let selected_paper = match self.plot_dialog.paper.as_str() { - "A3" => PaperSize::A3, - "A2" => PaperSize::A2, - "A1" => PaperSize::A1, - "A0" => PaperSize::A0, - _ => PaperSize::A4, - }; - let selected_orientation = if self.plot_dialog.orientation == "Portrait" { - Orientation::Portrait - } else { - Orientation::Landscape - }; - let selected_sheet = sheet_mm(selected_paper, selected_orientation); + let selected_sheet = plot_dialog_sheet_mm(&self.plot_dialog); let (source_wires, hatches, wipeouts, mut group_splits) = plot_scene_content( scene, @@ -3463,14 +3585,21 @@ pub(super) fn on_open_file(&mut self) -> Task { let one_to_one = scale_name_for_factor(&scales, 1.0) .or_else(|| scales.first().map(|(name, _)| name.clone())) .unwrap_or_else(|| "1:1".into()); - // Keep the user's last print preferences (printer, copies, quality, - // output options — persisted in `self.plot_dialog`); only refresh the - // runtime printer list and reseed the drawing-specific fields from the - // active layout. + let paper_space = self.tabs[self.active_tab].scene.current_layout != "Model"; + let plot_views = self.tabs[self.active_tab] + .scene + .document + .views + .iter() + .filter(|view| view.paper_space == paper_space) + .map(|view| view.name.clone()) + .collect(); + // Keep session-only choices while loading drawing fields from the layout. let d = &mut self.plot_dialog; d.printers = crate::io::print_to_printer::list_printers(); d.plot_styles = crate::io::plot_style::available_ctb_names(); d.scales = scales; + d.plot_views = plot_views; if d.scale.eq_ignore_ascii_case("fit") { d.fit_to_paper = true; d.scale_lw = false; @@ -3478,7 +3607,7 @@ pub(super) fn on_open_file(&mut self) -> Task { } else if !d.scales.iter().any(|(name, _)| name == &d.scale) { d.scale = one_to_one.clone(); } - d.paper_space = self.tabs[self.active_tab].scene.current_layout != "Model"; + d.paper_space = paper_space; d.paper = self.plot_format.label().to_string(); d.orientation = match self.plot_orientation { Orientation::Portrait => "Portrait", @@ -3510,32 +3639,24 @@ pub(super) fn on_open_file(&mut self) -> Task { .map(|t| t.name.clone()) .unwrap_or_default(); d.style_missing = false; - // `area`, scale and fit are remembered user choices, - // not reseeded from the layout. Offset / center / rotation ARE layout - // properties, so reflect them. - if let Some(ps) = self.tabs[self.active_tab].scene.effective_plot_settings() { - let d = &mut self.plot_dialog; - d.center = ps.flags.plot_centered; - d.offset_x = format!("{:.2}", ps.origin_x); - d.offset_y = format!("{:.2}", ps.origin_y); - let deg = ps.rotation.to_degrees() as i32; - d.upside_down = matches!(deg, 180 | 270); - } self.plot_dialog.name_input = None; self.plot_dialog.name_rename = false; if self.plot_dialog.fit_to_paper { self.plot_dialog.scale_lw = false; } - // Refresh document/runtime lists, then restore the live choices from - // the preceding dialog session. Previously the snapshot happened - // after the layout values above were reloaded, so opening Plot itself - // destroyed the user's last paper, area, scale and output choices. self.refresh_page_setups(); self.plot_prev = Some(previous); - self.select_page_setup(crate::ui::window::plot::SETUP_PREV); - // `` may come from another tab/drawing whose custom scale is - // not present in this document. Validate again after restoring it; the - // pre-restore validation above only saw the temporarily reseeded state. + let cur = self.tabs[self.active_tab].scene.current_layout.clone(); + let layout_entry = format!("*{cur}*"); + if self.tabs[self.active_tab] + .scene + .plot_settings_for(&cur) + .is_some() + { + self.select_page_setup(&layout_entry); + } else { + self.select_page_setup(crate::ui::window::plot::SETUP_PREV); + } if self.plot_dialog.scale.eq_ignore_ascii_case("fit") { self.plot_dialog.fit_to_paper = true; self.plot_dialog.scale = one_to_one.clone(); @@ -3550,10 +3671,7 @@ pub(super) fn on_open_file(&mut self) -> Task { if self.plot_dialog.fit_to_paper { self.plot_dialog.scale_lw = false; } - // A model-space plot cannot use the paper-only Layout area. Other - // values remain exactly as the user left them; selecting a layout or - // named setup explicitly still reloads that setup below. - let cur = self.tabs[self.active_tab].scene.current_layout.clone(); + // A model-space plot cannot use the paper-only Layout area. if cur == "Model" { if self.plot_dialog.area == "Layout" { self.plot_dialog.area = "Window".into(); @@ -3608,10 +3726,16 @@ pub(super) fn on_open_file(&mut self) -> Task { } M::Paper(s) => { self.plot_dialog.paper = s; + let (w, h) = plot_dialog_sheet_mm(&self.plot_dialog); + self.plot_dialog.paper_width_mm = w; + self.plot_dialog.paper_height_mm = h; Task::none() } M::Orientation(s) => { self.plot_dialog.orientation = s; + let (w, h) = plot_dialog_sheet_mm(&self.plot_dialog); + self.plot_dialog.paper_width_mm = w; + self.plot_dialog.paper_height_mm = h; Task::none() } M::Area(s) => { @@ -3681,6 +3805,12 @@ pub(super) fn on_open_file(&mut self) -> Task { PlotFlag::ScaleLw if !d.fit_to_paper => { d.scale_lw = !d.scale_lw } + PlotFlag::PlotStyles if !d.style_name.is_empty() => { + d.apply_plot_styles = !d.apply_plot_styles + } + PlotFlag::DisplayStyles if d.paper_space && !d.style_name.is_empty() => { + d.show_plot_styles = !d.show_plot_styles + } PlotFlag::UpsideDown => { d.upside_down = !d.upside_down; } @@ -3700,11 +3830,14 @@ pub(super) fn on_open_file(&mut self) -> Task { if name == STYLE_NONE { self.active_plot_style = None; self.plot_dialog.style_name.clear(); + self.plot_dialog.apply_plot_styles = false; + self.plot_dialog.show_plot_styles = false; self.plot_dialog.style_missing = false; } else { match crate::io::plot_style::PlotStyleTable::load_named(&name) { Ok(table) => { self.plot_dialog.style_name = table.name.clone(); + self.plot_dialog.apply_plot_styles = true; self.plot_dialog.style_missing = false; self.active_plot_style = Some(table); } @@ -3852,7 +3985,7 @@ pub(super) fn on_open_file(&mut self) -> Task { } M::Preview => self.on_plot_dlg_commit(true), M::Commit if self.print_all_options => { - if self.plot_dialog.style_missing { + if self.plot_dialog.style_missing && self.plot_dialog.apply_plot_styles { self.command_line.push_error(crate::tf!( "Plot style table '{}' is not loaded.", self.plot_dialog.style_name @@ -3863,9 +3996,11 @@ pub(super) fn on_open_file(&mut self) -> Task { self.plot_dialog.area = "Layout".into(); self.sync_dialog_plot_runtime(); self.save_config(); + self.print_all_settings_override = true; self.print_all_options = false; self.print_all_options_prev = None; self.print_all_plot_style_prev = None; + self.print_all_plot_setup_prev = None; if let Some(previous) = self.print_all_plot_window_prev.take() { self.plot_window = previous; } @@ -3894,12 +4029,15 @@ pub(super) fn on_open_file(&mut self) -> Task { use crate::ui::window::plot::{SETUP_NONE, SETUP_PREV}; self.plot_dialog.selected_setup = name.to_string(); if name == SETUP_NONE { + self.plot_setup_template = None; // No page setup: default geometry + PDF output. let is_model = self.tabs[self.active_tab].scene.current_layout == "Model"; let d = &mut self.plot_dialog; d.to_file = true; d.paper = "A4".into(); d.orientation = "Landscape".into(); + d.paper_width_mm = 297.0; + d.paper_height_mm = 210.0; d.area = if is_model { "Window".into() } else { "Layout".into() }; d.center = true; d.offset_x = "0.0".into(); @@ -3947,32 +4085,32 @@ pub(super) fn on_open_file(&mut self) -> Task { PlotPaperUnits, PlotRotation, PlotSettings, PlotType, ScaledType, ShadePlotMode, ShadePlotResolutionLevel, }; - use crate::io::paper_sizes::{sheet_mm, Orientation, PaperSize}; let d = &self.plot_dialog; - let paper = match d.paper.as_str() { - "A3" => PaperSize::A3, - "A2" => PaperSize::A2, - "A1" => PaperSize::A1, - "A0" => PaperSize::A0, - _ => PaperSize::A4, + let (w, h) = plot_dialog_sheet_mm(d); + let mut ps = match self.plot_setup_template.clone() { + Some(settings) => settings, + None => { + let mut settings = PlotSettings::new(""); + settings.paper_units = PlotPaperUnits::Millimeters; + settings + } }; - let orient = if d.orientation == "Portrait" { - Orientation::Portrait - } else { - Orientation::Landscape - }; - let (w, h) = sheet_mm(paper, orient); - let mut ps = PlotSettings::new(""); ps.paper_width = w; ps.paper_height = h; ps.paper_size = d.paper.clone(); - ps.paper_units = PlotPaperUnits::Millimeters; ps.plot_type = match d.area.as_str() { "Window" => PlotType::Window, "Layout" => PlotType::Layout, "Display" => PlotType::LastScreenDisplay, + "Limits" => PlotType::Limits, + area if area.starts_with("View: ") => PlotType::View, _ => PlotType::Extents, }; + ps.plot_view_name = d + .area + .strip_prefix("View: ") + .unwrap_or("") + .to_string(); if d.area == "Window" { if let Some((x0, y0, x1, y1)) = self.plot_window { ps.set_plot_window(x0, y0, x1, y1); @@ -3998,6 +4136,7 @@ pub(super) fn on_open_file(&mut self) -> Task { }; if d.area == "Layout" { ps.set_standard_scale(ScaledType::OneToOne); + ps.standard_scale_factor = 1.0; } else if d.fit_to_paper { ps.set_scale_to_fit(); } else { @@ -4005,6 +4144,8 @@ pub(super) fn on_open_file(&mut self) -> Task { ps.scale_type = ScaledType::CustomScale; ps.scale_numerator = factor; ps.scale_denominator = 1.0; + ps.standard_scale_factor = factor; + ps.flags.use_standard_scale = false; } ps.printer_name = if d.to_file { crate::ui::window::plot::OUT_PDF.into() @@ -4014,9 +4155,10 @@ pub(super) fn on_open_file(&mut self) -> Task { ps.current_style_sheet = d.style_name.clone(); ps.flags.scale_lineweights = d.scale_lw; ps.flags.print_lineweights = d.lineweights; - ps.flags.plot_plot_styles = !d.style_name.is_empty(); + ps.flags.plot_plot_styles = d.apply_plot_styles && !d.style_name.is_empty(); + ps.flags.show_plot_styles = d.show_plot_styles && !d.style_name.is_empty(); ps.flags.draw_viewports_first = d.paperspace_last; - ps.flags.plot_hidden = false; + ps.flags.plot_hidden = d.shade == "Hidden Line"; ps.shade_plot_mode = match d.shade.as_str() { "2D Wireframe" | "3D Wireframe" => ShadePlotMode::Wireframe, "Hidden Line" => ShadePlotMode::Hidden, @@ -4037,6 +4179,7 @@ pub(super) fn on_open_file(&mut self) -> Task { use acadrust::objects::{ PlotType, ShadePlotMode, ShadePlotResolutionLevel, }; + self.plot_setup_template = Some(ps.clone()); if matches!(ps.plot_type, PlotType::Window) && !ps.plot_window.is_empty() { self.plot_window = Some(( ps.plot_window.lower_left_x, @@ -4059,7 +4202,7 @@ pub(super) fn on_open_file(&mut self) -> Task { } let active_style_name = self.active_plot_style.as_ref().map(|table| table.name.clone()); let style_name = if ps.current_style_sheet.is_empty() { - active_style_name.clone().unwrap_or_default() + String::new() } else if active_style_name .as_deref() .is_some_and(|name| name.eq_ignore_ascii_case(&ps.current_style_sheet)) @@ -4072,17 +4215,30 @@ pub(super) fn on_open_file(&mut self) -> Task { && active_style_name .as_deref() .is_some_and(|name| name.eq_ignore_ascii_case(&style_name)); - let (paper, orient) = paper_label_from_dims(ps.paper_width, ps.paper_height); + let (mut paper, orient) = paper_label_from_dims(ps.paper_width, ps.paper_height); + if !matches!(paper.as_str(), "A4" | "A3" | "A2" | "A1" | "A0") + && !ps.paper_size.is_empty() + { + paper = ps.paper_size.clone(); + } let d = &mut self.plot_dialog; d.paper = paper; + d.paper_width_mm = ps.paper_width.max(1.0); + d.paper_height_mm = ps.paper_height.max(1.0); d.orientation = orient; d.area = match ps.plot_type { - PlotType::Window => "Window", - PlotType::Layout => "Layout", - PlotType::LastScreenDisplay => "Display", - _ => "Extents", - } - .to_string(); + PlotType::Window => "Window".to_string(), + PlotType::Layout => "Layout".to_string(), + PlotType::LastScreenDisplay => "Display".to_string(), + PlotType::Limits => "Limits".to_string(), + PlotType::View if !ps.plot_view_name.is_empty() => { + d.plot_views.push(ps.plot_view_name.clone()); + d.plot_views.sort(); + d.plot_views.dedup(); + format!("View: {}", ps.plot_view_name) + } + _ => "Extents".to_string(), + }; if !d.paper_space && d.area == "Layout" { d.area = "Extents".into(); } @@ -4102,6 +4258,12 @@ pub(super) fn on_open_file(&mut self) -> Task { d.fit_to_paper = d.area != "Layout" && ps.is_scale_to_fit(); let target_factor = if d.area == "Layout" { 1.0 + } else if ps.flags.use_standard_scale { + if ps.standard_scale_factor.is_finite() && ps.standard_scale_factor > 0.0 { + ps.standard_scale_factor + } else { + ps.scale_type.scale_factor() + } } else if ps.scale_denominator.abs() > 1e-9 { ps.scale_numerator / ps.scale_denominator } else { @@ -4116,11 +4278,15 @@ pub(super) fn on_open_file(&mut self) -> Task { d.scale_lw = ps.flags.scale_lineweights && !d.fit_to_paper; d.lineweights = ps.flags.print_lineweights; d.paperspace_last = ps.flags.draw_viewports_first; - d.shade = match ps.shade_plot_mode { - ShadePlotMode::Wireframe => "2D Wireframe", - ShadePlotMode::Hidden => "Hidden Line", - ShadePlotMode::Rendered => "Gouraud Shaded", - ShadePlotMode::AsDisplayed => "As displayed", + d.shade = if ps.flags.plot_hidden { + "Hidden Line" + } else { + match ps.shade_plot_mode { + ShadePlotMode::Wireframe => "2D Wireframe", + ShadePlotMode::Hidden => "Hidden Line", + ShadePlotMode::Rendered => "Gouraud Shaded", + ShadePlotMode::AsDisplayed => "As displayed", + } } .into(); d.quality = match ps.shade_plot_resolution { @@ -4139,6 +4305,8 @@ pub(super) fn on_open_file(&mut self) -> Task { d.printer = (!ps.printer_name.is_empty()).then(|| ps.printer_name.clone()); } d.style_name = style_name; + d.apply_plot_styles = ps.flags.plot_plot_styles; + d.show_plot_styles = ps.flags.show_plot_styles; d.style_missing = !d.style_name.is_empty() && !style_loaded; } @@ -4164,10 +4332,9 @@ pub(super) fn on_open_file(&mut self) -> Task { } fn apply_dialog_to_layout(&mut self) { - use crate::io::paper_sizes::sheet_mm; self.sync_dialog_plot_runtime(); let d = self.plot_dialog.clone(); - let (sheet_w, sheet_h) = sheet_mm(self.plot_format, self.plot_orientation); + let (sheet_w, sheet_h) = plot_dialog_sheet_mm(&d); let rotation: i16 = if d.upside_down { 180 } else { 0 }; let off_x = d.offset_x.parse::().unwrap_or(0.0); let off_y = d.offset_y.parse::().unwrap_or(0.0); @@ -4185,7 +4352,7 @@ pub(super) fn on_open_file(&mut self) -> Task { /// 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 { let d = self.plot_dialog.clone(); - if d.style_missing { + if d.style_missing && d.apply_plot_styles { self.command_line.push_error(crate::tf!( "Plot style table '{}' is not loaded.", d.style_name @@ -4203,11 +4370,16 @@ pub(super) fn on_open_file(&mut self) -> Task { let plot_style = self.dialog_plot_style(&d); // Extents, Window and Display use one plot path in both spaces. Only // Paper-space Layout is special: it uses the physical sheet bounds. - if matches!(d.area.as_str(), "Extents" | "Window" | "Display") { + if d.area != "Layout" { let job = match d.area.as_str() { "Display" => self.display_plot_job(), "Extents" => self.extents_plot_job(), - _ => self.window_plot_job(), + "Limits" => self.limits_plot_job(), + "Window" => self.window_plot_job(), + area if area.starts_with("View: ") => { + self.named_view_plot_job(area.trim_start_matches("View: ")) + } + _ => None, }; let Some(( w_wires, @@ -4341,7 +4513,7 @@ pub(super) fn on_open_file(&mut self) -> Task { &self, d: &crate::ui::window::plot::PlotDialogState, ) -> Option { - if d.style_name.is_empty() || d.style_missing { + if d.style_name.is_empty() || d.style_missing || !d.apply_plot_styles { return None; } self.active_plot_style @@ -4358,6 +4530,34 @@ pub(super) fn on_open_file(&mut self) -> Task { self.area_plot_job(self.display_plot_window()?) } + fn limits_plot_job(&self) -> Option { + 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 { + 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 { let scene = &self.tabs[self.active_tab].scene; if scene.current_layout == "Model" { @@ -4446,13 +4646,13 @@ pub(super) fn on_open_file(&mut self) -> Task { /// Render a selected rectangle through one shared Model/Paper path. The /// window may lie partly or wholly outside a paper sheet. fn area_plot_job(&self, window: (f64, f64, f64, f64)) -> Option { - use crate::io::paper_sizes::{sheet_mm, window_to_sheet, PlotScale}; + use crate::io::paper_sizes::{window_to_sheet, PlotScale}; let i = self.active_tab; let (x0, y0, x1, y1) = window; if (x1 - x0) < 1e-6 || (y1 - y0) < 1e-6 { return None; } - let (sheet_w, sheet_h) = sheet_mm(self.plot_format, self.plot_orientation); + let (sheet_w, sheet_h) = plot_dialog_sheet_mm(&self.plot_dialog); let win_w = (x1 - x0).max(1e-9); let win_h = (y1 - y0).max(1e-9); let scale_sel = if self.plot_dialog.fit_to_paper { diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 67f37af0c..a98bb6762 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -151,6 +151,9 @@ impl OpenCADStudio { if let Some(previous) = self.print_all_plot_window_prev.take() { self.plot_window = previous; } + if let Some(previous) = self.print_all_plot_setup_prev.take() { + self.plot_setup_template = previous; + } self.print_all_options = false; self.active_modal = Some(PrintAll); self.reset_modal_geometry(); @@ -6917,6 +6920,9 @@ impl OpenCADStudio { self.plot_dialog.style_missing = false; self.plot_dialog.plot_styles = crate::io::plot_style::available_ctb_names(); + self.tabs[self.active_tab] + .scene + .invalidate_display_plot_style(); self.command_line.push_output( crate::tf!( @@ -6968,6 +6974,9 @@ impl OpenCADStudio { self.plot_dialog.style_missing = false; self.plot_dialog.plot_styles = crate::io::plot_style::available_ctb_names(); + self.tabs[self.active_tab] + .scene + .invalidate_display_plot_style(); self.command_line.push_output(crate::tf!( "Plot style table saved to \"{}\".", path.display() diff --git a/src/app/update/util.rs b/src/app/update/util.rs index 3a01d3945..8779ffc2f 100644 --- a/src/app/update/util.rs +++ b/src/app/update/util.rs @@ -62,8 +62,7 @@ pub(super) fn layout_entry_name(s: &str) -> &str { s.trim_start_matches('*').trim_end_matches('*') } -/// Infer the closest A-series paper label and orientation from sheet -/// dimensions (mm). Falls back to A4 when nothing is close. +/// Infer an A-series label when dimensions match; otherwise retain the size. pub(super) fn paper_label_from_dims(w: f64, h: f64) -> (String, String) { use crate::io::paper_sizes::PaperSize; let orient = if w >= h { "Landscape" } else { "Portrait" }; @@ -76,7 +75,12 @@ pub(super) fn paper_label_from_dims(w: f64, h: f64) -> (String, String) { best = (p.label().to_string(), err); } } - (best.0, orient.to_string()) + let label = if best.1 <= 2.0 { + best.0 + } else { + format!("{w:.2} × {h:.2} mm") + }; + (label, orient.to_string()) } pub(super) fn parse_plot_scale(s: &str) -> (f64, f64) { diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index da0615fe1..a527895b9 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -5293,8 +5293,7 @@ impl OpenCADStudio { self.tabs[i].scene.set_current_layout(name.clone()); self.tabs[i].scene.deselect_all(); let switch_ms = perf_phase.elapsed().as_secs_f64() * 1000.0; - // UCS follows the pane: model header UCS in the Model tab, none - // in plain paper space (a viewport's UCS is adopted on entry). + // UCS follows the active model, layout, or floating viewport pane. let perf_phase = Instant::now(); self.tabs[i].refresh_active_ucs(); self.tabs[i].scene.restore_saved_camera(); @@ -5358,10 +5357,28 @@ impl OpenCADStudio { self.push_undo_snapshot(i, "LAYOUT"); match self.tabs[i].scene.document.add_layout(&new_name) { Ok(_) => { - // Override the acadrust default limits (12×9 imperial) with A4 landscape. + let layout_flags = i16::from( + self.tabs[i] + .scene + .document + .header + .paper_space_linetype_scaling, + ) | (i16::from( + self.tabs[i] + .scene + .document + .header + .paper_space_limit_check, + ) << 1); + let plot_style = self + .active_plot_style + .as_ref() + .map(|style| style.name.clone()) + .unwrap_or_default(); for obj in self.tabs[i].scene.document.objects.values_mut() { if let acadrust::objects::ObjectType::Layout(l) = obj { if l.name == new_name { + l.flags = layout_flags; l.min_limits = (0.0, 0.0); l.max_limits = (297.0, 210.0); l.min_extents = (0.0, 0.0, 0.0); @@ -5371,6 +5388,14 @@ impl OpenCADStudio { l.plot_paper_units = 1; l.plot_scale_numerator = 1.0; l.plot_scale_denominator = 1.0; + l.plot_scale_type = 16; + l.plot_scale_factor = 1.0; + l.plot_type = 5; + l.plot_flags.use_standard_scale = true; + l.plot_flags.print_lineweights = true; + l.plot_flags.plot_plot_styles = !plot_style.is_empty(); + l.plot_flags.show_plot_styles = !plot_style.is_empty(); + l.plot_style_sheet = plot_style; l.paper_size = "ISO_A4_(297.00_x_210.00_MM)".into(); break; } diff --git a/src/io/pdf_export.rs b/src/io/pdf_export.rs index 3f8c5d2d7..bd32fd035 100644 --- a/src/io/pdf_export.rs +++ b/src/io/pdf_export.rs @@ -25,6 +25,7 @@ use std::io::Write; use std::path::Path; #[derive(Clone, Debug)] +#[cfg_attr(target_arch = "wasm32", allow(dead_code))] pub struct PlotWire { pub wire: WireModel, pub draw_depth: f32, @@ -120,6 +121,7 @@ pub struct PdfPageInput { pub scale: f32, pub clip: Option<(f32, f32, f32, f32)>, pub options: PdfPlotOptions, + pub plot_style: Option, } impl Default for PdfPlotOptions { @@ -270,7 +272,7 @@ fn build_pdf_pages(pages: &[PdfPageInput], plot_style: Option<&PlotStyleTable>) page.rotation_deg, page.scale, page.clip, - plot_style, + page.plot_style.as_ref().or(plot_style), page.options, ); } @@ -440,6 +442,8 @@ fn append_pdf_page( let mut last_color: Option<[f32; 3]> = None; let mut last_lw: Option = None; + let mut last_cap = Some(LineCapStyle::Round); + let mut last_join = Some(LineJoinStyle::Round); // Current PDF dash array (empty = solid). Tracked so the dash op is only // re-emitted when it actually changes between wires. let mut last_dash: Option> = None; @@ -453,7 +457,9 @@ fn append_pdf_page( ox, oy, plot_style, + scale, options, + normal_blend.as_ref(), ); last_color = None; last_lw = None; @@ -506,6 +512,8 @@ fn append_pdf_page( let mut lw_override: Option = None; let mut screening = 1.0; let mut color_overridden = false; + let mut cap = None; + let mut join = None; if let Some(ctb) = plot_style { if wire.aci > 0 { if let Some([cr, cg, cb]) = ctb.resolve_color(wire.aci) { @@ -518,6 +526,20 @@ fn append_pdf_page( .resolve_lineweight(wire.aci) .map(|mm| (mm * MM_TO_PT).max(0.1)); screening = ctb.resolve_screening(wire.aci); + if let Some(entry) = ctb.aci_entries.get(wire.aci as usize) { + cap = match entry.end_style { + 0 => Some(LineCapStyle::Butt), + 1 | 3 => Some(LineCapStyle::ProjectingSquare), + 2 => Some(LineCapStyle::Round), + _ => None, + }; + join = match entry.join_style { + 0 => Some(LineJoinStyle::Miter), + 1 | 3 => Some(LineJoinStyle::Bevel), + 2 => Some(LineJoinStyle::Round), + _ => None, + }; + } } } // Near-white and near-yellow (viewport active border) → dark grey for print @@ -539,6 +561,17 @@ fn append_pdf_page( } [r, g, b] = plotted_color([r, g, b], a, screening, options); + let cap = cap.unwrap_or(LineCapStyle::Round); + if last_cap != Some(cap) { + ops.push(Op::SetLineCapStyle { cap }); + last_cap = Some(cap); + } + let join = join.unwrap_or(LineJoinStyle::Round); + if last_join != Some(join) { + ops.push(Op::SetLineJoinStyle { join }); + last_join = Some(join); + } + if last_color .map(|c| (c[0] - r).abs() > 0.01 || (c[1] - g).abs() > 0.01 || (c[2] - b).abs() > 0.01) .unwrap_or(true) @@ -576,13 +609,11 @@ fn append_pdf_page( let lw_pt = if wire.world_width > 0.0 { wire.world_width * MM_TO_PT } else { - let physical = lw_override.unwrap_or_else(|| { - if options.object_lineweights { - (wire.line_weight_px * LW_PX_TO_PT).max(0.1) - } else { - 0.1 - } - }); + let physical = if options.object_lineweights { + lw_override.unwrap_or_else(|| (wire.line_weight_px * LW_PX_TO_PT).max(0.1)) + } else { + 0.1 + }; physical / pen_divisor }; if last_lw.map(|l| (l - lw_pt).abs() > 0.01).unwrap_or(true) { @@ -852,12 +883,71 @@ fn emit_wire_fills( ox: f64, oy: f64, plot_style: Option<&PlotStyleTable>, + scale: f32, options: PdfPlotOptions, + normal_blend: Option<&ExtendedGraphicsStateId>, ) { for wire in wires { if wire.fill_tris.is_empty() { continue; } + let styled_pattern = plot_style.and_then(|table| { + (wire.aci > 0) + .then(|| table.aci_entries.get(wire.aci as usize)) + .flatten() + .and_then(|entry| { + (65..=72) + .contains(&entry.fill_style) + .then(|| { + crate::scene::model::hatch_model::plot_style_fill_pattern( + entry.fill_style, + ) + }) + .flatten() + }) + }); + if let Some(pattern) = styled_pattern { + for (triangle_index, triangle) in wire.fill_tris.chunks_exact(3).enumerate() { + let mut boundary = Vec::with_capacity(4); + for (point_index, point) in triangle.iter().enumerate() { + let index = triangle_index * 3 + point_index; + let low = wire.fill_tris_low.get(index).copied().unwrap_or([0.0; 3]); + boundary.push([point[0] + low[0], point[1] + low[1]]); + } + boundary.push(boundary[0]); + let hatch = HatchModel { + render_instance: wire.render_instance.clone(), + world_origin: [0.0, 0.0], + boundary: std::sync::Arc::new(boundary), + boundary_wcs: None, + fill_plane: None, + fill_plane_boundary: None, + boundary_exterior: None, + boundary_sources: None, + boundary_paths: None, + style: acadrust::entities::HatchStyleType::Normal, + pattern: pattern.clone(), + name: "PLOTSTYLE".to_string(), + color: wire.color, + aci: wire.aci, + line_weight_px: wire.line_weight_px, + angle_offset: 0.0, + scale: 1.0 / scale.max(1.0e-6), + draw_depth: wire.depth_override.unwrap_or(0.0), + }; + emit_hatch( + ops, + &hatch, + ox, + oy, + plot_style, + scale, + options, + normal_blend, + ); + } + continue; + } let [mut r, mut g, mut b, a] = wire.color; if a < 0.01 { continue; @@ -964,6 +1054,26 @@ fn emit_hatch( if hatch.boundary.is_empty() { return; } + let mut styled_hatch = None; + if let Some(table) = plot_style { + if hatch.aci > 0 && matches!(hatch.pattern, HatchPattern::Solid) { + if let Some(pattern) = table + .aci_entries + .get(hatch.aci as usize) + .and_then(|entry| { + crate::scene::model::hatch_model::plot_style_fill_pattern( + entry.fill_style, + ) + }) + { + let mut model = hatch.clone(); + model.pattern = pattern; + model.scale = 1.0 / scale.max(1.0e-6); + styled_hatch = Some(model); + } + } + } + let hatch = styled_hatch.as_ref().unwrap_or(hatch); let [mut r, mut g, mut b, a] = hatch.color; if a < 0.01 { return; @@ -1082,13 +1192,11 @@ fn emit_hatch( // Pattern hatches: rasterise the family lines clipped to the boundary // and emit each as a stroked line. Skips the polygon outline entirely. if matches!(hatch.pattern, HatchPattern::Pattern(_)) { - let physical = lw_override.unwrap_or_else(|| { - if options.object_lineweights { - (hatch.line_weight_px * LW_PX_TO_PT).max(0.1) - } else { - 0.1 - } - }); + let physical = if options.object_lineweights { + lw_override.unwrap_or_else(|| (hatch.line_weight_px * LW_PX_TO_PT).max(0.1)) + } else { + 0.1 + }; let divisor = if options.scale_lineweights { 1.0 } else { @@ -1248,16 +1356,16 @@ fn emit_text( if let Some(ctb) = plot_style { if wire.aci > 0 { ctb_color = ctb.resolve_color(wire.aci); - lw_override = ctb - .resolve_lineweight(wire.aci) - .map(|mm| { + lw_override = options.object_lineweights.then(|| { + ctb.resolve_lineweight(wire.aci).map(|mm| { let divisor = if options.scale_lineweights { 1.0 } else { scale.max(1e-6) }; (mm * MM_TO_PT).max(0.1) / divisor - }); + }) + }).flatten(); screening = ctb.resolve_screening(wire.aci); } } diff --git a/src/io/plot_style.rs b/src/io/plot_style.rs index a1e013608..f44eb88c0 100644 --- a/src/io/plot_style.rs +++ b/src/io/plot_style.rs @@ -1,7 +1,7 @@ //! Plot Style Table — CTB (color-based) and STB (named) file support. //! //! CTB files map indexed drawing colors (ACI, 1-255) to pen properties: -//! RGB color override, lineweight, and screeing percentage. +//! RGB color override, lineweight, and screening percentage. //! //! File format: a fixed 60-byte header followed by zlib-compressed text. //! @@ -17,6 +17,21 @@ use std::path::PathBuf; pub const DEFAULT_PLOT_STYLE: &str = "ocad.ctb"; pub const MONOCHROME_PLOT_STYLE: &str = "monochrome.ctb"; +pub const GRAYSCALE_PLOT_STYLE: &str = "Grayscale.ctb"; +pub const FILL_PATTERNS_PLOT_STYLE: &str = "Fill Patterns.ctb"; +pub const SCREENING_100_PLOT_STYLE: &str = "Screening 100%.ctb"; +pub const SCREENING_75_PLOT_STYLE: &str = "Screening 75%.ctb"; +pub const SCREENING_50_PLOT_STYLE: &str = "Screening 50%.ctb"; +pub const SCREENING_25_PLOT_STYLE: &str = "Screening 25%.ctb"; + +const STANDARD_PLOT_STYLES: &[&str] = &[ + GRAYSCALE_PLOT_STYLE, + FILL_PATTERNS_PLOT_STYLE, + SCREENING_100_PLOT_STYLE, + SCREENING_75_PLOT_STYLE, + SCREENING_50_PLOT_STYLE, + SCREENING_25_PLOT_STYLE, +]; #[cfg(not(target_arch = "wasm32"))] pub fn plot_styles_dir() -> Result { @@ -52,10 +67,14 @@ pub fn available_ctb_names() -> Vec { #[cfg(not(target_arch = "wasm32"))] { let Ok(dir) = ensure_plot_styles_dir() else { - return vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()]; + let mut names = vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()]; + names.extend(STANDARD_PLOT_STYLES.iter().map(|name| (*name).to_string())); + return names; }; let Ok(entries) = std::fs::read_dir(dir) else { - return vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()]; + let mut names = vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()]; + names.extend(STANDARD_PLOT_STYLES.iter().map(|name| (*name).to_string())); + return names; }; let mut names: Vec = entries .filter_map(Result::ok) @@ -67,13 +86,16 @@ pub fn available_ctb_names() -> Vec { .then(|| entry.file_name().to_string_lossy().into_owned()) }) .collect(); + names.extend(STANDARD_PLOT_STYLES.iter().map(|name| (*name).to_string())); names.sort_by_key(|name| name.to_ascii_lowercase()); names.dedup_by(|left, right| left.eq_ignore_ascii_case(right)); names } #[cfg(target_arch = "wasm32")] { - vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()] + let mut names = vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()]; + names.extend(STANDARD_PLOT_STYLES.iter().map(|name| (*name).to_string())); + names } } @@ -199,10 +221,44 @@ impl PlotStyleTable { MONOCHROME_PLOT_STYLE, include_bytes!("../../assets/plotstyles/monochrome.ctb"), ), + "grayscale.ctb" => { + let mut table = Self::identity(GRAYSCALE_PLOT_STYLE); + for aci in 1..=255u8 { + if let Some((r, g, b)) = acadrust::types::aci_to_rgb(aci) { + let gray = if aci == 7 { + 0 + } else { + (0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32) + .round() as u8 + }; + table.aci_entries[aci as usize].color = Some([gray, gray, gray]); + } + } + Ok(table) + } + "fill patterns.ctb" => { + let mut table = Self::identity(FILL_PATTERNS_PLOT_STYLE); + for aci in 1..=9usize { + table.aci_entries[aci].fill_style = 63 + aci as u8; + } + Ok(table) + } + "screening 100%.ctb" => Ok(Self::screening(SCREENING_100_PLOT_STYLE, 100)), + "screening 75%.ctb" => Ok(Self::screening(SCREENING_75_PLOT_STYLE, 75)), + "screening 50%.ctb" => Ok(Self::screening(SCREENING_50_PLOT_STYLE, 50)), + "screening 25%.ctb" => Ok(Self::screening(SCREENING_25_PLOT_STYLE, 25)), _ => Err(format!("Unknown built-in plot style: {name}")), } } + fn screening(name: &str, percent: u8) -> Self { + let mut table = Self::identity(name); + for entry in table.aci_entries.iter_mut().skip(1) { + entry.screening = percent; + } + table + } + /// Load one CTB by file name from the per-user plot styles folder. pub fn load_named(name: &str) -> Result { let path = Path::new(name); @@ -227,9 +283,11 @@ impl PlotStyleTable { .to_string_lossy() .eq_ignore_ascii_case(name) }) - .map(|entry| entry.path()) - .ok_or_else(|| format!("Plot style not found: {name}"))?; - return Self::load(&matched); + .map(|entry| entry.path()); + return match matched { + Some(path) => Self::load(&path), + None => Self::builtin(name), + }; } #[cfg(target_arch = "wasm32")] @@ -248,9 +306,23 @@ impl PlotStyleTable { /// Returns None if no override (use object color). pub fn resolve_color(&self, aci: u8) -> Option<[f32; 3]> { let entry = self.aci_entries.get(aci as usize)?; - entry + let mut color = entry .color - .map(|[r, g, b]| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0]) + .map(|[r, g, b]| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0]); + if entry.color_policy & 2 != 0 { + let rgb = color.or_else(|| { + if aci == 7 { + Some([0.0; 3]) + } else { + acadrust::types::aci_to_rgb(aci).map(|(r, g, b)| { + [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0] + }) + } + })?; + let gray = 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]; + color = Some([gray; 3]); + } + color } /// Resolve the effective lineweight in mm for the given ACI index. @@ -260,7 +332,13 @@ impl PlotStyleTable { if matches!(entry.lineweight, 0 | 255) { None } else { - self.lineweights.get(entry.lineweight as usize).copied() + self.lineweights.get(entry.lineweight as usize).copied().map(|weight| { + if self.apply_factor { + weight * self.scale_factor.max(0.0) + } else { + weight + } + }) } } diff --git a/src/io/print_to_printer.rs b/src/io/print_to_printer.rs index 648e41629..7e3128101 100644 --- a/src/io/print_to_printer.rs +++ b/src/io/print_to_printer.rs @@ -29,6 +29,11 @@ pub(crate) fn temp_pdf_path(kind: &str) -> std::path::PathBuf { )) } +#[cfg(target_arch = "wasm32")] +pub(crate) fn temp_pdf_path(kind: &str) -> std::path::PathBuf { + std::path::PathBuf::from(format!("{kind}.pdf")) +} + /// Extra options for a print job. On CUPS (Linux/macOS) these map to `lp` /// flags / `-o` options. On Windows the generated PDF already carries render /// options. Windows queues repeated jobs when more than one copy is requested; diff --git a/src/scene/layout.rs b/src/scene/layout.rs index bcf85378c..ac9750a04 100644 --- a/src/scene/layout.rs +++ b/src/scene/layout.rs @@ -72,8 +72,7 @@ impl Scene { // Remove the Layout object itself. self.document.objects.remove(&layout_handle); - // Drop the layout's entry from the ACAD_LAYOUT dictionary so it does not - // dangle (and so AutoCAD doesn't try to recover a now-missing layout). + // Drop the layout's dictionary entry so it does not dangle. let dict_handle = self.document.header.acad_layout_dict_handle; if let Some(ObjectType::Dictionary(d)) = self.document.objects.get_mut(&dict_handle) { d.entries.retain(|(k, _)| k != name); @@ -90,20 +89,6 @@ impl Scene { self.document.block_records.remove(&bn); } - // Drop any standalone PlotSettings page setup tied to this layout. - let ps_handles: Vec = self - .document - .objects - .iter() - .filter_map(|(h, o)| match o { - ObjectType::PlotSettings(ps) if ps.page_name == name => Some(*h), - _ => None, - }) - .collect(); - for h in ps_handles { - self.document.objects.remove(&h); - } - // If the deleted layout was active, fall back to Model space. if self.current_layout == name { self.current_layout = "Model".to_string(); @@ -498,9 +483,7 @@ impl Scene { /// Reconstruct a `pane_grid` configuration from a set of (tile-index, rect) /// items covering `region`, by recursively finding a full vertical or -/// horizontal guillotine cut. AutoCAD tiled configs (and anything pane_grid -/// produces) are guillotine layouts, so this round-trips them. A non-guillotine -/// set falls back to chaining panes so nothing is lost. +/// horizontal guillotine cut. Non-guillotine input falls back to chained panes. fn config_from_rects( items: &[(usize, iced::Rectangle)], region: iced::Rectangle, diff --git a/src/scene/limits.rs b/src/scene/limits.rs index b6b18daa8..09783637a 100644 --- a/src/scene/limits.rs +++ b/src/scene/limits.rs @@ -91,6 +91,7 @@ impl Scene { self.document.header.limit_check = enabled; } else { self.document.header.paper_space_limit_check = enabled; + self.persist_current_layout_state(); } } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index f706fc99e..313058bc4 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1614,6 +1614,15 @@ pub struct Scene { /// movement scanned the whole document for wipeouts and recreated the same /// Arcs, making Paper frame construction CPU-bound on large drawings. paper_sheet_render_cache: RefCell>, + display_plot_style_cache: + RefCell>>>, + styled_wire_cache: RefCell< + HashMap<(u64, String), (u64, Arc>)>, + >, + styled_hatch_cache: + RefCell>>>, + styled_wire_fill_cache: + RefCell>>>, /// Per-viewport projected wire cache for paper-space content viewports. /// Stores projected + clipped wires in paper-space coordinates. /// Maps vp_handle → (geometry_epoch, Vec). @@ -1880,6 +1889,10 @@ impl Scene { paper_sheet_cache: RefCell::new(HashMap::default()), paper_viewport_cache: RefCell::new(HashMap::default()), paper_sheet_render_cache: RefCell::new(HashMap::default()), + display_plot_style_cache: RefCell::new(HashMap::default()), + styled_wire_cache: RefCell::new(HashMap::default()), + styled_hatch_cache: RefCell::new(HashMap::default()), + styled_wire_fill_cache: RefCell::new(HashMap::default()), paper_projected_cache: RefCell::new(HashMap::default()), current_layout: "Model".to_string(), block_edit_block: None, @@ -2878,7 +2891,9 @@ impl Scene { /// active layout or its background are dropped. pub fn set_current_layout(&mut self, name: String) { if self.current_layout != name { + self.persist_current_layout_state(); self.current_layout = name; + self.load_current_layout_state(); self.sync_active_space_to_document(); self.recolor_meshes(); *self.wire_cache.borrow_mut() = None; @@ -2888,6 +2903,91 @@ impl Scene { } } + pub fn load_current_layout_state(&mut self) { + self.display_plot_style_cache + .borrow_mut() + .retain(|(layout, _), _| layout != &self.current_layout); + self.styled_wire_cache.borrow_mut().clear(); + self.styled_hatch_cache.borrow_mut().clear(); + self.styled_wire_fill_cache.borrow_mut().clear(); + if self.current_layout == "Model" { + return; + } + if let Some((flags, insertion_base, min_extents, max_extents, min_limits, max_limits)) = + self.document.objects.values().find_map(|object| { + let ObjectType::Layout(layout) = object else { + return None; + }; + (layout.name == self.current_layout).then_some(( + layout.flags, + layout.insertion_base, + layout.min_extents, + layout.max_extents, + layout.min_limits, + layout.max_limits, + )) + }) { + self.document.header.paper_space_linetype_scaling = flags & 1 != 0; + self.document.header.paper_space_limit_check = flags & 2 != 0; + self.document.header.paper_space_insertion_base = acadrust::types::Vector3::new( + insertion_base.0, + insertion_base.1, + insertion_base.2, + ); + self.document.header.paper_space_extents_min = acadrust::types::Vector3::new( + min_extents.0, + min_extents.1, + min_extents.2, + ); + self.document.header.paper_space_extents_max = acadrust::types::Vector3::new( + max_extents.0, + max_extents.1, + max_extents.2, + ); + self.document.header.paper_space_limits_min = + acadrust::types::Vector2::new(min_limits.0, min_limits.1); + self.document.header.paper_space_limits_max = + acadrust::types::Vector2::new(max_limits.0, max_limits.1); + } + } + + pub fn persist_current_layout_state(&mut self) { + if self.current_layout == "Model" { + return; + } + let header = &self.document.header; + let psltscale = header.paper_space_linetype_scaling; + let plimcheck = header.paper_space_limit_check; + let insertion_base = header.paper_space_insertion_base; + let min_extents = header.paper_space_extents_min; + let max_extents = header.paper_space_extents_max; + let min_limits = header.paper_space_limits_min; + let max_limits = header.paper_space_limits_max; + for object in self.document.objects.values_mut() { + let ObjectType::Layout(layout) = object else { + continue; + }; + if layout.name == self.current_layout { + layout.flags = if psltscale { + layout.flags | 1 + } else { + layout.flags & !1 + }; + layout.flags = if plimcheck { + layout.flags | 2 + } else { + layout.flags & !2 + }; + layout.insertion_base = (insertion_base.x, insertion_base.y, insertion_base.z); + layout.min_extents = (min_extents.x, min_extents.y, min_extents.z); + layout.max_extents = (max_extents.x, max_extents.y, max_extents.z); + layout.min_limits = (min_limits.x, min_limits.y); + layout.max_limits = (max_limits.x, max_limits.y); + break; + } + } + } + /// Mirror the active space (Model tile-mode vs a paper layout) into the /// document's persisted settings so it round-trips on save: the `$TILEMODE` /// header (`show_model_space`) and the `CTAB` current-tab variable. The @@ -3305,32 +3405,17 @@ impl Scene { Some(wire) } - /// The effective plot settings for the current layout: a standalone - /// PlotSettings page setup if one exists, otherwise the settings embedded in - /// the LAYOUT object (paper size, margins, origin, rotation, scale). Loaded - /// The fallback preserves rotation, origin and scale from loaded files. + /// The effective plot settings embedded in the current layout. pub fn effective_plot_settings(&self) -> Option { self.plot_settings_for(&self.current_layout) } - /// Plot settings for a specific layout by name: its standalone - /// `PlotSettings` object if one exists, else synthesized from the `Layout` - /// object's embedded fields. + /// Plot settings embedded in a specific layout. pub fn plot_settings_for(&self, name: &str) -> Option { use acadrust::objects::{ ObjectType, PaperMargin, PlotPaperUnits, PlotRotation, PlotSettings, PlotType, - PlotWindow, ScaledType, + PlotWindow, ScaledType, ShadePlotMode, ShadePlotResolutionLevel, }; - if let Some(ps) = self.document.objects.values().find_map(|o| { - if let ObjectType::PlotSettings(ps) = o { - if ps.page_name.as_str() == name { - return Some(ps.clone()); - } - } - None - }) { - return Some(ps); - } self.document.objects.values().find_map(|o| { let ObjectType::Layout(l) = o else { return None; @@ -3338,10 +3423,13 @@ impl Scene { if l.name.as_str() != name { return None; } - let mut ps = PlotSettings::new(l.name.clone()); + let mut ps = PlotSettings::new(l.plot_page_name.clone()); + ps.printer_name = l.plot_printer_name.clone(); ps.paper_width = l.paper_width; ps.paper_height = l.paper_height; ps.paper_size = l.paper_size.clone(); + ps.plot_view_name = l.plot_view_name.clone(); + ps.current_style_sheet = l.plot_style_sheet.clone(); ps.margins = PaperMargin::new( l.plot_margin_left, l.plot_margin_bottom, @@ -3362,10 +3450,84 @@ impl Scene { ps.scale_type = ScaledType::from_code(l.plot_scale_type); ps.scale_numerator = l.plot_scale_numerator; ps.scale_denominator = l.plot_scale_denominator; + ps.flags = l.plot_flags; + ps.standard_scale_factor = l.plot_scale_factor; + ps.paper_image_origin_x = l.paper_image_origin_x; + ps.paper_image_origin_y = l.paper_image_origin_y; + ps.shade_plot_mode = ShadePlotMode::from_code(l.shade_plot_mode); + ps.shade_plot_resolution = + ShadePlotResolutionLevel::from_code(l.shade_plot_resolution); + ps.shade_plot_dpi = l.shade_plot_dpi; + ps.plot_view_handle = l.plot_view_handle; + ps.visual_style_handle = l.visual_style_handle; Some(ps) }) } + /// Replace the plot-settings portion embedded in one layout. + pub fn set_layout_plot_settings( + &mut self, + name: &str, + ps: &acadrust::objects::PlotSettings, + ) -> bool { + let Some(layout) = self.document.objects.values_mut().find_map(|object| { + let ObjectType::Layout(layout) = object else { + return None; + }; + (layout.name == name).then_some(layout) + }) else { + return false; + }; + + layout.plot_page_name = ps.page_name.clone(); + layout.plot_printer_name = ps.printer_name.clone(); + layout.paper_size = ps.paper_size.clone(); + layout.plot_view_name = ps.plot_view_name.clone(); + layout.plot_style_sheet = ps.current_style_sheet.clone(); + layout.plot_margin_left = ps.margins.left; + layout.plot_margin_bottom = ps.margins.bottom; + layout.plot_margin_right = ps.margins.right; + layout.plot_margin_top = ps.margins.top; + layout.paper_width = ps.paper_width; + layout.paper_height = ps.paper_height; + layout.plot_origin_x = ps.origin_x; + layout.plot_origin_y = ps.origin_y; + layout.plot_window_min_x = ps.plot_window.lower_left_x; + layout.plot_window_min_y = ps.plot_window.lower_left_y; + layout.plot_window_max_x = ps.plot_window.upper_right_x; + layout.plot_window_max_y = ps.plot_window.upper_right_y; + layout.plot_scale_numerator = ps.scale_numerator; + layout.plot_scale_denominator = ps.scale_denominator; + layout.plot_paper_units = ps.paper_units.to_code(); + layout.plot_rotation = ps.rotation.to_code(); + layout.plot_type = ps.plot_type.to_code(); + layout.plot_scale_type = ps.scale_type.to_code(); + layout.shade_plot_mode = ps.shade_plot_mode.to_code(); + layout.shade_plot_resolution = ps.shade_plot_resolution.to_code(); + layout.shade_plot_dpi = ps.shade_plot_dpi; + layout.plot_flags = ps.flags; + layout.plot_scale_factor = ps.standard_scale_factor; + layout.paper_image_origin_x = ps.paper_image_origin_x; + layout.paper_image_origin_y = ps.paper_image_origin_y; + layout.plot_view_handle = ps.plot_view_handle; + layout.visual_style_handle = ps.visual_style_handle; + layout.raw_plot_settings_codes = None; + self.display_plot_style_cache + .borrow_mut() + .retain(|(layout_name, _), _| layout_name != name); + self.styled_wire_cache.borrow_mut().clear(); + self.styled_hatch_cache.borrow_mut().clear(); + self.styled_wire_fill_cache.borrow_mut().clear(); + true + } + + pub fn invalidate_display_plot_style(&self) { + self.display_plot_style_cache.borrow_mut().clear(); + self.styled_wire_cache.borrow_mut().clear(); + self.styled_hatch_cache.borrow_mut().clear(); + self.styled_wire_fill_cache.borrow_mut().clear(); + } + /// PlotSettings store the paper size and plot margins in millimetres, but a /// layout's paper space is laid out in its own units — its viewports, /// drawing limits and the sheet the user sees can be in inches, millimetres, diff --git a/src/scene/model/hatch_model.rs b/src/scene/model/hatch_model.rs index f0f13c93b..1ae480b4d 100644 --- a/src/scene/model/hatch_model.rs +++ b/src/scene/model/hatch_model.rs @@ -139,6 +139,39 @@ impl GradientKind { } } +pub(crate) fn plot_style_fill_pattern(style: u8) -> Option { + let family = |angle_deg, dx, dy, dashes: &[f32]| PatFamily { + angle_deg, + x0: 0.0, + y0: 0.0, + dx, + dy, + dashes: dashes.to_vec(), + }; + let pattern = match style { + 64 => HatchPattern::Solid, + 65 => HatchPattern::Pattern(vec![ + family(0.0, 0.0, 2.0, &[2.0, -2.0]), + family(90.0, 2.0, 0.0, &[2.0, -2.0]), + ]), + 66 => HatchPattern::Pattern(vec![ + family(0.0, 0.0, 2.0, &[]), + family(90.0, 2.0, 0.0, &[]), + ]), + 67 => HatchPattern::Pattern(vec![ + family(45.0, 0.0, 2.0, &[]), + family(135.0, 0.0, 2.0, &[]), + ]), + 68 => HatchPattern::Pattern(vec![family(0.0, 0.0, 2.0, &[])]), + 69 => HatchPattern::Pattern(vec![family(135.0, 0.0, 2.0, &[])]), + 70 => HatchPattern::Pattern(vec![family(45.0, 0.0, 2.0, &[])]), + 71 => HatchPattern::Pattern(vec![family(0.0, 0.0, 2.0, &[0.0, -2.0])]), + 72 => HatchPattern::Pattern(vec![family(90.0, 2.0, 0.0, &[])]), + _ => return None, + }; + Some(pattern) +} + /// Hatch fill pattern. #[derive(Clone, Debug)] pub enum HatchPattern { diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 2e5fd7032..daaa3f5a6 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -22,6 +22,8 @@ use crate::scene::{ SceneLight, Uniforms, ViewportInstance, WireModel, }; +const DISPLAY_STYLE_CACHE_LIMIT: usize = if cfg!(target_arch = "wasm32") { 4 } else { 24 }; + // ── Camera hover state (shader::Program::State) ─────────────────────────── #[derive(Clone, Default)] @@ -2496,6 +2498,238 @@ impl Scene { }; (final_color, pl, pat, lw, aci) } + + fn display_plot_style(&self) -> Option> { + 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>, + source_gen: u64, + ) -> (Arc>, 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>, + wire_fills: Option<&Arc>>, + pattern_scale: f32, + ) -> Arc> { + 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>, + source_gen: u64, + pattern_scale: f32, + ) -> Option>> { + let Some(style) = self.display_plot_style() else { + return None; + }; + let key = ( + source_gen, + style.name.to_ascii_lowercase(), + pattern_scale.to_bits(), + ); + if let Some(hatches) = self.styled_wire_fill_cache.borrow().get(&key) { + return Some(Arc::clone(hatches)); + } + let mut hatches = Vec::new(); + for wire in wires.iter().filter(|wire| wire.fill_is_2d_solid && wire.aci > 0) { + let Some(pattern) = style + .aci_entries + .get(wire.aci as usize) + .and_then(|entry| { + (65..=72) + .contains(&entry.fill_style) + .then(|| { + crate::scene::model::hatch_model::plot_style_fill_pattern( + entry.fill_style, + ) + }) + .flatten() + }) + else { + continue; + }; + let mut color = wire.color; + self.apply_display_plot_style(&mut color, wire.aci, &style); + let line_weight_px = style + .resolve_lineweight(wire.aci) + .map(|mm| (mm * (96.0 / 25.4) * 2.0).max(1.0)) + .unwrap_or(wire.line_weight_px); + for (triangle_index, triangle) in wire.fill_tris.chunks_exact(3).enumerate() { + let mut boundary = Vec::with_capacity(4); + for (point_index, point) in triangle.iter().enumerate() { + let index = triangle_index * 3 + point_index; + let low = wire.fill_tris_low.get(index).copied().unwrap_or([0.0; 3]); + boundary.push([point[0] + low[0], point[1] + low[1]]); + } + boundary.push(boundary[0]); + hatches.push(HatchModel { + render_instance: wire.render_instance.clone(), + world_origin: [0.0, 0.0], + boundary: Arc::new(boundary), + boundary_wcs: None, + fill_plane: None, + fill_plane_boundary: None, + boundary_exterior: None, + boundary_sources: None, + boundary_paths: None, + style: acadrust::entities::HatchStyleType::Normal, + pattern: pattern.clone(), + name: "PLOTSTYLE".to_string(), + color, + aci: 0, + line_weight_px, + angle_offset: 0.0, + scale: pattern_scale, + draw_depth: wire.depth_override.unwrap_or(0.0), + }); + } + } + let hatches = Arc::new(hatches); + let mut cache = self.styled_wire_fill_cache.borrow_mut(); + if cache.len() >= DISPLAY_STYLE_CACHE_LIMIT { + cache.clear(); + } + cache.insert(key, Arc::clone(&hatches)); + Some(hatches) + } } /// Whether an entity sits on a locked layer (via the document's layer table). @@ -3403,11 +3637,15 @@ impl Scene { } else { self.model_wires_for_viewport_arc(inst.handle, full.height) }; + let source_gen = self.last_model_wire_gen.get(); + let styled_fill_source = Arc::clone(&base_arc); + let (base_arc, styled_gen) = self.display_styled_wires(base_arc, source_gen); + self.last_model_wire_gen.set(styled_gen); // Wire-buffer content id for the upload gate. Preview / interim wires // are NOT part of this buffer anymore (they go in a separate per-frame // overlay buffer below), so the base id is the source's stable content // gen — a drag or camera move never re-uploads the base wire set. - let base_wire_content_id = self.last_model_wire_gen.get(); + let base_wire_content_id = styled_gen; let base_wire_patch = self.model_wire_patch_for(base_wire_content_id); // Split Face3D wires from the rest. The split is content-only (keyed // by the wire-set content id), so while the geometry is unchanged it's @@ -3548,8 +3786,15 @@ impl Scene { width: full.width.max(1.0), height: full.height.max(1.0), }; - let mut uniforms = - Uniforms::new(&inst.camera, full_bounds, self.document.header.lineweight_display); + let display_plot_lineweights = self.current_layout != "Model" + && self.effective_plot_settings().is_some_and(|settings| { + settings.flags.show_plot_styles && settings.flags.print_lineweights + }); + let mut uniforms = Uniforms::new( + &inst.camera, + full_bounds, + self.document.header.lineweight_display || display_plot_lineweights, + ); if self.document.header.paper_space_linetype_scaling && !inst.paper_sheet && inst.tile_idx.is_none() @@ -3642,6 +3887,29 @@ impl Scene { None, ) }; + let viewport_scale = if inst.paper_sheet { + 1.0 + } else { + self.document + .get_entity(inst.handle) + .and_then(|entity| match entity { + EntityType::Viewport(viewport) => Some(vp_effective_scale( + viewport.custom_scale, + viewport.view_height, + viewport.height, + )), + _ => None, + }) + .unwrap_or(1.0) + }; + let pattern_scale = (self.paper_space_unit_factor() / viewport_scale.max(1.0e-9)) as f32; + let styled_wire_fills = + self.display_styled_wire_fills(&styled_fill_source, source_gen, pattern_scale); + let hatches = self.display_styled_hatches( + hatches, + styled_wire_fills.as_ref().filter(|fills| !fills.is_empty()), + pattern_scale, + ); let images = if let Some(images) = paper_images { images } else { diff --git a/src/ui/window/plot.rs b/src/ui/window/plot.rs index db7678fc3..28b5243a8 100644 --- a/src/ui/window/plot.rs +++ b/src/ui/window/plot.rs @@ -63,6 +63,8 @@ pub enum PlotFlag { FitToPaper, Center, ScaleLw, + PlotStyles, + DisplayStyles, UpsideDown, Lineweights, Transparency, @@ -132,6 +134,10 @@ pub struct PlotDialogState { /// Output goes to a PDF file instead of a printer. pub to_file: bool, pub paper: String, + #[serde(skip)] + pub paper_width_mm: f64, + #[serde(skip)] + pub paper_height_mm: f64, pub orientation: String, pub upside_down: bool, pub copies: String, @@ -144,6 +150,8 @@ pub struct PlotDialogState { pub fit_to_paper: bool, #[serde(skip)] pub scales: Vec<(String, f64)>, + #[serde(skip)] + pub plot_views: Vec, pub scale_lw: bool, pub quality: String, pub shade: String, @@ -155,6 +163,8 @@ pub struct PlotDialogState { pub stamp: bool, /// Display name of the active plot style table ("" = none). pub style_name: String, + pub apply_plot_styles: bool, + pub show_plot_styles: bool, /// CTB file names discovered in the per-user plot styles folder. #[serde(skip)] pub plot_styles: Vec, @@ -185,6 +195,8 @@ impl Default for PlotDialogState { printer: None, to_file: false, paper: "A4".into(), + paper_width_mm: 297.0, + paper_height_mm: 210.0, orientation: "Landscape".into(), upside_down: false, copies: "1".into(), @@ -195,6 +207,7 @@ impl Default for PlotDialogState { scale: "1:1".into(), fit_to_paper: true, scales: Vec::new(), + plot_views: Vec::new(), scale_lw: false, quality: "Normal".into(), shade: "As displayed".into(), @@ -205,6 +218,8 @@ impl Default for PlotDialogState { paperspace_last: false, stamp: false, style_name: String::new(), + apply_plot_styles: true, + show_plot_styles: false, plot_styles: Vec::new(), style_missing: false, page_setups: Vec::new(), @@ -224,6 +239,8 @@ impl PlotDialogState { self.printer = o.printer.clone(); self.to_file = o.to_file; self.paper = o.paper.clone(); + self.paper_width_mm = o.paper_width_mm; + self.paper_height_mm = o.paper_height_mm; self.orientation = o.orientation.clone(); self.upside_down = o.upside_down; self.copies = o.copies.clone(); @@ -243,6 +260,8 @@ impl PlotDialogState { self.paperspace_last = o.paperspace_last; self.stamp = o.stamp; self.style_name = o.style_name.clone(); + self.apply_plot_styles = o.apply_plot_styles; + self.show_plot_styles = o.show_plot_styles; self.style_missing = o.style_missing; } @@ -622,7 +641,10 @@ pub fn view_window( None => PlotChoice::localized(OUT_DEFAULT), }) }; - let paper_opts: Vec = PaperSize::ALL.iter().map(|p| p.label().to_string()).collect(); + let mut paper_opts: Vec = PaperSize::ALL.iter().map(|p| p.label().to_string()).collect(); + if !paper_opts.iter().any(|name| name == &s.paper) { + paper_opts.push(s.paper.clone()); + } let paper_note: Element<'_, Message> = if s.area == "Layout" { text(t!("Layout plots the current sheet using the selected paper size.")) .size(10) @@ -685,11 +707,18 @@ pub fn view_window( let mut area_options = if print_all_options { choices(&["Layout"]) } else { - choices(&["Extents", "Display", "Window"]) + choices(&["Extents", "Limits", "Display", "Window"]) }; if s.paper_space && !print_all_options { area_options.insert(0, PlotChoice::localized("Layout")); } + if !print_all_options { + area_options.extend( + s.plot_views + .iter() + .map(|name| PlotChoice::raw(format!("View: {name}"))), + ); + } let mut area_row = row![ text(t!("What to plot")).size(11).style(muted_style).width(92), iced::widget::pick_list( @@ -779,6 +808,18 @@ pub fn view_window( PlotDlgMsg::Style, width, ), + check_enabled( + t!("Plot with plot styles"), + s.apply_plot_styles, + PlotFlag::PlotStyles, + !s.style_name.is_empty(), + ), + check_enabled( + t!("Display plot styles"), + s.show_plot_styles, + PlotFlag::DisplayStyles, + s.paper_space && !s.style_name.is_empty(), + ), row![ button(text(t!("Load…")).size(11)) .on_press(Message::PlotDlg(PlotDlgMsg::LoadStyle))