diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 1661853a3..8658ec9e1 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -1148,8 +1148,19 @@ impl OpenCADStudio { use acadrust::objects::{ObjectType, PlotSettings}; let layout_name = self.tabs[i].scene.current_layout.clone(); if layout_name == "Model" { - self.command_line - .push_error("PLOTWINDOW: switch to a paper space layout first."); + // Model space: remember the window (world X/Y) for the plot dialog. + let x0 = p1.x.min(p2.x); + let y0 = p1.y.min(p2.y); + let x1 = p1.x.max(p2.x); + let y1 = p1.y.max(p2.y); + self.plot_window = Some((x0, y0, x1, y1)); + self.command_line.push_output(&format!( + "Plot window: {x0:.2},{y0:.2} to {x1:.2},{y1:.2}" + )); + // Pick window closed Page Setup so the viewport could + // receive the two clicks — bring it back for scale + // review and the "Plot window → PDF" button. + self.active_modal = Some(super::ModalKind::PageSetup); } else { let block_handle = self.tabs[i].scene.current_layout_block_handle_pub(); let doc = &mut self.tabs[i].scene.document; diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs index 2023a59fb..7a0dd3e51 100644 --- a/src/app/commands/display.rs +++ b/src/app/commands/display.rs @@ -509,13 +509,11 @@ impl OpenCADStudio { } } + // Model space now has its own dialog section (window-plot format / + // orientation / pick), so PAGESETUP opens there too, not just on + // paper-space layouts. "PAGESETUP" => { - if self.tabs[i].scene.current_layout == "Model" { - self.command_line - .push_error("PAGESETUP: switch to a paper space layout first."); - } else { - return Some(Task::done(Message::PageSetupOpen)); - } + return Some(Task::done(Message::PageSetupOpen)); } // ── Recognized commands whose full implementation is pending ───────── diff --git a/src/app/mod.rs b/src/app/mod.rs index a143b732f..7e9bd3f90 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -520,6 +520,10 @@ pub(super) struct OpenCADStudio { page_setup_rotation: String, /// Plot scale: "Fit" | "1:1" | "1:2" | "1:4" | "1:5" | "1:10" | "1:20" | "1:50" | "1:100" | "2:1". page_setup_scale: String, + /// Pending model-space plot window (x0, y0, x1, y1) in world XY, or None. + plot_window: Option<(f64, f64, f64, f64)>, + plot_format: crate::io::paper_sizes::PaperSize, + plot_orientation: crate::io::paper_sizes::Orientation, // ── Plot Style Table ────────────────────────────────────────────────── /// Currently loaded CTB/STB table (None = no override). @@ -1745,6 +1749,14 @@ pub enum Message { PlotExport, /// Callback after the user picks (or cancels) the export path. PlotExportPath(Option), + /// User picked a paper size for the model-space window plot. + PlotFormat(crate::io::paper_sizes::PaperSize), + /// User picked a sheet orientation for the model-space window plot. + PlotOrientation(crate::io::paper_sizes::Orientation), + /// Export the pending model-space plot window (from PLOTWINDOW) to PDF. + PlotWindowExport, + /// Callback after the user picks (or cancels) the window-export path. + PlotWindowExportPath(Option), /// Send current layout to the system printer (via lp / lpr). PrintToPrinter, /// Callback from the async printer job. @@ -2089,6 +2101,9 @@ impl OpenCADStudio { page_setup_offset_y: "0.0".to_string(), page_setup_rotation: "0".to_string(), page_setup_scale: "Fit".to_string(), + plot_window: None, + plot_format: crate::io::paper_sizes::PaperSize::A4, + plot_orientation: crate::io::paper_sizes::Orientation::Landscape, opening: None, pending_close: None, save_dialog_format: "DWG 2018".to_string(), diff --git a/src/app/update/file.rs b/src/app/update/file.rs index a7b4c1428..89fd79a98 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -1026,6 +1026,8 @@ pub(super) fn on_open_file(&mut self) -> Task { draw_ox as f32, draw_oy as f32, rotation_deg, + 1.0, + None, &path, self.active_plot_style.as_ref(), ) { @@ -1038,6 +1040,93 @@ pub(super) fn on_open_file(&mut self) -> Task { Task::none() } + /// Export the pending model-space plot window (set by PLOTWINDOW while on + /// the Model tab) to PDF, using the chosen paper size/orientation/scale. + pub(super) fn on_plot_window_export_path_some(&mut self, path: std::path::PathBuf) -> Task { + use crate::io::paper_sizes::{sheet_mm, window_to_sheet, PlotScale}; + let i = self.active_tab; + if self.tabs[i].scene.current_layout != "Model" { + self.command_line + .push_error("Plot window export is for model space."); + return Task::none(); + } + let Some((x0, y0, x1, y1)) = self.plot_window else { + self.command_line.push_error("No plot window. Pick one first."); + return Task::none(); + }; + if (x1 - x0) < 1e-6 || (y1 - y0) < 1e-6 { + self.command_line + .push_error("Plot window is empty. Pick a larger window."); + return Task::none(); + } + let (sheet_w, sheet_h) = sheet_mm(self.plot_format, self.plot_orientation); + let win_w = (x1 - x0).max(1e-9); + let win_h = (y1 - y0).max(1e-9); + let scale_sel = if self.page_setup_scale.trim().eq_ignore_ascii_case("fit") { + PlotScale::Fit + } else { + let (num, den) = parse_plot_scale(&self.page_setup_scale); + if num > 0.0 && den > 0.0 { + PlotScale::Ratio(num / den) + } else { + PlotScale::Fit + } + }; + let (scale, ox, oy) = + window_to_sheet((win_w, win_h), (x0, y0), (sheet_w, sheet_h), scale_sel); + + let scene = &self.tabs[i].scene; + // Cull to the window so a small-area plot doesn't write the whole + // drawing into the PDF; the clip below trims the partial crossers. + // aabb is world X/Y [minx, miny, maxx, maxy]. + let (wx0, wy0, wx1, wy1) = (x0 as f32, y0 as f32, x1 as f32, y1 as f32); + let wires: Vec<_> = scene + .entity_wires() + .into_iter() + .filter(|w| { + w.aabb[0] <= wx1 && w.aabb[2] >= wx0 && w.aabb[1] <= wy1 && w.aabb[3] >= wy0 + }) + .collect(); + let hatches = scene.paper_canvas_hatches(); + let wipeouts = scene.paper_canvas_wipeouts(); + + // build_pdf maps a wire coordinate to sheet mm as (coord + offset) * + // scale (rotation_deg == 0 adds no further CTM translation). + // window_to_sheet's (ox, oy) is the sheet-mm target for the window's + // min corner, so: scale * (x0 + offset_x) = ox => offset_x = ox/scale - x0. + let offset_x = ((ox / scale) - x0) as f32; + let offset_y = ((oy / scale) - y0) as f32; + // Clip rect in pre-scale space (build_pdf's CTM applies `scale` to it, + // same as the wires) so the final sheet-mm rect lands at (ox, oy). + let clip = Some(((ox / scale) as f32, (oy / scale) as f32, win_w as f32, win_h as f32)); + + let res = crate::io::pdf_export::export_pdf( + &wires, + hatches.as_slice(), + wipeouts.as_slice(), + sheet_w, + sheet_h, + offset_x, + offset_y, + 0, + scale as f32, + clip, + &path, + self.active_plot_style.as_ref(), + ); + match res { + Ok(()) => { + self.command_line.push_info(&format!( + "Plotted window to {}", + path.file_name().unwrap_or_default().to_string_lossy() + )); + self.close_active_modal(); + } + Err(e) => self.command_line.push_error(&format!("Plot failed: {e}")), + } + Task::none() + } + pub(super) fn on_print_to_printer(&mut self) -> Task { let i = self.active_tab; let scene = &self.tabs[i].scene; diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index af3eecdaa..ccac95819 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -831,6 +831,14 @@ impl OpenCADStudio { self.tabs[i].scene.selection.borrow_mut().context_menu = None; // Any command also dismisses the Isolate action menu. self.isolate_popup_open = false; + // "Pick window" (PLOTWINDOW) from Page Setup needs the backdrop + // gone so the viewport pick lands; every other command leaves an + // open modal (and its staged edits) untouched. + if cmd.trim().eq_ignore_ascii_case("PLOTWINDOW") + || cmd.trim().eq_ignore_ascii_case("PW") + { + self.close_active_modal(); + } self.dispatch_command(&cmd) } @@ -2987,6 +2995,30 @@ impl OpenCADStudio { Message::PlotExportPath(None) => Task::none(), Message::PlotExportPath(Some(path)) => self.on_plot_export_path_some(path), + Message::PlotFormat(f) => { + self.plot_format = f; + Task::none() + } + Message::PlotOrientation(o) => { + self.plot_orientation = o; + Task::none() + } + Message::PlotWindowExport => { + let i = self.active_tab; + let stem = self.tabs[i] + .current_path + .as_deref() + .and_then(|p: &std::path::Path| p.file_stem()) + .map(|s: &std::ffi::OsStr| s.to_string_lossy().into_owned()) + .unwrap_or_else(|| "drawing".into()); + Task::perform( + crate::io::pdf_export::pick_pdf_path_owned(stem), + Message::PlotWindowExportPath, + ) + } + Message::PlotWindowExportPath(None) => Task::none(), + Message::PlotWindowExportPath(Some(path)) => self.on_plot_window_export_path_some(path), + // ── Print to system printer ─────────────────────────────────────── Message::PrintToPrinter => self.on_print_to_printer(), Message::PrintResult(Ok(printer)) => { diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index f20d83785..cbbc5d65f 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -57,6 +57,8 @@ impl OpenCADStudio { &self.page_setup_offset_y, &self.page_setup_rotation, &self.page_setup_scale, + self.plot_format, + self.plot_orientation, ), 520, 460, diff --git a/src/io/mod.rs b/src/io/mod.rs index bba939ae8..b9471168c 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -14,6 +14,7 @@ pub mod xref; pub mod linetypes; pub mod patterns; pub mod update_check; +pub mod paper_sizes; use crate::scene::DerivedCaches; use acadrust::entities::EntityType; diff --git a/src/io/paper_sizes.rs b/src/io/paper_sizes.rs new file mode 100644 index 000000000..dcc7e8a82 --- /dev/null +++ b/src/io/paper_sizes.rs @@ -0,0 +1,130 @@ +//! ISO paper sizes and sheet orientation for window plotting. + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum PaperSize { + A4, + A3, + A2, + A1, + A0, +} + +impl PaperSize { + pub const ALL: [PaperSize; 5] = [ + PaperSize::A4, + PaperSize::A3, + PaperSize::A2, + PaperSize::A1, + PaperSize::A0, + ]; + + pub fn label(self) -> &'static str { + match self { + PaperSize::A4 => "A4", + PaperSize::A3 => "A3", + PaperSize::A2 => "A2", + PaperSize::A1 => "A1", + PaperSize::A0 => "A0", + } + } + + /// Portrait dimensions in mm (width, height); width < height. + pub fn dimensions_mm(self) -> (f64, f64) { + match self { + PaperSize::A4 => (210.0, 297.0), + PaperSize::A3 => (297.0, 420.0), + PaperSize::A2 => (420.0, 594.0), + PaperSize::A1 => (594.0, 841.0), + PaperSize::A0 => (841.0, 1189.0), + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum Orientation { + Portrait, + Landscape, +} + +/// Sheet dimensions in mm for the given size and orientation. +pub fn sheet_mm(size: PaperSize, o: Orientation) -> (f64, f64) { + let (w, h) = size.dimensions_mm(); + match o { + Orientation::Portrait => (w, h), + Orientation::Landscape => (h, w), + } +} + +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum PlotScale { + /// Scale so the window fills the sheet minus a 5% margin. + Fit, + /// Exact scale factor (mm per drawing unit). + Ratio(f64), +} + +/// Map a world-space window onto a sheet. Returns (scale, offset_x, offset_y): +/// world (x, y) -> sheet mm ((x - window_min.0) * scale + offset_x, (y - window_min.1) * scale + offset_y). +/// The scaled window is centered on the sheet. +pub fn window_to_sheet( + window_wh: (f64, f64), + window_min: (f64, f64), + sheet_mm: (f64, f64), + scale: PlotScale, +) -> (f64, f64, f64) { + let (ww, wh) = (window_wh.0.max(1e-9), window_wh.1.max(1e-9)); + let scale = match scale { + PlotScale::Ratio(r) => r.max(1e-9), + PlotScale::Fit => { + const MARGIN: f64 = 1.05; + let sx = (sheet_mm.0 / MARGIN) / ww; + let sy = (sheet_mm.1 / MARGIN) / wh; + sx.min(sy) + } + }; + let scaled_w = ww * scale; + let scaled_h = wh * scale; + let offset_x = (sheet_mm.0 - scaled_w) / 2.0; + let offset_y = (sheet_mm.1 - scaled_h) / 2.0; + let _ = window_min; // offset already places the window min at (offset_x, offset_y) + (scale, offset_x, offset_y) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn iso_dimensions_and_orientation() { + assert_eq!(PaperSize::A4.dimensions_mm(), (210.0, 297.0)); + assert_eq!(PaperSize::A0.dimensions_mm(), (841.0, 1189.0)); + assert_eq!(PaperSize::ALL.len(), 5); + assert_eq!(PaperSize::A3.label(), "A3"); + // Portrait keeps (w,h); landscape swaps. + assert_eq!(sheet_mm(PaperSize::A4, Orientation::Portrait), (210.0, 297.0)); + assert_eq!(sheet_mm(PaperSize::A4, Orientation::Landscape), (297.0, 210.0)); + } + + #[test] + fn fit_centers_and_scales_within_margin() { + // 100×100 window onto a 210×297 sheet, Fit. Limiting axis is width: + // usable = 210/1.05 = 200; scale = 200/100 = 2.0. + let (s, ox, oy) = window_to_sheet((100.0, 100.0), (0.0, 0.0), (210.0, 297.0), PlotScale::Fit); + assert!((s - 2.0).abs() < 1e-9, "scale {s}"); + // Window is 100*2 = 200 wide/tall; centered on 210×297. + assert!((ox - (210.0 - 200.0) / 2.0).abs() < 1e-9, "ox {ox}"); + assert!((oy - (297.0 - 200.0) / 2.0).abs() < 1e-9, "oy {oy}"); + } + + #[test] + fn ratio_applies_exact_scale_and_offsets_window_min() { + // Ratio(0.5): scale is exactly 0.5; a window starting at (10,20) + // maps its min corner to the offset so (10,20) -> (ox,oy). + let (s, ox, oy) = window_to_sheet((100.0, 80.0), (10.0, 20.0), (210.0, 297.0), PlotScale::Ratio(0.5)); + assert!((s - 0.5).abs() < 1e-9); + // Centered: sheet_center - scaled_window_center, then min maps to offset. + // scaled window = 50×40; centered box origin = ((210-50)/2,(297-40)/2). + assert!((ox - (210.0 - 50.0) / 2.0).abs() < 1e-9, "ox {ox}"); + assert!((oy - (297.0 - 40.0) / 2.0).abs() < 1e-9, "oy {oy}"); + } +} diff --git a/src/io/pdf_export.rs b/src/io/pdf_export.rs index 646e4e233..9fc7def9b 100644 --- a/src/io/pdf_export.rs +++ b/src/io/pdf_export.rs @@ -35,6 +35,8 @@ pub fn export_pdf( _offset_x: f32, _offset_y: f32, _rotation_deg: i32, + _scale: f32, + _clip: Option<(f32, f32, f32, f32)>, _path: &Path, _plot_style: Option<&PlotStyleTable>, ) -> Result<(), String> { @@ -64,6 +66,8 @@ pub fn export_pdf( offset_x: f32, offset_y: f32, rotation_deg: i32, + scale: f32, + clip: Option<(f32, f32, f32, f32)>, path: &Path, plot_style: Option<&PlotStyleTable>, ) -> Result<(), String> { @@ -76,6 +80,8 @@ pub fn export_pdf( offset_x, offset_y, rotation_deg, + scale, + clip, plot_style, ); let mut file = std::fs::File::create(path).map_err(|e| e.to_string())?; @@ -107,6 +113,8 @@ fn build_pdf( ox: f32, oy: f32, rotation_deg: i32, + scale: f32, + clip: Option<(f32, f32, f32, f32)>, plot_style: Option<&PlotStyleTable>, ) -> Vec { let mut doc = PdfDocument::new("Open CAD Studio Export"); @@ -133,32 +141,67 @@ fn build_pdf( join: LineJoinStyle::Round, }); - // Apply rotation transform if needed. + // Apply rotation/scale/clip transform if needed. // PDF uses mm-based coordinate system with origin at bottom-left. - // We save state, apply a CTM, then restore it after drawing. - let needs_rotation = rotation_deg != 0; - if needs_rotation { + // We save state, apply a CTM (+ optional clip path), then restore after drawing. + let needs_state = rotation_deg != 0 || (scale - 1.0).abs() > 1e-6 || clip.is_some(); + if needs_state { let (cos_a, sin_a, tx, ty) = match rotation_deg { 90 => (0.0_f64, 1.0_f64, 0.0, paper_h as f64), 180 => (-1.0_f64, 0.0_f64, paper_w as f64, paper_h as f64), 270 => (0.0_f64, -1.0_f64, paper_w as f64, 0.0), _ => (1.0_f64, 0.0_f64, 0.0, 0.0), }; - // PDF CTM: [a b c d e f] = [cos sin -sin cos tx ty] + let s = scale as f64; + // PDF CTM: [a b c d e f] = [cos*s sin*s -sin*s cos*s tx ty] ops.push(Op::SaveGraphicsState); // Convert mm translation to points (1 mm = 2.834645 pt). let tx_pt = (tx * 2.834645) as f32; let ty_pt = (ty * 2.834645) as f32; ops.push(Op::SetTransformationMatrix { matrix: printpdf::CurTransMat::Raw([ - cos_a as f32, - sin_a as f32, - -(sin_a as f32), - cos_a as f32, + (cos_a * s) as f32, + (sin_a * s) as f32, + (-(sin_a) * s) as f32, + (cos_a * s) as f32, tx_pt, ty_pt, ]), }); + // Clip rectangle (mm), applied in the pre-scale coordinate space so it + // matches the wires drawn under the same CTM. + if let Some((cx, cy, cw, ch)) = clip { + const MM_TO_PT: f32 = 2.834645; + ops.push(Op::DrawPolygon { + polygon: Polygon { + rings: vec![PolygonRing { + points: vec![ + LinePoint { + p: Point { x: Pt(cx * MM_TO_PT), y: Pt(cy * MM_TO_PT) }, + bezier: false, + }, + LinePoint { + p: Point { x: Pt((cx + cw) * MM_TO_PT), y: Pt(cy * MM_TO_PT) }, + bezier: false, + }, + LinePoint { + p: Point { + x: Pt((cx + cw) * MM_TO_PT), + y: Pt((cy + ch) * MM_TO_PT), + }, + bezier: false, + }, + LinePoint { + p: Point { x: Pt(cx * MM_TO_PT), y: Pt((cy + ch) * MM_TO_PT) }, + bezier: false, + }, + ], + }], + mode: PaintMode::Clip, + winding_order: WindingOrder::NonZero, + }, + }); + } } // mm to PDF points (1 mm = 2.834645 pt). @@ -203,7 +246,7 @@ fn build_pdf( } lw_override = ctb .resolve_lineweight(wire.aci) - .map(|mm| (mm * MM_TO_PT).max(0.1)); + .map(|mm| (mm * MM_TO_PT).max(0.1) / scale.max(1e-6)); } } // Near-white and near-yellow (viewport active border) → dark grey for print @@ -239,8 +282,13 @@ fn build_pdf( last_color = Some([r, g, b]); } - // Line weight: CTB override (in pt) or screen px → points. - let lw_pt = lw_override.unwrap_or_else(|| (wire.line_weight_px * LW_PX_TO_PT).max(0.1)); + // Line weight: CTB override (in pt) or screen px → points. Divided by + // `scale` in both branches so pen widths stay absolute under the + // scaled CTM above (AutoCAD keeps lineweights independent of plot + // scale) — without this a Fit plot of a large window renders + // near-invisible hairlines. + let lw_pt = lw_override + .unwrap_or_else(|| (wire.line_weight_px * LW_PX_TO_PT).max(0.1) / scale.max(1e-6)); if last_lw.map(|l| (l - lw_pt).abs() > 0.01).unwrap_or(true) { ops.push(Op::SetOutlineThickness { pt: Pt(lw_pt) }); last_lw = Some(lw_pt); @@ -275,7 +323,7 @@ fn build_pdf( flush_line(&mut ops, &segment); } - if needs_rotation { + if needs_state { ops.push(Op::RestoreGraphicsState); } @@ -440,3 +488,34 @@ fn emit_hatch(ops: &mut Vec, hatch: &HatchModel, ox: f32, oy: f32) { }, }); } + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use super::*; + + #[test] + fn clip_and_scale_emit_pdf_bytes() { + let w = WireModel::solid( + "test".into(), + vec![[0.0, 0.0, 0.0], [50.0, 50.0, 0.0]], + WireModel::WHITE, + false, + ); + let bytes = build_pdf( + &[w], + &[], + &[], + 210.0, + 297.0, + 0.0, + 0.0, + 0, + 2.0, + Some((10.0, 10.0, 100.0, 100.0)), + None, + ); + // A valid PDF is produced (starts with the PDF header) and is non-trivial. + assert!(bytes.starts_with(b"%PDF"), "not a PDF"); + assert!(bytes.len() > 200, "suspiciously small: {}", bytes.len()); + } +} diff --git a/src/io/print_to_printer.rs b/src/io/print_to_printer.rs index 2e0f95242..c175edd05 100644 --- a/src/io/print_to_printer.rs +++ b/src/io/print_to_printer.rs @@ -57,6 +57,8 @@ pub async fn print_wires( offset_x, offset_y, rotation_deg, + 1.0, + None, &tmp_path, plot_style.as_ref(), )?; diff --git a/src/modules/view/mod.rs b/src/modules/view/mod.rs index 234924a02..e3fdb5beb 100644 --- a/src/modules/view/mod.rs +++ b/src/modules/view/mod.rs @@ -33,7 +33,7 @@ mod zoom_in; mod zoom_out; pub mod zoom_window; -use crate::modules::{CadModule, RibbonGroup, RibbonItem}; +use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef}; pub struct ViewModule; @@ -137,6 +137,19 @@ impl CadModule for ViewModule { RibbonItem::Tool(cascade::tool()), ], }, + // ── Plot ────────────────────────────────────────────────────────── + // Model space has no paper-space side toolbar, so Page Setup + // (format/orientation/pick window for PLOTWINDOW) needs an + // entry here too. + RibbonGroup { + title: "Plot", + tools: vec![RibbonItem::Tool(ToolDef { + id: "PAGESETUP", + label: "Page Setup", + icon: IconKind::Svg(include_bytes!("../../../assets/icons/pagesetup.svg")), + event: ModuleEvent::Command("PAGESETUP".to_string()), + })], + }, ] }) } diff --git a/src/modules/view/plot_window.rs b/src/modules/view/plot_window.rs index 5445ebc77..5582329cf 100644 --- a/src/modules/view/plot_window.rs +++ b/src/modules/view/plot_window.rs @@ -1,7 +1,7 @@ // PLOTWINDOW command — pick two corners to define the plot window area. // -// Works only in paper space layouts. After picking P1 and P2, writes the -// window to the layout's PlotSettings (PlotType::Window). +// In paper space it writes the layout's PlotSettings (PlotType::Window); in +// model space the host stores the window for the plot dialog. use crate::command::{CadCommand, CmdResult}; use crate::scene::model::wire_model::WireModel; diff --git a/src/ui/window/page_setup.rs b/src/ui/window/page_setup.rs index 1ebc4b69b..f4f83950c 100644 --- a/src/ui/window/page_setup.rs +++ b/src/ui/window/page_setup.rs @@ -1,6 +1,7 @@ //! Page Setup window — fills the entire OS window. use crate::app::Message; +use crate::io::paper_sizes::{Orientation, PaperSize}; use iced::widget::{button, column, container, row, scrollable, text, text_input, Space}; use iced::{Background, Border, Color, Element, Fill, Theme}; @@ -154,6 +155,8 @@ pub fn view_window<'a>( offset_y: &'a str, rotation: &'a str, scale: &'a str, + plot_format: PaperSize, + plot_orientation: Orientation, ) -> Element<'a, Message> { // ── Toolbar ─────────────────────────────────────────────────────────── let toolbar = container( @@ -286,6 +289,46 @@ pub fn view_window<'a>( ] .spacing(4); + // ── Model-space window plot ─────────────────────────────────────────── + // Sheet size/orientation for PLOTWINDOW's clipped export; the scale + // pills above (`scale_row1`/`scale_row2`) double as its plot scale. + let format_row = { + let mut r = row![lbl("Format")].spacing(4).align_y(iced::Center); + for size in PaperSize::ALL { + r = r.push( + button(text(size.label()).size(10)) + .on_press(Message::PlotFormat(size)) + .style(pill(plot_format == size)) + .padding([3, 8]), + ); + } + r + }; + let orient_row = row![ + lbl("Orientation"), + button(text("Portrait").size(10)) + .on_press(Message::PlotOrientation(Orientation::Portrait)) + .style(pill(plot_orientation == Orientation::Portrait)) + .padding([3, 8]), + button(text("Landscape").size(10)) + .on_press(Message::PlotOrientation(Orientation::Landscape)) + .style(pill(plot_orientation == Orientation::Landscape)) + .padding([3, 8]), + ] + .spacing(4) + .align_y(iced::Center); + let window_row = row![ + button(text("Pick window").size(10)) + .on_press(Message::Command("PLOTWINDOW".into())) + .style(btn(false)) + .padding([4, 10]), + button(text("Plot window → PDF").size(10)) + .on_press(Message::PlotWindowExport) + .style(btn(true)) + .padding([4, 10]), + ] + .spacing(6); + // ── Main scrollable form ────────────────────────────────────────────── let form = column![ section_label("Paper Size"), @@ -366,6 +409,11 @@ pub fn view_window<'a>( section_label("Plot Scale"), scale_row1, scale_row2, + hdivider(), + section_label("Model-Space Window Plot"), + format_row, + orient_row, + window_row, ] .spacing(10) .padding(16)