diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 7917b4356..e6a4007b8 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -1235,6 +1235,13 @@ impl OpenCADStudio { self.tabs[i].scene.clear_preview_wire(); self.restore_pre_cmd_tangent(); } + CmdResult::QuickPrint(handles) => { + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + self.restore_pre_cmd_tangent(); + return self.on_quick_print_handles(handles); + } CmdResult::StretchEntities { handles, win_min, diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs index acb0744b9..66a6f260a 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -654,6 +654,13 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(cmd)); } + "QUICKPRINT" | "QP" => { + use crate::modules::view::quick_print::QuickPrintCommand; + let cmd = QuickPrintCommand::new(); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + // Bare ZOOM enters the interactive window zoom (pick two corners) — // the common "zoom to a rectangle" action. The sub-keyword forms // (ZOOM EXTENTS / IN / OUT / …) are matched above. diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index 8085f75f4..6d329da59 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -512,6 +512,8 @@ inventory::submit!(crate::command::CommandRegistration { "PROPS", "PSPACE", "PURGE", + "QP", + "QUICKPRINT", "QS", "QSAVE", "QSELECT", diff --git a/src/app/update/file.rs b/src/app/update/file.rs index abf7ab08b..bb2515b45 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -1221,6 +1221,89 @@ pub(super) fn on_open_file(&mut self) -> Task { ) } + /// QUICKPRINT / QP — use the current selection's bounding box as the plot + /// window and export a PDF (drawing folder + name + timestamp) with the + /// active page setup, no dialog. Model space only. (#325) + pub(crate) fn on_quick_print_handles( + &mut self, + handles: Vec, + ) -> Task { + let i = self.active_tab; + if self.tabs[i].scene.current_layout != "Model" { + self.command_line + .push_error("Quick print works in model space."); + return Task::none(); + } + let set: std::collections::HashSet = handles.into_iter().collect(); + // Union the AABBs of the picked entities' wires (world XY), matched by + // each wire's handle. + let (x0, y0, x1, y1, any) = { + let scene = &self.tabs[i].scene; + let mut x0 = f32::INFINITY; + let mut y0 = f32::INFINITY; + let mut x1 = f32::NEG_INFINITY; + let mut y1 = f32::NEG_INFINITY; + let mut any = false; + for w in scene.entity_wires() { + let picked = crate::scene::Scene::handle_from_wire_name(&w.name) + .is_some_and(|h| set.contains(&h)); + if !picked { + continue; + } + let [ax0, ay0, ax1, ay1] = w.aabb; + if ax0.is_finite() && ay0.is_finite() && ax1.is_finite() && ay1.is_finite() { + x0 = x0.min(ax0); + y0 = y0.min(ay0); + x1 = x1.max(ax1); + y1 = y1.max(ay1); + any = true; + } + } + (x0, y0, x1, y1, any) + }; + if !any { + self.command_line + .push_error("Selection has no printable geometry."); + return Task::none(); + } + if !(x1 > x0 && y1 > y0) { + self.command_line + .push_error("Selection has no printable area."); + return Task::none(); + } + // Small margin so the outermost strokes aren't clipped flush to the edge. + let mx = ((x1 - x0) * 0.02).max(0.0); + let my = ((y1 - y0) * 0.02).max(0.0); + self.plot_window = Some(( + (x0 - mx) as f64, + (y0 - my) as f64, + (x1 + mx) as f64, + (y1 + my) as f64, + )); + let path = self.quick_print_path(); + self.on_plot_window_export_path_some(path) + } + + /// Auto output path for quick print: the drawing's folder + name + a + /// timestamp, falling back to the temp dir / "drawing" when unsaved. + fn quick_print_path(&self) -> std::path::PathBuf { + let i = self.active_tab; + let cur = self.tabs[i].current_path.as_deref(); + let dir = cur + .and_then(|p| p.parent()) + .map(|d| d.to_path_buf()) + .unwrap_or_else(std::env::temp_dir); + let stem = cur + .and_then(|p| p.file_stem()) + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_else(|| "drawing".into()); + let ts = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + dir.join(format!("{stem}_{ts}.pdf")) + } + /// Open the full Plot / Print dialog, seeding its state from the active /// layout's plot settings and the printers found on the system. pub(super) fn on_plot_dialog_open(&mut self) -> Task { diff --git a/src/command.rs b/src/command.rs index 27629f385..0f0cab207 100644 --- a/src/command.rs +++ b/src/command.rs @@ -241,6 +241,8 @@ pub enum CmdResult { }, /// Set the plot window on the active layout's PlotSettings. SetPlotWindow { p1: DVec3, p2: DVec3 }, + /// Quick-print the bounding box of the given selected entities to a PDF. + QuickPrint(Vec), /// Replace the text content of a Text/MText entity in-place. DdeditEntity { handle: Handle, new_text: String }, /// Open the in-place editor (plain box or rich MText editor, per type) for diff --git a/src/modules/view/mod.rs b/src/modules/view/mod.rs index e3fdb5beb..a5d09e51d 100644 --- a/src/modules/view/mod.rs +++ b/src/modules/view/mod.rs @@ -9,6 +9,7 @@ mod ortho; mod pan; mod persp; pub mod plot_window; +pub mod quick_print; mod properties_palette; mod sheetset; mod solid; diff --git a/src/modules/view/quick_print.rs b/src/modules/view/quick_print.rs new file mode 100644 index 000000000..886f3e171 --- /dev/null +++ b/src/modules/view/quick_print.rs @@ -0,0 +1,63 @@ +// QUICKPRINT / QP — run the command, select objects, then Enter to plot the +// selection's bounding box to a PDF (handled by the host). No dialog. (#325) + +use crate::command::{CadCommand, CmdResult}; +use acadrust::Handle; +use glam::DVec3; + +pub struct QuickPrintCommand { + /// Latest selection set, refreshed on every selection action. + handles: Vec, +} + +impl QuickPrintCommand { + pub fn new() -> Self { + Self { handles: Vec::new() } + } +} + +impl CadCommand for QuickPrintCommand { + fn name(&self) -> &'static str { + "QUICKPRINT" + } + + fn prompt(&self) -> String { + if self.handles.is_empty() { + "QUICKPRINT Select objects to quick-print:".into() + } else { + format!( + "QUICKPRINT {} selected — Enter to plot, or keep selecting:", + self.handles.len() + ) + } + } + + fn is_selection_gathering(&self) -> bool { + true + } + + fn on_selection_complete(&mut self, handles: Vec) -> CmdResult { + self.handles = handles; + CmdResult::NeedPoint + } + + fn on_point(&mut self, _pt: DVec3) -> CmdResult { + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + if self.handles.is_empty() { + CmdResult::Cancel + } else { + CmdResult::QuickPrint(std::mem::take(&mut self.handles)) + } + } + + fn on_escape(&mut self) -> CmdResult { + CmdResult::Cancel + } +} + +inventory::submit!(crate::command::CommandRegistration { + names: &["QUICKPRINT", "QP"] +}); // QuickPrintCommand