forked from 1b-agent/opencadstudio-upstream
feat(plot): unified plot/print dialog, retire Page Setup
Replace the old Page Setup panel with a single OCS-styled plot/print dialog (dropdowns for printer, paper, orientation, rotation, plot area, scale, quality, shaded plot; checkboxes for plot options). PLOT / PRINT / PAGESETUP all open it. - Printer output via lp: -d printer, -n copies, -o ColorModel=Gray, print-quality, Resolution; Windows printto verb; list_printers via lpstat. Preview opens a temp PDF in the OS viewer (never a save dialog). PDF export reuses the tested pipeline. - Window pick returns to the new dialog (was the old Page Setup) and the clipped window plot drives PDF / printer / preview. - Remember the user's print preferences across sessions in config/plot.txt (printer, copies, quality, output options, area, scale); drawing-specific fields (offset/center/rotation) seed from the layout. Default plot area Window, scale Fit. - Delete src/ui/window/page_setup.rs and its ModalKind / Message / buffer state; fold on_page_setup_commit into apply_plot_page_settings. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
e9621d618b
commit
8820b43127
11 files changed
+1242
-660
No files matched your search
@@ -1187,10 +1187,11 @@ impl OpenCADStudio {
|
||||
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);
|
||||
// Pick window closed the plot dialog so the viewport could
|
||||
// receive the two clicks — bring the dialog back with the
|
||||
// window now active.
|
||||
self.plot_dialog.area = "Window".to_string();
|
||||
self.active_modal = Some(super::ModalKind::Plot);
|
||||
} else {
|
||||
let block_handle = self.tabs[i].scene.current_layout_block_handle_pub();
|
||||
let doc = &mut self.tabs[i].scene.document;
|
||||
|
||||
@@ -358,12 +358,13 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
// ── Plot / Page Setup ──────────────────────────────────────────
|
||||
"PLOT" | "EXPORT" | "EXPORTPDF" => {
|
||||
return Some(Task::done(Message::PlotExport));
|
||||
// PLOT / PRINT open the full plot dialog (printer, paper, scale,
|
||||
// options); EXPORT / EXPORTPDF stay a direct PDF export.
|
||||
"PLOT" | "PRINT" => {
|
||||
return Some(Task::done(Message::PlotDialogOpen));
|
||||
}
|
||||
// PRINT — send current layout to the system default printer.
|
||||
"PRINT" => {
|
||||
return Some(Task::done(Message::PrintToPrinter));
|
||||
"EXPORT" | "EXPORTPDF" => {
|
||||
return Some(Task::done(Message::PlotExport));
|
||||
}
|
||||
// PLOTSTYLE — load or clear CTB/STB plot style table
|
||||
cmd if cmd == "PLOTSTYLE" || cmd.starts_with("PLOTSTYLE ") => {
|
||||
@@ -509,11 +510,9 @@ 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 is folded into the unified plot dialog.
|
||||
"PAGESETUP" => {
|
||||
return Some(Task::done(Message::PageSetupOpen));
|
||||
return Some(Task::done(Message::PlotDialogOpen));
|
||||
}
|
||||
|
||||
// ── Recognized commands whose full implementation is pending ─────────
|
||||
|
||||
+11
-49
@@ -513,26 +513,14 @@ pub(super) struct OpenCADStudio {
|
||||
last_vp_click_time: Option<Instant>,
|
||||
/// Screen position of the previous viewport left-click release.
|
||||
last_vp_click_pos: Option<Point>,
|
||||
/// Editable paper width buffer for the Page Setup panel (string while typing).
|
||||
page_setup_w: String,
|
||||
/// Editable paper height buffer for the Page Setup panel (string while typing).
|
||||
page_setup_h: String,
|
||||
/// Plot area type: "Layout" | "Extents".
|
||||
page_setup_plot_area: String,
|
||||
/// Center the drawing on the page when exporting.
|
||||
page_setup_center: bool,
|
||||
/// Plot offset X in mm (applied after optional centering).
|
||||
page_setup_offset_x: String,
|
||||
/// Plot offset Y in mm.
|
||||
page_setup_offset_y: String,
|
||||
/// Plot rotation in degrees: "0" | "90" | "180" | "270".
|
||||
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,
|
||||
/// Plot scale for model-space window plots: "Fit" | "1:1" | … | "2:1".
|
||||
plot_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,
|
||||
/// Backing state for the full Plot / Print dialog.
|
||||
plot_dialog: crate::ui::window::plot::PlotDialogState,
|
||||
|
||||
// ── Plot Style Table ──────────────────────────────────────────────────
|
||||
/// Currently loaded CTB/STB table (None = no override).
|
||||
@@ -1058,7 +1046,7 @@ pub enum ModalKind {
|
||||
PluginManager,
|
||||
UpdateNotice,
|
||||
Layers,
|
||||
PageSetup,
|
||||
Plot,
|
||||
LayoutManager,
|
||||
Plotstyle,
|
||||
TextStyle,
|
||||
@@ -1791,30 +1779,6 @@ pub enum Message {
|
||||
/// User clicked the "Open release page" button — opens the GitHub
|
||||
/// release URL in the OS default browser and closes the notice.
|
||||
UpdateNoticeOpenRelease,
|
||||
// ── Page Setup ────────────────────────────────────────────────────────
|
||||
/// Open the Page Setup panel for the current layout.
|
||||
PageSetupOpen,
|
||||
/// Close (cancel) the Page Setup panel without applying changes.
|
||||
PageSetupClose,
|
||||
/// Live-edit of the paper width field.
|
||||
PageSetupWidthEdit(String),
|
||||
/// Live-edit of the paper height field.
|
||||
PageSetupHeightEdit(String),
|
||||
/// User selected a paper size preset (e.g. "A4 Portrait").
|
||||
PageSetupPreset(String),
|
||||
/// User changed the plot area type ("Layout" or "Extents").
|
||||
PageSetupPlotArea(String),
|
||||
/// Toggle center-on-page.
|
||||
PageSetupCenterToggle,
|
||||
/// Live-edit of plot offset X.
|
||||
PageSetupOffsetXEdit(String),
|
||||
/// Live-edit of plot offset Y.
|
||||
PageSetupOffsetYEdit(String),
|
||||
/// User changed plot rotation.
|
||||
PageSetupRotation(String),
|
||||
PageSetupScale(String),
|
||||
/// Apply the changes entered in Page Setup.
|
||||
PageSetupCommit,
|
||||
// ── Plot / Export ─────────────────────────────────────────────────────
|
||||
/// Show the SVG save-file dialog and trigger export.
|
||||
PlotExport,
|
||||
@@ -1832,6 +1796,10 @@ pub enum Message {
|
||||
PrintToPrinter,
|
||||
/// Callback from the async printer job.
|
||||
PrintResult(Result<String, String>),
|
||||
/// Open the full Plot / Print dialog (seeds state from the layout).
|
||||
PlotDialogOpen,
|
||||
/// An edit inside the Plot / Print dialog.
|
||||
PlotDlg(crate::ui::window::plot::PlotDlgMsg),
|
||||
// ── Plot Style Table ─────────────────────────────────────────────────
|
||||
/// Open file dialog to load a CTB/STB plot style table.
|
||||
PlotStyleLoad,
|
||||
@@ -2167,17 +2135,11 @@ impl OpenCADStudio {
|
||||
layout_rename_state: None,
|
||||
last_vp_click_time: None,
|
||||
last_vp_click_pos: None,
|
||||
page_setup_w: String::new(),
|
||||
page_setup_h: String::new(),
|
||||
page_setup_plot_area: "Layout".to_string(),
|
||||
page_setup_center: true,
|
||||
page_setup_offset_x: "0.0".to_string(),
|
||||
page_setup_offset_y: "0.0".to_string(),
|
||||
page_setup_rotation: "0".to_string(),
|
||||
page_setup_scale: "Fit".to_string(),
|
||||
plot_scale: "Fit".to_string(),
|
||||
plot_window: None,
|
||||
plot_format: crate::io::paper_sizes::PaperSize::A4,
|
||||
plot_orientation: crate::io::paper_sizes::Orientation::Landscape,
|
||||
plot_dialog: crate::ui::window::plot::PlotDialogState::load(),
|
||||
opening: None,
|
||||
pending_close: None,
|
||||
save_dialog_format: "DWG 2018".to_string(),
|
||||
|
||||
+455
-75
@@ -808,18 +808,28 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
iced::exit()
|
||||
}
|
||||
|
||||
pub(super) fn on_page_setup_commit(&mut self) -> Task<Message> {
|
||||
/// Write the given plot page settings into the active layout's Layout +
|
||||
/// PlotSettings objects (paper size, plot area, offset, rotation, scale).
|
||||
/// No-op on the Model tab (which has no paper layout). Marks the tab dirty
|
||||
/// and re-tessellates the sheet. Called by the Plot dialog on commit.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn apply_plot_page_settings(
|
||||
&mut self,
|
||||
w: f64,
|
||||
h: f64,
|
||||
extents: bool,
|
||||
center: bool,
|
||||
offset_x: f64,
|
||||
offset_y: f64,
|
||||
rotation: i16,
|
||||
scale_str: &str,
|
||||
) {
|
||||
let i = self.active_tab;
|
||||
let layout_name = self.tabs[i].scene.current_layout.clone();
|
||||
if layout_name != "Model" {
|
||||
let w: f64 = self.page_setup_w.parse::<f64>().unwrap_or(297.0).max(1.0);
|
||||
let h: f64 = self.page_setup_h.parse::<f64>().unwrap_or(210.0).max(1.0);
|
||||
let plot_area = self.page_setup_plot_area.clone();
|
||||
let center = self.page_setup_center;
|
||||
let offset_x = self.page_setup_offset_x.parse::<f64>().unwrap_or(0.0);
|
||||
let offset_y = self.page_setup_offset_y.parse::<f64>().unwrap_or(0.0);
|
||||
let rotation: i16 = self.page_setup_rotation.parse().unwrap_or(0);
|
||||
let scale_str = self.page_setup_scale.clone();
|
||||
let w: f64 = w.max(1.0);
|
||||
let h: f64 = h.max(1.0);
|
||||
let plot_area = if extents { "Extents" } else { "Layout" };
|
||||
|
||||
// Update the Layout object's limits AND its embedded
|
||||
// PlotSettings fields. `paper_limits()` (sheet rendering) and
|
||||
@@ -905,7 +915,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
};
|
||||
// Apply plot scale.
|
||||
use acadrust::objects::ScaledType;
|
||||
let (num, den) = parse_plot_scale(&scale_str);
|
||||
let (num, den) = parse_plot_scale(scale_str);
|
||||
if scale_str == "Fit" {
|
||||
ps.set_scale_to_fit();
|
||||
} else {
|
||||
@@ -924,8 +934,6 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
center={center} rot={rotation}°"
|
||||
));
|
||||
}
|
||||
self.active_modal = None;
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(super) fn on_plot_export_path_some(&mut self, path: std::path::PathBuf) -> Task<Message> {
|
||||
@@ -1062,10 +1070,10 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
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") {
|
||||
let scale_sel = if self.plot_scale.trim().eq_ignore_ascii_case("fit") {
|
||||
PlotScale::Fit
|
||||
} else {
|
||||
let (num, den) = parse_plot_scale(&self.page_setup_scale);
|
||||
let (num, den) = parse_plot_scale(&self.plot_scale);
|
||||
if num > 0.0 && den > 0.0 {
|
||||
PlotScale::Ratio(num / den)
|
||||
} else {
|
||||
@@ -1126,70 +1134,442 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(super) fn on_print_to_printer(&mut self) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let scene = &self.tabs[i].scene;
|
||||
let wires = scene.entity_wires();
|
||||
let hatches: Vec<_> = scene.paper_canvas_hatches().as_ref().clone();
|
||||
let wipeouts: Vec<_> = scene.paper_canvas_wipeouts().as_ref().clone();
|
||||
use acadrust::objects::PlotType;
|
||||
let ps_snap = scene.effective_plot_settings();
|
||||
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)
|
||||
/// Build the render inputs and page geometry for a full-layout plot: wires
|
||||
/// plus hatch / wipeout fills, the effective (rotation-swapped) sheet size,
|
||||
/// the draw-origin offset, and the rotation. Shared by print + preview so
|
||||
/// the Plot dialog reuses the same tested derivation.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(super) fn layout_plot_params(
|
||||
&self,
|
||||
) -> (
|
||||
Vec<crate::scene::WireModel>,
|
||||
Vec<crate::scene::model::hatch_model::HatchModel>,
|
||||
Vec<crate::scene::model::hatch_model::HatchModel>,
|
||||
f64,
|
||||
f64,
|
||||
f32,
|
||||
f32,
|
||||
i32,
|
||||
) {
|
||||
let i = self.active_tab;
|
||||
let scene = &self.tabs[i].scene;
|
||||
let wires = scene.entity_wires();
|
||||
let hatches: Vec<_> = scene.paper_canvas_hatches().as_ref().clone();
|
||||
let wipeouts: Vec<_> = scene.paper_canvas_wipeouts().as_ref().clone();
|
||||
use acadrust::objects::PlotType;
|
||||
let ps_snap = scene.effective_plot_settings();
|
||||
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 {
|
||||
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),
|
||||
(-x0, -y0)
|
||||
}
|
||||
} else {
|
||||
(-x0, -y0)
|
||||
};
|
||||
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,
|
||||
hatches,
|
||||
wipeouts,
|
||||
eff_w,
|
||||
eff_h,
|
||||
draw_ox as f32,
|
||||
draw_oy as f32,
|
||||
rotation_deg,
|
||||
plot_style,
|
||||
)
|
||||
.await
|
||||
},
|
||||
Message::PrintResult,
|
||||
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),
|
||||
};
|
||||
(
|
||||
wires,
|
||||
hatches,
|
||||
wipeouts,
|
||||
eff_w,
|
||||
eff_h,
|
||||
draw_ox as f32,
|
||||
draw_oy as f32,
|
||||
rotation_deg,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn on_print_to_printer(&mut self) -> Task<Message> {
|
||||
let (wires, hatches, wipeouts, eff_w, eff_h, ox, oy, rotation_deg) =
|
||||
self.layout_plot_params();
|
||||
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, hatches, wipeouts, eff_w, eff_h, ox, oy, rotation_deg, plot_style,
|
||||
)
|
||||
.await
|
||||
},
|
||||
Message::PrintResult,
|
||||
)
|
||||
}
|
||||
|
||||
/// 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<Message> {
|
||||
use crate::io::paper_sizes::Orientation;
|
||||
// Keep the user's last print preferences (printer, copies, quality,
|
||||
// output options — persisted in `self.plot_dialog`); only refresh the
|
||||
// runtime printer list and reseed the drawing-specific fields from the
|
||||
// active layout.
|
||||
let d = &mut self.plot_dialog;
|
||||
d.printers = crate::io::print_to_printer::list_printers();
|
||||
d.paper = self.plot_format.label().to_string();
|
||||
d.orientation = match self.plot_orientation {
|
||||
Orientation::Portrait => "Portrait",
|
||||
Orientation::Landscape => "Landscape",
|
||||
}
|
||||
.to_string();
|
||||
d.style_name = self
|
||||
.active_plot_style
|
||||
.as_ref()
|
||||
.map(|t| t.name.clone())
|
||||
.unwrap_or_default();
|
||||
// `area` and `scale` are remembered user choices (default Window / Fit),
|
||||
// not reseeded from the layout. Offset / center / rotation ARE layout
|
||||
// properties, so reflect them.
|
||||
if let Some(ps) = self.tabs[self.active_tab].scene.effective_plot_settings() {
|
||||
let d = &mut self.plot_dialog;
|
||||
d.center = ps.flags.plot_centered;
|
||||
d.offset_x = format!("{:.2}", ps.origin_x);
|
||||
d.offset_y = format!("{:.2}", ps.origin_y);
|
||||
let deg = ps.rotation.to_degrees() as i32;
|
||||
d.rotation = format!("{deg}°");
|
||||
}
|
||||
self.active_modal = Some(crate::app::ModalKind::Plot);
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Handle one edit / action from the Plot dialog.
|
||||
pub(super) fn on_plot_dlg(
|
||||
&mut self,
|
||||
msg: crate::ui::window::plot::PlotDlgMsg,
|
||||
) -> Task<Message> {
|
||||
use crate::ui::window::plot::{PlotDlgMsg as M, PlotFlag, OUT_DEFAULT, OUT_PDF};
|
||||
match msg {
|
||||
M::Close => {
|
||||
self.close_active_modal();
|
||||
Task::none()
|
||||
}
|
||||
M::Printer(s) => {
|
||||
if s == OUT_PDF {
|
||||
self.plot_dialog.to_file = true;
|
||||
} else if s == OUT_DEFAULT {
|
||||
self.plot_dialog.to_file = false;
|
||||
self.plot_dialog.printer = None;
|
||||
} else {
|
||||
self.plot_dialog.to_file = false;
|
||||
self.plot_dialog.printer = Some(s);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
M::Paper(s) => {
|
||||
self.plot_dialog.paper = s;
|
||||
Task::none()
|
||||
}
|
||||
M::Orientation(s) => {
|
||||
self.plot_dialog.orientation = s;
|
||||
Task::none()
|
||||
}
|
||||
M::Rotation(s) => {
|
||||
self.plot_dialog.rotation = s;
|
||||
Task::none()
|
||||
}
|
||||
M::Area(s) => {
|
||||
self.plot_dialog.area = s;
|
||||
Task::none()
|
||||
}
|
||||
M::Scale(s) => {
|
||||
self.plot_dialog.scale = s;
|
||||
Task::none()
|
||||
}
|
||||
M::Quality(s) => {
|
||||
self.plot_dialog.quality = s;
|
||||
Task::none()
|
||||
}
|
||||
M::Shade(s) => {
|
||||
self.plot_dialog.shade = s;
|
||||
Task::none()
|
||||
}
|
||||
M::Copies(s) => {
|
||||
self.plot_dialog.copies = s;
|
||||
Task::none()
|
||||
}
|
||||
M::OffsetX(s) => {
|
||||
self.plot_dialog.offset_x = s;
|
||||
Task::none()
|
||||
}
|
||||
M::OffsetY(s) => {
|
||||
self.plot_dialog.offset_y = s;
|
||||
Task::none()
|
||||
}
|
||||
M::Dpi(s) => {
|
||||
self.plot_dialog.dpi = s;
|
||||
Task::none()
|
||||
}
|
||||
M::Flag(f) => {
|
||||
let d = &mut self.plot_dialog;
|
||||
match f {
|
||||
PlotFlag::Center => d.center = !d.center,
|
||||
PlotFlag::ScaleLw => d.scale_lw = !d.scale_lw,
|
||||
PlotFlag::Mono => d.mono = !d.mono,
|
||||
PlotFlag::Lineweights => d.lineweights = !d.lineweights,
|
||||
PlotFlag::WithStyles => d.with_styles = !d.with_styles,
|
||||
PlotFlag::Transparency => d.transparency = !d.transparency,
|
||||
PlotFlag::PaperspaceLast => d.paperspace_last = !d.paperspace_last,
|
||||
PlotFlag::HidePaperspace => d.hide_paperspace = !d.hide_paperspace,
|
||||
PlotFlag::Stamp => d.stamp = !d.stamp,
|
||||
PlotFlag::SaveLayout => d.save_layout = !d.save_layout,
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
M::LoadStyle => Task::done(Message::PlotStyleLoad),
|
||||
M::ClearStyle => {
|
||||
self.active_plot_style = None;
|
||||
self.plot_dialog.style_name.clear();
|
||||
Task::none()
|
||||
}
|
||||
M::PickWindow => {
|
||||
self.close_active_modal();
|
||||
Task::done(Message::Command("PLOTWINDOW".into()))
|
||||
}
|
||||
M::Preview => self.on_plot_dlg_commit(true),
|
||||
M::Commit => self.on_plot_dlg_commit(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the dialog's page settings to the layout, then either open a
|
||||
/// preview PDF, export a PDF, or send the job to the chosen printer.
|
||||
fn on_plot_dlg_commit(&mut self, preview: bool) -> Task<Message> {
|
||||
use crate::io::paper_sizes::{sheet_mm, Orientation, PaperSize};
|
||||
// Remember the user's print preferences across sessions.
|
||||
self.plot_dialog.save();
|
||||
let d = self.plot_dialog.clone();
|
||||
let paper = match d.paper.as_str() {
|
||||
"A3" => PaperSize::A3,
|
||||
"A2" => PaperSize::A2,
|
||||
"A1" => PaperSize::A1,
|
||||
"A0" => PaperSize::A0,
|
||||
_ => PaperSize::A4,
|
||||
};
|
||||
let orient = if d.orientation == "Portrait" {
|
||||
Orientation::Portrait
|
||||
} else {
|
||||
Orientation::Landscape
|
||||
};
|
||||
let (sheet_w, sheet_h) = sheet_mm(paper, orient);
|
||||
// Persist the dialog's page settings into the layout's PlotSettings /
|
||||
// paper_limits, then reuse the tested layout-plot derivation. `plot_scale`
|
||||
// feeds the model-space window plot path.
|
||||
self.plot_format = paper;
|
||||
self.plot_orientation = orient;
|
||||
self.plot_scale = d.scale.clone();
|
||||
let rotation: i16 = d.rotation.trim_end_matches('°').parse().unwrap_or(0);
|
||||
let off_x = d.offset_x.parse::<f64>().unwrap_or(0.0);
|
||||
let off_y = d.offset_y.parse::<f64>().unwrap_or(0.0);
|
||||
let extents = d.area != "Layout";
|
||||
self.apply_plot_page_settings(
|
||||
sheet_w, sheet_h, extents, d.center, off_x, off_y, rotation, &d.scale,
|
||||
);
|
||||
self.active_modal = None;
|
||||
|
||||
let plot_style = if d.with_styles {
|
||||
self.active_plot_style.clone()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// ── Window area: clipped model-space plot ────────────────────────────
|
||||
if d.area == "Window" {
|
||||
// Preview renders to a temp PDF and opens it — never a save dialog,
|
||||
// whatever the output target is.
|
||||
if preview {
|
||||
let Some((w_wires, w_hatches, w_wipeouts, sw, sh, wox, woy, wscale, wclip)) =
|
||||
self.window_plot_job()
|
||||
else {
|
||||
self.command_line
|
||||
.push_error("Pick a plot window first (model space).");
|
||||
self.active_modal = Some(crate::app::ModalKind::Plot);
|
||||
return Task::none();
|
||||
};
|
||||
let tmp = std::env::temp_dir().join("open_cad_studio_preview.pdf");
|
||||
let exp = crate::io::pdf_export::export_pdf(
|
||||
&w_wires, &w_hatches, &w_wipeouts, sw, sh, wox, woy, 0, wscale, wclip, &tmp,
|
||||
plot_style.as_ref(),
|
||||
);
|
||||
match exp.and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp)) {
|
||||
Ok(()) => self.command_line.push_info("Opened plot preview."),
|
||||
Err(e) => self.command_line.push_error(&format!("Preview failed: {e}")),
|
||||
}
|
||||
self.active_modal = Some(crate::app::ModalKind::Plot);
|
||||
return Task::none();
|
||||
}
|
||||
if d.to_file {
|
||||
// Tested clipped export (opens a save dialog).
|
||||
return Task::done(Message::PlotWindowExport);
|
||||
}
|
||||
let Some((w_wires, w_hatches, w_wipeouts, sw, sh, wox, woy, wscale, wclip)) =
|
||||
self.window_plot_job()
|
||||
else {
|
||||
self.command_line
|
||||
.push_error("Pick a plot window first (model space).");
|
||||
self.active_modal = Some(crate::app::ModalKind::Plot);
|
||||
return Task::none();
|
||||
};
|
||||
let tmp = std::env::temp_dir().join("open_cad_studio_print.pdf");
|
||||
let exp = crate::io::pdf_export::export_pdf(
|
||||
&w_wires, &w_hatches, &w_wipeouts, sw, sh, wox, woy, 0, wscale, wclip, &tmp,
|
||||
plot_style.as_ref(),
|
||||
);
|
||||
let opts = self.plot_print_options(&d);
|
||||
match exp.and_then(|_| crate::io::print_to_printer::print_existing_pdf(&tmp, &opts)) {
|
||||
Ok(printer) => self
|
||||
.command_line
|
||||
.push_info(&format!("Sent to printer: {printer}")),
|
||||
Err(e) => self.command_line.push_error(&format!("Print failed: {e}")),
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
let (wires, hatches, wipeouts, eff_w, eff_h, ox, oy, rot) = self.layout_plot_params();
|
||||
|
||||
if preview {
|
||||
let tmp = std::env::temp_dir().join("open_cad_studio_preview.pdf");
|
||||
let res = crate::io::pdf_export::export_pdf(
|
||||
&wires,
|
||||
&hatches,
|
||||
&wipeouts,
|
||||
eff_w,
|
||||
eff_h,
|
||||
ox,
|
||||
oy,
|
||||
rot,
|
||||
1.0,
|
||||
None,
|
||||
&tmp,
|
||||
plot_style.as_ref(),
|
||||
);
|
||||
match res.and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp)) {
|
||||
Ok(()) => self.command_line.push_info("Opened plot preview."),
|
||||
Err(e) => self.command_line.push_error(&format!("Preview failed: {e}")),
|
||||
}
|
||||
// Preview leaves the dialog open for further tweaks.
|
||||
self.active_modal = Some(crate::app::ModalKind::Plot);
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
if d.to_file {
|
||||
// Reuse the tested PDF export flow (it opens a save dialog).
|
||||
return Task::done(Message::PlotExport);
|
||||
}
|
||||
|
||||
let opts = self.plot_print_options(&d);
|
||||
self.command_line.push_info("Sending to system printer…");
|
||||
Task::perform(
|
||||
async move {
|
||||
crate::io::print_to_printer::print_wires_with(
|
||||
wires, hatches, wipeouts, eff_w, eff_h, ox, oy, rot, plot_style, opts,
|
||||
)
|
||||
.await
|
||||
},
|
||||
Message::PrintResult,
|
||||
)
|
||||
}
|
||||
|
||||
/// Build a [`PrintOptions`](crate::io::print_to_printer::PrintOptions) from
|
||||
/// the dialog state.
|
||||
fn plot_print_options(
|
||||
&self,
|
||||
d: &crate::ui::window::plot::PlotDialogState,
|
||||
) -> crate::io::print_to_printer::PrintOptions {
|
||||
crate::io::print_to_printer::PrintOptions {
|
||||
printer: d.printer.clone(),
|
||||
copies: d.copies.trim().parse::<u32>().unwrap_or(1).max(1),
|
||||
mono: d.mono,
|
||||
quality: Some(d.quality.clone()),
|
||||
dpi: d.dpi.trim().parse::<u32>().ok().filter(|v| *v > 0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Render inputs + page geometry for a clipped model-space window plot
|
||||
/// (the pending `plot_window`, fitted onto the dialog's sheet). Returns
|
||||
/// `None` when not in model space or no window has been picked. Mirrors
|
||||
/// [`Self::on_plot_window_export_path_some`] but hands back the params so
|
||||
/// the plot can go to a printer or preview, not only a save-dialog PDF.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn window_plot_job(
|
||||
&self,
|
||||
) -> Option<(
|
||||
Vec<crate::scene::WireModel>,
|
||||
Vec<crate::scene::model::hatch_model::HatchModel>,
|
||||
Vec<crate::scene::model::hatch_model::HatchModel>,
|
||||
f64,
|
||||
f64,
|
||||
f32,
|
||||
f32,
|
||||
f32,
|
||||
Option<(f32, f32, f32, f32)>,
|
||||
)> {
|
||||
use crate::io::paper_sizes::{sheet_mm, window_to_sheet, PlotScale};
|
||||
let i = self.active_tab;
|
||||
if self.tabs[i].scene.current_layout != "Model" {
|
||||
return None;
|
||||
}
|
||||
let (x0, y0, x1, y1) = self.plot_window?;
|
||||
if (x1 - x0) < 1e-6 || (y1 - y0) < 1e-6 {
|
||||
return 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.plot_scale.trim().eq_ignore_ascii_case("fit") {
|
||||
PlotScale::Fit
|
||||
} else {
|
||||
let (num, den) = parse_plot_scale(&self.plot_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), (sheet_w, sheet_h), scale_sel);
|
||||
let scene = &self.tabs[i].scene;
|
||||
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: Vec<_> = scene.paper_canvas_hatches().as_ref().clone();
|
||||
let wipeouts: Vec<_> = scene.paper_canvas_wipeouts().as_ref().clone();
|
||||
let offset_x = ((ox / scale) - x0) as f32;
|
||||
let offset_y = ((oy / scale) - y0) as f32;
|
||||
let clip = Some((
|
||||
(ox / scale) as f32,
|
||||
(oy / scale) as f32,
|
||||
win_w as f32,
|
||||
win_h as f32,
|
||||
));
|
||||
Some((
|
||||
wires, hatches, wipeouts, sheet_w, sheet_h, offset_x, offset_y, scale as f32, clip,
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) fn on_plot_style_panel_apply(&mut self) -> Task<Message> {
|
||||
|
||||
+7
-73
@@ -79,7 +79,11 @@ impl OpenCADStudio {
|
||||
}
|
||||
match self.active_modal {
|
||||
Some(Layers) => self.ribbon.deactivate_tool_if("LAYERS"),
|
||||
Some(PageSetup) => self.ribbon.deactivate_tool_if("PAGESETUP"),
|
||||
Some(Plot) => {
|
||||
self.ribbon.deactivate_tool_if("PLOT");
|
||||
self.ribbon.deactivate_tool_if("PRINT");
|
||||
self.ribbon.deactivate_tool_if("PAGESETUP");
|
||||
}
|
||||
Some(TextStyle) => {
|
||||
self.ribbon.deactivate_tool_if("STYLE");
|
||||
self.ribbon.deactivate_tool_if("TEXTSTYLE");
|
||||
@@ -3020,23 +3024,6 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
// ── Page Setup ────────────────────────────────────────────────
|
||||
Message::PageSetupOpen => {
|
||||
let i = self.active_tab;
|
||||
// Populate edit buffers from current paper limits.
|
||||
let (w, h) = if let Some(((x0, y0), (x1, y1))) = self.tabs[i].scene.paper_limits() {
|
||||
(x1 - x0, y1 - y0)
|
||||
} else {
|
||||
(297.0, 210.0) // A4 default
|
||||
};
|
||||
self.page_setup_w = format!("{w:.1}");
|
||||
self.page_setup_h = format!("{h:.1}");
|
||||
self.active_modal = Some(super::ModalKind::PageSetup);
|
||||
Task::none()
|
||||
}
|
||||
Message::PageSetupClose => {
|
||||
self.close_active_modal();
|
||||
Task::none()
|
||||
}
|
||||
Message::UpdateCheckResult(latest) => {
|
||||
let Some(info) = latest else {
|
||||
return Task::none();
|
||||
@@ -3081,61 +3068,8 @@ impl OpenCADStudio {
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::PageSetupWidthEdit(s) => {
|
||||
self.page_setup_w = s;
|
||||
Task::none()
|
||||
}
|
||||
Message::PageSetupHeightEdit(s) => {
|
||||
self.page_setup_h = s;
|
||||
Task::none()
|
||||
}
|
||||
Message::PageSetupPreset(name) => {
|
||||
// Paper size presets defined in view.rs — mirror them here.
|
||||
let sizes: &[(&str, f64, f64)] = &[
|
||||
("A4 Portrait", 210.0, 297.0),
|
||||
("A4 Landscape", 297.0, 210.0),
|
||||
("A3 Portrait", 297.0, 420.0),
|
||||
("A3 Landscape", 420.0, 297.0),
|
||||
("A2 Portrait", 420.0, 594.0),
|
||||
("A2 Landscape", 594.0, 420.0),
|
||||
("A1 Portrait", 594.0, 841.0),
|
||||
("A1 Landscape", 841.0, 594.0),
|
||||
("A0 Portrait", 841.0, 1189.0),
|
||||
("A0 Landscape", 1189.0, 841.0),
|
||||
("Letter Portrait", 215.9, 279.4),
|
||||
("Letter Landscape", 279.4, 215.9),
|
||||
];
|
||||
if let Some(&(_, w, h)) = sizes.iter().find(|(n, _, _)| *n == name) {
|
||||
self.page_setup_w = format!("{w:.1}");
|
||||
self.page_setup_h = format!("{h:.1}");
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::PageSetupPlotArea(s) => {
|
||||
self.page_setup_plot_area = s;
|
||||
Task::none()
|
||||
}
|
||||
Message::PageSetupCenterToggle => {
|
||||
self.page_setup_center = !self.page_setup_center;
|
||||
Task::none()
|
||||
}
|
||||
Message::PageSetupOffsetXEdit(s) => {
|
||||
self.page_setup_offset_x = s;
|
||||
Task::none()
|
||||
}
|
||||
Message::PageSetupOffsetYEdit(s) => {
|
||||
self.page_setup_offset_y = s;
|
||||
Task::none()
|
||||
}
|
||||
Message::PageSetupRotation(s) => {
|
||||
self.page_setup_rotation = s;
|
||||
Task::none()
|
||||
}
|
||||
Message::PageSetupScale(s) => {
|
||||
self.page_setup_scale = s;
|
||||
Task::none()
|
||||
}
|
||||
Message::PageSetupCommit => self.on_page_setup_commit(),
|
||||
Message::PlotDialogOpen => self.on_plot_dialog_open(),
|
||||
Message::PlotDlg(m) => self.on_plot_dlg(m),
|
||||
|
||||
// ── Plot / Export ─────────────────────────────────────────────
|
||||
Message::PlotExport => {
|
||||
|
||||
+1
-1
@@ -1466,7 +1466,7 @@ impl OpenCADStudio {
|
||||
PluginManager => (520, 460),
|
||||
UpdateNotice => (560, 460),
|
||||
Layers => (900, 360),
|
||||
PageSetup => (520, 460),
|
||||
Plot => (660, 540),
|
||||
LayoutManager => (640, 320),
|
||||
Plotstyle => (780, 540),
|
||||
TextStyle => (860, 480),
|
||||
|
||||
+3
-16
@@ -47,22 +47,9 @@ impl OpenCADStudio {
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
sized(tab.layers.view_window(), 900, 360)
|
||||
}
|
||||
super::super::ModalKind::PageSetup => sized(
|
||||
crate::ui::window::page_setup::view_window(
|
||||
&self.page_setup_w,
|
||||
&self.page_setup_h,
|
||||
&self.page_setup_plot_area,
|
||||
self.page_setup_center,
|
||||
&self.page_setup_offset_x,
|
||||
&self.page_setup_offset_y,
|
||||
&self.page_setup_rotation,
|
||||
&self.page_setup_scale,
|
||||
self.plot_format,
|
||||
self.plot_orientation,
|
||||
),
|
||||
520,
|
||||
460,
|
||||
),
|
||||
super::super::ModalKind::Plot => {
|
||||
sized(crate::ui::window::plot::view_window(&self.plot_dialog), 660, 540)
|
||||
}
|
||||
super::super::ModalKind::LayoutManager => {
|
||||
let i = self.active_tab;
|
||||
let layouts = self.tabs[i].scene.layout_names();
|
||||
|
||||
Reference in new issue
Block a user