diff --git a/src/app/commands.rs b/src/app/commands.rs index a1883d332..d95d4a502 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -3794,9 +3794,13 @@ impl H7CAD { } // ── Plot / Page Setup ────────────────────────────────────────── - "PRINT"|"PLOT"|"EXPORT" => { + "PLOT"|"EXPORT" => { return Task::done(Message::PlotExport); } + // PRINT — send current layout to the system default printer. + "PRINT" => { + return Task::done(Message::PrintToPrinter); + } // PLOTSTYLE — load or clear CTB/STB plot style table cmd if cmd == "PLOTSTYLE" || cmd.starts_with("PLOTSTYLE ") => { let sub = cmd.split_once(' ') diff --git a/src/app/mod.rs b/src/app/mod.rs index 7d0e14c7d..3a43c0845 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -402,6 +402,10 @@ pub enum Message { PlotExport, /// Callback after the user picks (or cancels) the export path. PlotExportPath(Option), + /// Send current layout to the system printer (via lp / lpr). + PrintToPrinter, + /// Callback from the async printer job. + PrintResult(Result), // ── Plot Style Table ───────────────────────────────────────────────── /// Open file dialog to load a CTB/STB plot style table. PlotStyleLoad, diff --git a/src/app/update.rs b/src/app/update.rs index 41d785a79..d0ace3e0b 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -2722,6 +2722,70 @@ impl H7CAD { Task::none() } + // ── Print to system printer ─────────────────────────────────────── + Message::PrintToPrinter => { + let i = self.active_tab; + let scene = &self.tabs[i].scene; + let layout_name = scene.current_layout.clone(); + let wires = scene.entity_wires(); + use acadrust::objects::{ObjectType, PlotType}; + let ps_snap = scene.document.objects.values().find_map(|obj| { + if let ObjectType::PlotSettings(ps) = obj { + if ps.page_name == layout_name { Some(ps.clone()) } else { None } + } else { None } + }); + let (paper_w, paper_h, draw_ox, draw_oy, rotation_deg) = + if let Some(((x0, y0), (x1, y1))) = scene.paper_limits() { + let (pw, ph) = (x1 - x0, y1 - y0); + let use_extents = ps_snap.as_ref() + .map(|ps| matches!(ps.plot_type, PlotType::Extents)) + .unwrap_or(false); + let (ox, oy) = if use_extents { + if let Some((mn, _mx)) = scene.model_space_extents() { + (-mn.x as f64, -mn.y as f64) + } else { (-x0, -y0) } + } else { (-x0, -y0) }; + let rot = ps_snap.as_ref() + .map(|ps| ps.rotation.to_degrees() as i32) + .unwrap_or(0); + (pw, ph, ox, oy, rot) + } else { + if let Some((mn, mx)) = scene.model_space_extents() { + let margin = 1.05_f64; + let w = ((mx.x - mn.x) as f64 * margin).max(1.0); + let h = ((mx.y - mn.y) as f64 * margin).max(1.0); + let pad_x = (w - (mx.x - mn.x) as f64) * 0.5; + let pad_y = (h - (mx.y - mn.y) as f64) * 0.5; + (w, h, -(mn.x as f64) + pad_x, -(mn.y as f64) + pad_y, 0) + } else { (297.0, 210.0, 0.0, 0.0, 0) } + }; + let (eff_w, eff_h) = match rotation_deg { + 90 | 270 => (paper_h, paper_w), + _ => (paper_w, paper_h), + }; + let plot_style = self.active_plot_style.clone(); + self.command_line.push_info("Sending to system printer…"); + Task::perform( + async move { + crate::io::print_to_printer::print_wires( + wires, eff_w, eff_h, + draw_ox as f32, draw_oy as f32, + rotation_deg, plot_style, + ) + .await + }, + Message::PrintResult, + ) + } + Message::PrintResult(Ok(printer)) => { + self.command_line.push_info(&format!("Sent to printer: {printer}")); + Task::none() + } + Message::PrintResult(Err(e)) => { + self.command_line.push_error(&format!("Print failed: {e}")); + Task::none() + } + // ── Plot Style Table ────────────────────────────────────────────── Message::PlotStyleLoad => { Task::perform( diff --git a/src/io/mod.rs b/src/io/mod.rs index 664bd7392..5ac8de71e 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -6,6 +6,7 @@ pub mod obj; pub mod pdf_export; pub mod plot_style; +pub mod print_to_printer; pub mod stl; pub mod xref; diff --git a/src/io/print_to_printer.rs b/src/io/print_to_printer.rs new file mode 100644 index 000000000..60cb80ec8 --- /dev/null +++ b/src/io/print_to_printer.rs @@ -0,0 +1,117 @@ +// print_to_printer — send the current layout to the system printer. +// +// Strategy: +// 1. Render the drawing to a temporary PDF (reusing the PDF export pipeline). +// 2. Send that PDF to the system printer with `lp` (Linux/macOS) or +// `ShellExecute PRINT` (Windows). +// +// The function is async so the UI remains responsive while the job is queued. + +use crate::io::plot_style::PlotStyleTable; +use crate::io::pdf_export; +use crate::scene::WireModel; + +/// Render `wires` to a temp PDF and dispatch it to the default system printer. +/// +/// Returns `Ok(printer_name)` on success or `Err(message)` on failure. +pub async fn print_wires( + wires: Vec, + paper_w: f64, + paper_h: f64, + offset_x: f32, + offset_y: f32, + rotation_deg: i32, + plot_style: Option, +) -> Result { + // ── 1. Write to a named temp file ───────────────────────────────────── + let tmp_path = std::env::temp_dir().join("h7cad_print.pdf"); + pdf_export::export_pdf( + &wires, + paper_w, + paper_h, + offset_x, + offset_y, + rotation_deg, + &tmp_path, + plot_style.as_ref(), + )?; + + // ── 2. Dispatch to system printer ───────────────────────────────────── + dispatch_to_printer(&tmp_path) +} + +/// Platform-specific dispatch of a PDF path to the system printer. +fn dispatch_to_printer(path: &std::path::Path) -> Result { + #[cfg(target_os = "windows")] + { + // Windows: ShellExecute with "print" verb. + use std::ffi::OsStr; + use std::os::windows::ffi::OsStrExt; + + let path_wide: Vec = path.as_os_str().encode_wide().chain(Some(0)).collect(); + let verb: Vec = OsStr::new("print\0").encode_wide().collect(); + let result = unsafe { + windows_sys::Win32::UI::Shell::ShellExecuteW( + std::ptr::null_mut(), + verb.as_ptr(), + path_wide.as_ptr(), + std::ptr::null(), + std::ptr::null(), + windows_sys::Win32::UI::WindowsAndMessaging::SW_HIDE, + ) as usize + }; + if result > 32 { + Ok("default printer".to_string()) + } else { + Err(format!("ShellExecute PRINT failed (code {result})")) + } + } + + #[cfg(not(target_os = "windows"))] + { + // Linux / macOS: prefer `lp`, fall back to `lpr`. + let path_str = path.to_string_lossy(); + + // Try `lp` first (CUPS). + let lp = std::process::Command::new("lp") + .arg("--") + .arg(path_str.as_ref()) + .output(); + + match lp { + Ok(out) if out.status.success() => { + // `lp` prints the job ID on stdout, e.g. "request id is lp-42 (1 file(s))" + let msg = String::from_utf8_lossy(&out.stdout); + let printer = msg + .split_whitespace() + .find(|w| w.contains('-')) + .unwrap_or("default") + .to_string(); + return Ok(printer); + } + Ok(out) => { + let err = String::from_utf8_lossy(&out.stderr).into_owned(); + // Fall through to lpr. + if !err.is_empty() { + // Try lpr as alternative. + } + } + Err(_) => { + // lp not found — try lpr. + } + } + + // Fall back to `lpr`. + let lpr = std::process::Command::new("lpr") + .arg(path_str.as_ref()) + .output() + .map_err(|e| format!("Could not launch lp or lpr: {e}"))?; + + if lpr.status.success() { + Ok("default printer".to_string()) + } else { + let err = String::from_utf8_lossy(&lpr.stderr).into_owned(); + Err(format!("lpr error: {err}")) + } + } +}