forked from 1b-agent/opencadstudio-upstream
feat: plot a model-space window to PDF
Pick a rectangular window in model space with the existing PLOTWINDOW command (now enabled in model space, previously paper-space only) and export it to a PDF at a chosen ISO paper size (A4-A0), orientation and scale (fit or ratio). The window is a hard clip boundary. Adds paper-size presets, a window-to-sheet transform, scale + clip support in the PDF exporter, and Page Setup controls (format/orientation/pick window).
This commit is contained in:
1 parent
ced42daff5
commit
363593b09f
13 files changed
+444
-24
No files matched your search
@@ -1148,8 +1148,19 @@ impl OpenCADStudio {
|
||||
use acadrust::objects::{ObjectType, PlotSettings};
|
||||
let layout_name = self.tabs[i].scene.current_layout.clone();
|
||||
if layout_name == "Model" {
|
||||
self.command_line
|
||||
.push_error("PLOTWINDOW: switch to a paper space layout first.");
|
||||
// Model space: remember the window (world X/Y) for the plot dialog.
|
||||
let x0 = p1.x.min(p2.x);
|
||||
let y0 = p1.y.min(p2.y);
|
||||
let x1 = p1.x.max(p2.x);
|
||||
let y1 = p1.y.max(p2.y);
|
||||
self.plot_window = Some((x0, y0, x1, y1));
|
||||
self.command_line.push_output(&format!(
|
||||
"Plot window: {x0:.2},{y0:.2} to {x1:.2},{y1:.2}"
|
||||
));
|
||||
// Pick window closed Page Setup so the viewport could
|
||||
// receive the two clicks — bring it back for scale
|
||||
// review and the "Plot window → PDF" button.
|
||||
self.active_modal = Some(super::ModalKind::PageSetup);
|
||||
} else {
|
||||
let block_handle = self.tabs[i].scene.current_layout_block_handle_pub();
|
||||
let doc = &mut self.tabs[i].scene.document;
|
||||
|
||||
@@ -509,13 +509,11 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
// Model space now has its own dialog section (window-plot format /
|
||||
// orientation / pick), so PAGESETUP opens there too, not just on
|
||||
// paper-space layouts.
|
||||
"PAGESETUP" => {
|
||||
if self.tabs[i].scene.current_layout == "Model" {
|
||||
self.command_line
|
||||
.push_error("PAGESETUP: switch to a paper space layout first.");
|
||||
} else {
|
||||
return Some(Task::done(Message::PageSetupOpen));
|
||||
}
|
||||
return Some(Task::done(Message::PageSetupOpen));
|
||||
}
|
||||
|
||||
// ── Recognized commands whose full implementation is pending ─────────
|
||||
|
||||
@@ -520,6 +520,10 @@ pub(super) struct OpenCADStudio {
|
||||
page_setup_rotation: String,
|
||||
/// Plot scale: "Fit" | "1:1" | "1:2" | "1:4" | "1:5" | "1:10" | "1:20" | "1:50" | "1:100" | "2:1".
|
||||
page_setup_scale: String,
|
||||
/// Pending model-space plot window (x0, y0, x1, y1) in world XY, or None.
|
||||
plot_window: Option<(f64, f64, f64, f64)>,
|
||||
plot_format: crate::io::paper_sizes::PaperSize,
|
||||
plot_orientation: crate::io::paper_sizes::Orientation,
|
||||
|
||||
// ── Plot Style Table ──────────────────────────────────────────────────
|
||||
/// Currently loaded CTB/STB table (None = no override).
|
||||
@@ -1745,6 +1749,14 @@ pub enum Message {
|
||||
PlotExport,
|
||||
/// Callback after the user picks (or cancels) the export path.
|
||||
PlotExportPath(Option<std::path::PathBuf>),
|
||||
/// User picked a paper size for the model-space window plot.
|
||||
PlotFormat(crate::io::paper_sizes::PaperSize),
|
||||
/// User picked a sheet orientation for the model-space window plot.
|
||||
PlotOrientation(crate::io::paper_sizes::Orientation),
|
||||
/// Export the pending model-space plot window (from PLOTWINDOW) to PDF.
|
||||
PlotWindowExport,
|
||||
/// Callback after the user picks (or cancels) the window-export path.
|
||||
PlotWindowExportPath(Option<std::path::PathBuf>),
|
||||
/// Send current layout to the system printer (via lp / lpr).
|
||||
PrintToPrinter,
|
||||
/// Callback from the async printer job.
|
||||
@@ -2089,6 +2101,9 @@ impl OpenCADStudio {
|
||||
page_setup_offset_y: "0.0".to_string(),
|
||||
page_setup_rotation: "0".to_string(),
|
||||
page_setup_scale: "Fit".to_string(),
|
||||
plot_window: None,
|
||||
plot_format: crate::io::paper_sizes::PaperSize::A4,
|
||||
plot_orientation: crate::io::paper_sizes::Orientation::Landscape,
|
||||
opening: None,
|
||||
pending_close: None,
|
||||
save_dialog_format: "DWG 2018".to_string(),
|
||||
|
||||
@@ -1026,6 +1026,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
draw_ox as f32,
|
||||
draw_oy as f32,
|
||||
rotation_deg,
|
||||
1.0,
|
||||
None,
|
||||
&path,
|
||||
self.active_plot_style.as_ref(),
|
||||
) {
|
||||
@@ -1038,6 +1040,93 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Export the pending model-space plot window (set by PLOTWINDOW while on
|
||||
/// the Model tab) to PDF, using the chosen paper size/orientation/scale.
|
||||
pub(super) fn on_plot_window_export_path_some(&mut self, path: std::path::PathBuf) -> Task<Message> {
|
||||
use crate::io::paper_sizes::{sheet_mm, window_to_sheet, PlotScale};
|
||||
let i = self.active_tab;
|
||||
if self.tabs[i].scene.current_layout != "Model" {
|
||||
self.command_line
|
||||
.push_error("Plot window export is for model space.");
|
||||
return Task::none();
|
||||
}
|
||||
let Some((x0, y0, x1, y1)) = self.plot_window else {
|
||||
self.command_line.push_error("No plot window. Pick one first.");
|
||||
return Task::none();
|
||||
};
|
||||
if (x1 - x0) < 1e-6 || (y1 - y0) < 1e-6 {
|
||||
self.command_line
|
||||
.push_error("Plot window is empty. Pick a larger window.");
|
||||
return Task::none();
|
||||
}
|
||||
let (sheet_w, sheet_h) = sheet_mm(self.plot_format, self.plot_orientation);
|
||||
let win_w = (x1 - x0).max(1e-9);
|
||||
let win_h = (y1 - y0).max(1e-9);
|
||||
let scale_sel = if self.page_setup_scale.trim().eq_ignore_ascii_case("fit") {
|
||||
PlotScale::Fit
|
||||
} else {
|
||||
let (num, den) = parse_plot_scale(&self.page_setup_scale);
|
||||
if num > 0.0 && den > 0.0 {
|
||||
PlotScale::Ratio(num / den)
|
||||
} else {
|
||||
PlotScale::Fit
|
||||
}
|
||||
};
|
||||
let (scale, ox, oy) =
|
||||
window_to_sheet((win_w, win_h), (x0, y0), (sheet_w, sheet_h), scale_sel);
|
||||
|
||||
let scene = &self.tabs[i].scene;
|
||||
// Cull to the window so a small-area plot doesn't write the whole
|
||||
// drawing into the PDF; the clip below trims the partial crossers.
|
||||
// aabb is world X/Y [minx, miny, maxx, maxy].
|
||||
let (wx0, wy0, wx1, wy1) = (x0 as f32, y0 as f32, x1 as f32, y1 as f32);
|
||||
let wires: Vec<_> = scene
|
||||
.entity_wires()
|
||||
.into_iter()
|
||||
.filter(|w| {
|
||||
w.aabb[0] <= wx1 && w.aabb[2] >= wx0 && w.aabb[1] <= wy1 && w.aabb[3] >= wy0
|
||||
})
|
||||
.collect();
|
||||
let hatches = scene.paper_canvas_hatches();
|
||||
let wipeouts = scene.paper_canvas_wipeouts();
|
||||
|
||||
// build_pdf maps a wire coordinate to sheet mm as (coord + offset) *
|
||||
// scale (rotation_deg == 0 adds no further CTM translation).
|
||||
// window_to_sheet's (ox, oy) is the sheet-mm target for the window's
|
||||
// min corner, so: scale * (x0 + offset_x) = ox => offset_x = ox/scale - x0.
|
||||
let offset_x = ((ox / scale) - x0) as f32;
|
||||
let offset_y = ((oy / scale) - y0) as f32;
|
||||
// Clip rect in pre-scale space (build_pdf's CTM applies `scale` to it,
|
||||
// same as the wires) so the final sheet-mm rect lands at (ox, oy).
|
||||
let clip = Some(((ox / scale) as f32, (oy / scale) as f32, win_w as f32, win_h as f32));
|
||||
|
||||
let res = crate::io::pdf_export::export_pdf(
|
||||
&wires,
|
||||
hatches.as_slice(),
|
||||
wipeouts.as_slice(),
|
||||
sheet_w,
|
||||
sheet_h,
|
||||
offset_x,
|
||||
offset_y,
|
||||
0,
|
||||
scale as f32,
|
||||
clip,
|
||||
&path,
|
||||
self.active_plot_style.as_ref(),
|
||||
);
|
||||
match res {
|
||||
Ok(()) => {
|
||||
self.command_line.push_info(&format!(
|
||||
"Plotted window to {}",
|
||||
path.file_name().unwrap_or_default().to_string_lossy()
|
||||
));
|
||||
self.close_active_modal();
|
||||
}
|
||||
Err(e) => self.command_line.push_error(&format!("Plot failed: {e}")),
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(super) fn on_print_to_printer(&mut self) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let scene = &self.tabs[i].scene;
|
||||
|
||||
@@ -831,6 +831,14 @@ impl OpenCADStudio {
|
||||
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
|
||||
// Any command also dismisses the Isolate action menu.
|
||||
self.isolate_popup_open = false;
|
||||
// "Pick window" (PLOTWINDOW) from Page Setup needs the backdrop
|
||||
// gone so the viewport pick lands; every other command leaves an
|
||||
// open modal (and its staged edits) untouched.
|
||||
if cmd.trim().eq_ignore_ascii_case("PLOTWINDOW")
|
||||
|| cmd.trim().eq_ignore_ascii_case("PW")
|
||||
{
|
||||
self.close_active_modal();
|
||||
}
|
||||
self.dispatch_command(&cmd)
|
||||
}
|
||||
|
||||
@@ -2987,6 +2995,30 @@ impl OpenCADStudio {
|
||||
Message::PlotExportPath(None) => Task::none(),
|
||||
Message::PlotExportPath(Some(path)) => self.on_plot_export_path_some(path),
|
||||
|
||||
Message::PlotFormat(f) => {
|
||||
self.plot_format = f;
|
||||
Task::none()
|
||||
}
|
||||
Message::PlotOrientation(o) => {
|
||||
self.plot_orientation = o;
|
||||
Task::none()
|
||||
}
|
||||
Message::PlotWindowExport => {
|
||||
let i = self.active_tab;
|
||||
let stem = self.tabs[i]
|
||||
.current_path
|
||||
.as_deref()
|
||||
.and_then(|p: &std::path::Path| p.file_stem())
|
||||
.map(|s: &std::ffi::OsStr| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| "drawing".into());
|
||||
Task::perform(
|
||||
crate::io::pdf_export::pick_pdf_path_owned(stem),
|
||||
Message::PlotWindowExportPath,
|
||||
)
|
||||
}
|
||||
Message::PlotWindowExportPath(None) => Task::none(),
|
||||
Message::PlotWindowExportPath(Some(path)) => self.on_plot_window_export_path_some(path),
|
||||
|
||||
// ── Print to system printer ───────────────────────────────────────
|
||||
Message::PrintToPrinter => self.on_print_to_printer(),
|
||||
Message::PrintResult(Ok(printer)) => {
|
||||
|
||||
@@ -57,6 +57,8 @@ impl OpenCADStudio {
|
||||
&self.page_setup_offset_y,
|
||||
&self.page_setup_rotation,
|
||||
&self.page_setup_scale,
|
||||
self.plot_format,
|
||||
self.plot_orientation,
|
||||
),
|
||||
520,
|
||||
460,
|
||||
|
||||
Reference in new issue
Block a user