feat: PRINT command — send layout to system printer via lp/lpr
The PRINT command renders the current layout to a temporary PDF (reusing the existing PDF export pipeline) and dispatches it to the OS default printer: - Linux/macOS: `lp` (CUPS) with `lpr` as fallback. - Windows: ShellExecute "print" verb (compiled-in, unused on Linux). PLOT and EXPORT still open the save-file dialog as before. Status messages are shown in the command line during the async job. Closes ROADMAP 1.8. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ff39759d02
commit
f515b556c7
5 changed files with 191 additions and 1 deletions
|
|
@ -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(' ')
|
||||
|
|
|
|||
|
|
@ -402,6 +402,10 @@ pub enum Message {
|
|||
PlotExport,
|
||||
/// Callback after the user picks (or cancels) the export path.
|
||||
PlotExportPath(Option<std::path::PathBuf>),
|
||||
/// Send current layout to the system printer (via lp / lpr).
|
||||
PrintToPrinter,
|
||||
/// Callback from the async printer job.
|
||||
PrintResult(Result<String, String>),
|
||||
// ── Plot Style Table ─────────────────────────────────────────────────
|
||||
/// Open file dialog to load a CTB/STB plot style table.
|
||||
PlotStyleLoad,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
117
src/io/print_to_printer.rs
Normal file
117
src/io/print_to_printer.rs
Normal file
|
|
@ -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<WireModel>,
|
||||
paper_w: f64,
|
||||
paper_h: f64,
|
||||
offset_x: f32,
|
||||
offset_y: f32,
|
||||
rotation_deg: i32,
|
||||
plot_style: Option<PlotStyleTable>,
|
||||
) -> Result<String, String> {
|
||||
// ── 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<String, String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Windows: ShellExecute with "print" verb.
|
||||
use std::ffi::OsStr;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
|
||||
let path_wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
|
||||
let verb: Vec<u16> = 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}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue