forked from 1b-agent/opencadstudio-upstream
feat(plot): complete plot workflow
This commit is contained in:
1 parent
eb0861cf6e
commit
bd1b5b4293
14 files changed
+1141
-541
No files matched your search
+77
-140
@@ -14,9 +14,8 @@ use crate::scene::model::hatch_model::HatchModel;
|
||||
use crate::scene::WireModel;
|
||||
|
||||
/// Extra options for a print job. On CUPS (Linux/macOS) these map to `lp`
|
||||
/// flags / `-o` options. On Windows only `printer` is honoured (via the
|
||||
/// "printto" shell verb); copies / quality / colour need the driver DEVMODE
|
||||
/// and are ignored there.
|
||||
/// flags / `-o` options. On Windows the generated PDF already carries render
|
||||
/// options, while copies and driver quality are not exposed by the shell verb.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||
pub struct PrintOptions {
|
||||
@@ -26,29 +25,12 @@ pub struct PrintOptions {
|
||||
pub copies: u32,
|
||||
/// Force grayscale output.
|
||||
pub mono: bool,
|
||||
/// Print quality: "Draft" | "Normal" | "High" | "Maximum".
|
||||
/// Print quality label selected in the plot dialog.
|
||||
pub quality: Option<String>,
|
||||
/// Rasterisation resolution in DPI.
|
||||
pub dpi: Option<u32>,
|
||||
}
|
||||
|
||||
// Printing routes through the native PDF pipeline + the OS print command, so it
|
||||
// is native-only; the web build gets a stub so the call site still compiles.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn print_wires(
|
||||
_wires: std::sync::Arc<Vec<WireModel>>,
|
||||
_hatches: Vec<HatchModel>,
|
||||
_wipeouts: Vec<HatchModel>,
|
||||
_paper_w: f64,
|
||||
_paper_h: f64,
|
||||
_offset_x: f64,
|
||||
_offset_y: f64,
|
||||
_rotation_deg: i32,
|
||||
_scale: f32,
|
||||
_clip: Option<(f32, f32, f32, f32)>,
|
||||
_plot_style: Option<PlotStyleTable>,
|
||||
) -> Result<String, String> {
|
||||
Err("Printing is not available in the web version.".into())
|
||||
/// Controls applied while building the intermediate PDF.
|
||||
pub render: crate::io::pdf_export::PdfPlotOptions,
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@@ -85,45 +67,6 @@ pub fn print_existing_pdf(_path: &std::path::Path, _opts: &PrintOptions) -> Resu
|
||||
Err("Printing is not available in the web version.".into())
|
||||
}
|
||||
|
||||
/// Render `wires` (plus hatch / wipeout fills) to a temp PDF and dispatch it
|
||||
/// to the default system printer.
|
||||
///
|
||||
/// Returns `Ok(printer_name)` on success or `Err(message)` on failure.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn print_wires(
|
||||
wires: std::sync::Arc<Vec<WireModel>>,
|
||||
hatches: Vec<HatchModel>,
|
||||
wipeouts: Vec<HatchModel>,
|
||||
paper_w: f64,
|
||||
paper_h: f64,
|
||||
offset_x: f64,
|
||||
offset_y: f64,
|
||||
rotation_deg: i32,
|
||||
scale: f32,
|
||||
clip: Option<(f32, f32, f32, f32)>,
|
||||
plot_style: Option<PlotStyleTable>,
|
||||
) -> Result<String, String> {
|
||||
// ── 1. Write to a named temp file ─────────────────────────────────────
|
||||
let tmp_path = std::env::temp_dir().join("open_cad_studio_print.pdf");
|
||||
pdf_export::export_pdf(
|
||||
&wires,
|
||||
&hatches,
|
||||
&wipeouts,
|
||||
paper_w,
|
||||
paper_h,
|
||||
offset_x,
|
||||
offset_y,
|
||||
rotation_deg,
|
||||
scale,
|
||||
clip,
|
||||
&tmp_path,
|
||||
plot_style.as_ref(),
|
||||
)?;
|
||||
|
||||
// ── 2. Dispatch to system printer ─────────────────────────────────────
|
||||
dispatch_to_printer(&tmp_path)
|
||||
}
|
||||
|
||||
/// Enumerate installed printers. Linux/macOS query CUPS via `lpstat -e`;
|
||||
/// Windows returns an empty list (the "printto" dispatch targets a named
|
||||
/// printer directly and the system default is always available).
|
||||
@@ -151,6 +94,46 @@ pub fn list_printers() -> Vec<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the operating system's printer configuration surface.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn open_printer_properties(printer: Option<&str>) -> Result<(), String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let mut command = std::process::Command::new("control.exe");
|
||||
command.arg("printers");
|
||||
return command
|
||||
.spawn()
|
||||
.map(|_| ())
|
||||
.map_err(|error| format!("Could not open printer properties: {error}"));
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let mut command = std::process::Command::new("open");
|
||||
command.arg("x-apple.systempreferences:com.apple.Print-Scan-Settings.extension");
|
||||
return command
|
||||
.spawn()
|
||||
.map(|_| ())
|
||||
.map_err(|error| format!("Could not open printer properties: {error}"));
|
||||
}
|
||||
#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
|
||||
{
|
||||
let target = printer
|
||||
.filter(|name| !name.is_empty())
|
||||
.map(|name| format!("http://localhost:631/printers/{name}"))
|
||||
.unwrap_or_else(|| "http://localhost:631/printers".to_string());
|
||||
std::process::Command::new("xdg-open")
|
||||
.arg(target)
|
||||
.spawn()
|
||||
.map(|_| ())
|
||||
.map_err(|error| format!("Could not open printer properties: {error}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn open_printer_properties(_printer: Option<&str>) -> Result<(), String> {
|
||||
Err("Printer properties are not available in the web version.".into())
|
||||
}
|
||||
|
||||
/// Like [`print_wires`] but honours a [`PrintOptions`] bundle (printer, copies,
|
||||
/// grayscale, quality, DPI).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -183,6 +166,7 @@ pub async fn print_wires_with(
|
||||
clip,
|
||||
&tmp_path,
|
||||
plot_style.as_ref(),
|
||||
opts.render,
|
||||
)?;
|
||||
dispatch_to_printer_opts(&tmp_path, &opts)
|
||||
}
|
||||
@@ -282,103 +266,56 @@ fn dispatch_to_printer_opts(
|
||||
// CUPS print-quality: 3 = draft, 4 = normal, 5 = high / best.
|
||||
let pq = match q {
|
||||
"Draft" => "3",
|
||||
"High" | "Maximum" => "5",
|
||||
"Presentation" | "Maximum" => "5",
|
||||
_ => "4",
|
||||
};
|
||||
cmd.arg("-o").arg(format!("print-quality={pq}"));
|
||||
}
|
||||
let out = cmd
|
||||
let lp_result = cmd
|
||||
.arg("--")
|
||||
.arg(path_str.as_ref())
|
||||
.output()
|
||||
.map_err(|e| format!("Could not launch lp: {e}"))?;
|
||||
if out.status.success() {
|
||||
.output();
|
||||
if let Ok(out) = &lp_result {
|
||||
if !out.status.success() {
|
||||
// Continue to the lpr fallback below.
|
||||
} else {
|
||||
let msg = String::from_utf8_lossy(&out.stdout);
|
||||
let printer = msg
|
||||
.split_whitespace()
|
||||
.find(|w| w.contains('-'))
|
||||
.unwrap_or("printer")
|
||||
.to_string();
|
||||
Ok(printer)
|
||||
} else {
|
||||
Err(String::from_utf8_lossy(&out.stderr).into_owned())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Platform-specific dispatch of a PDF path to the system printer.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
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")
|
||||
let mut fallback = std::process::Command::new("lpr");
|
||||
if let Some(printer) = opts.printer.as_deref().filter(|name| !name.is_empty()) {
|
||||
fallback.arg("-P").arg(printer);
|
||||
}
|
||||
if copies > 1 {
|
||||
fallback.arg(format!("-#{copies}"));
|
||||
}
|
||||
let out = fallback
|
||||
.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())
|
||||
.map_err(|error| match lp_result {
|
||||
Ok(ref lp) => format!(
|
||||
"lp failed: {}; lpr could not launch: {error}",
|
||||
String::from_utf8_lossy(&lp.stderr)
|
||||
),
|
||||
Err(ref lp) => format!("lp could not launch: {lp}; lpr could not launch: {error}"),
|
||||
})?;
|
||||
if out.status.success() {
|
||||
Ok(opts
|
||||
.printer
|
||||
.clone()
|
||||
.unwrap_or_else(|| "default printer".into()))
|
||||
} else {
|
||||
let err = String::from_utf8_lossy(&lpr.stderr).into_owned();
|
||||
Err(format!("lpr error: {err}"))
|
||||
Err(format!(
|
||||
"lpr failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user