feat(plot): complete plot workflow

This commit is contained in:
Hakan Seven committed 2026-07-31 12:06:12 +03:00
1 parent eb0861cf6e
commit bd1b5b4293
14 files changed
+1141 -541

No files matched your search

+77 -140
View File
@@ -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)
))
}
}
}