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 ──────────────────────────────────────────
|
// ── Plot / Page Setup ──────────────────────────────────────────
|
||||||
"PRINT"|"PLOT"|"EXPORT" => {
|
"PLOT"|"EXPORT" => {
|
||||||
return Task::done(Message::PlotExport);
|
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
|
// PLOTSTYLE — load or clear CTB/STB plot style table
|
||||||
cmd if cmd == "PLOTSTYLE" || cmd.starts_with("PLOTSTYLE ") => {
|
cmd if cmd == "PLOTSTYLE" || cmd.starts_with("PLOTSTYLE ") => {
|
||||||
let sub = cmd.split_once(' ')
|
let sub = cmd.split_once(' ')
|
||||||
|
|
|
||||||
|
|
@ -402,6 +402,10 @@ pub enum Message {
|
||||||
PlotExport,
|
PlotExport,
|
||||||
/// Callback after the user picks (or cancels) the export path.
|
/// Callback after the user picks (or cancels) the export path.
|
||||||
PlotExportPath(Option<std::path::PathBuf>),
|
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 ─────────────────────────────────────────────────
|
// ── Plot Style Table ─────────────────────────────────────────────────
|
||||||
/// Open file dialog to load a CTB/STB plot style table.
|
/// Open file dialog to load a CTB/STB plot style table.
|
||||||
PlotStyleLoad,
|
PlotStyleLoad,
|
||||||
|
|
|
||||||
|
|
@ -2722,6 +2722,70 @@ impl H7CAD {
|
||||||
Task::none()
|
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 ──────────────────────────────────────────────
|
// ── Plot Style Table ──────────────────────────────────────────────
|
||||||
Message::PlotStyleLoad => {
|
Message::PlotStyleLoad => {
|
||||||
Task::perform(
|
Task::perform(
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
pub mod obj;
|
pub mod obj;
|
||||||
pub mod pdf_export;
|
pub mod pdf_export;
|
||||||
pub mod plot_style;
|
pub mod plot_style;
|
||||||
|
pub mod print_to_printer;
|
||||||
pub mod stl;
|
pub mod stl;
|
||||||
pub mod xref;
|
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