forked from 1b-agent/opencadstudio-upstream
feat(plot): add Print All workflow
This commit is contained in:
1 parent
734afd5611
commit
f6b9d6514e
11 files changed
+655
-41
No files matched your search
@@ -448,6 +448,9 @@ impl OpenCADStudio {
|
||||
"PLOT" | "PRINT" => {
|
||||
return Some(Task::done(Message::PlotDialogOpen));
|
||||
}
|
||||
"PRINTALL" => {
|
||||
return Some(Task::done(Message::PrintAllOpen));
|
||||
}
|
||||
"EXPORT" | "EXPORTPDF" => {
|
||||
return Some(Task::done(Message::PlotExport));
|
||||
}
|
||||
|
||||
@@ -282,6 +282,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
// Standard aliases for existing commands.
|
||||
"BMAKE",
|
||||
"EXPORTPDF",
|
||||
"PRINTALL",
|
||||
"DDIM",
|
||||
// Inquiry: list the whole drawing database.
|
||||
"DBLIST",
|
||||
|
||||
@@ -669,6 +669,16 @@ pub(super) struct OpenCADStudio {
|
||||
/// Snapshot of the dialog's settings taken when it opened, restored by the
|
||||
/// `<previous>` list entry.
|
||||
plot_prev: Option<crate::ui::window::plot::PlotDialogState>,
|
||||
/// Paper layouts shown by Print All, in tab order with their selection.
|
||||
print_all_layouts: Vec<(String, bool)>,
|
||||
/// True while the Plot dialog is editing settings for Print All.
|
||||
print_all_options: bool,
|
||||
/// Settings restored when the Print All options dialog is cancelled.
|
||||
print_all_options_prev: Option<crate::ui::window::plot::PlotDialogState>,
|
||||
/// Plot style restored together with cancelled Print All options.
|
||||
print_all_plot_style_prev: Option<Option<crate::io::plot_style::PlotStyleTable>>,
|
||||
/// Plot window restored together with cancelled Print All options.
|
||||
print_all_plot_window_prev: Option<Option<(f64, f64, f64, f64)>>,
|
||||
|
||||
// ── Plot Style Table ──────────────────────────────────────────────────
|
||||
/// Currently loaded CTB/STB table (None = no override).
|
||||
@@ -1409,6 +1419,7 @@ pub enum ModalKind {
|
||||
LayerStateManager,
|
||||
LayerStateEditor,
|
||||
Plot,
|
||||
PrintAll,
|
||||
LayoutManager,
|
||||
Plotstyle,
|
||||
TextStyle,
|
||||
@@ -2455,6 +2466,23 @@ pub enum Message {
|
||||
PlotDialogOpen,
|
||||
/// An edit inside the Plot / Print dialog.
|
||||
PlotDlg(crate::ui::window::plot::PlotDlgMsg),
|
||||
/// Open the paper-layout batch output dialog.
|
||||
PrintAllOpen,
|
||||
/// Toggle one paper layout in the batch.
|
||||
PrintAllToggle(String),
|
||||
/// Select or clear every paper layout in the batch.
|
||||
PrintAllSelectAll,
|
||||
PrintAllSelectNone,
|
||||
/// Edit the shared batch output settings in the Plot dialog.
|
||||
PrintAllOptions,
|
||||
/// Save the selected layouts as one multi-page PDF.
|
||||
PrintAllPdf,
|
||||
/// Callback after the multi-page PDF path is picked or cancelled.
|
||||
PrintAllPdfPath(Option<std::path::PathBuf>),
|
||||
/// Send the selected layouts to the configured printer as one job.
|
||||
PrintAllPrint,
|
||||
/// Completion of a Print All PDF or printer job.
|
||||
PrintAllFinished(Result<String, String>),
|
||||
// ── Plot Style Table ─────────────────────────────────────────────────
|
||||
/// Open file dialog to load a CTB/STB plot style table.
|
||||
PlotStyleLoad,
|
||||
@@ -2896,6 +2924,11 @@ impl OpenCADStudio {
|
||||
plot_orientation: crate::io::paper_sizes::Orientation::Landscape,
|
||||
plot_dialog: crate::ui::window::plot::PlotDialogState::default(),
|
||||
plot_prev: None,
|
||||
print_all_layouts: Vec::new(),
|
||||
print_all_options: false,
|
||||
print_all_options_prev: None,
|
||||
print_all_plot_style_prev: None,
|
||||
print_all_plot_window_prev: None,
|
||||
opening: None,
|
||||
open_job_serial: 0,
|
||||
recovery_report: None,
|
||||
|
||||
@@ -2408,6 +2408,192 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn on_print_all_open(&mut self) -> Task<Message> {
|
||||
self.print_all_layouts = self.tabs[self.active_tab]
|
||||
.scene
|
||||
.layout_names()
|
||||
.into_iter()
|
||||
.filter(|name| name != "Model")
|
||||
.map(|name| (name, true))
|
||||
.collect();
|
||||
self.active_modal = Some(crate::app::ModalKind::PrintAll);
|
||||
self.reset_modal_geometry();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(super) fn on_print_all_options(&mut self) -> Task<Message> {
|
||||
let previous = self.plot_dialog.clone();
|
||||
let previous_style = self.active_plot_style.clone();
|
||||
let previous_window = self.plot_window;
|
||||
let task = self.on_plot_dialog_open();
|
||||
self.print_all_options_prev = Some(previous);
|
||||
self.print_all_plot_style_prev = Some(previous_style);
|
||||
self.print_all_plot_window_prev = Some(previous_window);
|
||||
self.print_all_options = true;
|
||||
self.plot_dialog.paper_space = true;
|
||||
self.plot_dialog.area = "Layout".into();
|
||||
task
|
||||
}
|
||||
|
||||
fn print_all_pages(&mut self) -> Result<Vec<crate::io::pdf_export::PdfPageInput>, String> {
|
||||
let available = self.tabs[self.active_tab].scene.layout_names();
|
||||
let selected: Vec<String> = self
|
||||
.print_all_layouts
|
||||
.iter()
|
||||
.filter(|(name, checked)| *checked && available.contains(name))
|
||||
.map(|(name, _)| name.clone())
|
||||
.collect();
|
||||
if selected.is_empty() {
|
||||
return Err(crate::t!("Select at least one layout.").into_owned());
|
||||
}
|
||||
|
||||
let i = self.active_tab;
|
||||
let original_layout = self.tabs[i].scene.current_layout.clone();
|
||||
let original_viewport = self.tabs[i].scene.active_viewport;
|
||||
let dialog = self.plot_dialog.clone();
|
||||
let mut pages = Vec::with_capacity(selected.len());
|
||||
for name in selected {
|
||||
// Plot helpers read the active layout. Swap only this transient
|
||||
// selector so the drawing's saved active-space metadata is not
|
||||
// touched while the owned page snapshot is collected.
|
||||
{
|
||||
let scene = &mut self.tabs[i].scene;
|
||||
scene.current_layout = name;
|
||||
scene.active_viewport = None;
|
||||
}
|
||||
let (
|
||||
wires,
|
||||
hatches,
|
||||
wipeouts,
|
||||
group_splits,
|
||||
paper_w,
|
||||
paper_h,
|
||||
offset_x,
|
||||
offset_y,
|
||||
rotation_deg,
|
||||
scale,
|
||||
clip,
|
||||
) = self.layout_plot_params_for("Layout");
|
||||
pages.push(crate::io::pdf_export::PdfPageInput {
|
||||
wires,
|
||||
hatches,
|
||||
wipeouts,
|
||||
paper_w,
|
||||
paper_h,
|
||||
offset_x,
|
||||
offset_y,
|
||||
rotation_deg,
|
||||
scale,
|
||||
clip,
|
||||
options: Self::pdf_plot_options(&dialog, group_splits),
|
||||
});
|
||||
}
|
||||
self.tabs[i].scene.current_layout = original_layout;
|
||||
self.tabs[i].scene.active_viewport = original_viewport;
|
||||
Ok(pages)
|
||||
}
|
||||
|
||||
pub(super) fn on_print_all_pdf_path_some(
|
||||
&mut self,
|
||||
path: std::path::PathBuf,
|
||||
) -> Task<Message> {
|
||||
let dialog = self.plot_dialog.clone();
|
||||
if dialog.style_missing {
|
||||
self.command_line.push_error(crate::tf!(
|
||||
"Plot style table '{}' is not loaded.",
|
||||
dialog.style_name
|
||||
).as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
let pages = match self.print_all_pages() {
|
||||
Ok(pages) => pages,
|
||||
Err(error) => {
|
||||
self.command_line.push_error(&error);
|
||||
return Task::none();
|
||||
}
|
||||
};
|
||||
let plot_style = self.dialog_plot_style(&dialog);
|
||||
let worker_path = path.clone();
|
||||
self.save_config();
|
||||
self.close_active_modal();
|
||||
let work = move || {
|
||||
crate::io::pdf_export::export_pdf_pages(
|
||||
&pages,
|
||||
&worker_path,
|
||||
plot_style.as_ref(),
|
||||
)
|
||||
.map(|_| format!("Exported {} layouts to {}", pages.len(), worker_path.display()))
|
||||
.map_err(|error| format!("Export failed: {error}"))
|
||||
};
|
||||
self.run_print_all_work(dialog.background, work)
|
||||
}
|
||||
|
||||
pub(super) fn on_print_all_print(&mut self) -> Task<Message> {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
self.command_line.push_error(
|
||||
crate::t!("Printing is not available in the web version.").as_ref(),
|
||||
);
|
||||
Task::none()
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let dialog = self.plot_dialog.clone();
|
||||
if dialog.style_missing {
|
||||
self.command_line.push_error(crate::tf!(
|
||||
"Plot style table '{}' is not loaded.",
|
||||
dialog.style_name
|
||||
).as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
let pages = match self.print_all_pages() {
|
||||
Ok(pages) => pages,
|
||||
Err(error) => {
|
||||
self.command_line.push_error(&error);
|
||||
return Task::none();
|
||||
}
|
||||
};
|
||||
let plot_style = self.dialog_plot_style(&dialog);
|
||||
let options = self.plot_print_options(&dialog, Default::default());
|
||||
let stamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|duration| duration.as_nanos())
|
||||
.unwrap_or(0);
|
||||
let temp_path = std::env::temp_dir().join(format!(
|
||||
"open_cad_studio_print_all_{stamp}.pdf"
|
||||
));
|
||||
self.save_config();
|
||||
self.close_active_modal();
|
||||
self.command_line.push_info(
|
||||
crate::t!("Sending selected layouts to the system printer…").as_ref(),
|
||||
);
|
||||
let work = move || {
|
||||
crate::io::pdf_export::export_pdf_pages(
|
||||
&pages,
|
||||
&temp_path,
|
||||
plot_style.as_ref(),
|
||||
)
|
||||
.and_then(|_| {
|
||||
crate::io::print_to_printer::print_existing_pdf(&temp_path, &options)
|
||||
})
|
||||
.map(|printer| format!("Sent {} layouts to printer: {printer}", pages.len()))
|
||||
.map_err(|error| format!("Print failed: {error}"))
|
||||
};
|
||||
self.run_print_all_work(dialog.background, work)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_print_all_work<F>(&mut self, background: bool, work: F) -> Task<Message>
|
||||
where
|
||||
F: FnOnce() -> Result<String, String> + Send + 'static,
|
||||
{
|
||||
if background {
|
||||
background_task(work, Message::PrintAllFinished)
|
||||
} else {
|
||||
Task::done(Message::PrintAllFinished(work()))
|
||||
}
|
||||
}
|
||||
|
||||
fn run_plot_work<F>(
|
||||
&mut self,
|
||||
background: bool,
|
||||
@@ -2940,6 +3126,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
Task::none()
|
||||
}
|
||||
M::Area(s) => {
|
||||
if self.print_all_options && s != "Layout" {
|
||||
return Task::none();
|
||||
}
|
||||
// A paper Layout page setup commonly carries physical-sheet
|
||||
// origin and 1:1 scale. Those values are correct only for
|
||||
// Layout; carrying them into Window/Extents/Display moves a
|
||||
@@ -3040,6 +3229,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
Task::none()
|
||||
}
|
||||
M::PickWindow => {
|
||||
if self.print_all_options {
|
||||
return Task::none();
|
||||
}
|
||||
let i = self.active_tab;
|
||||
if self.tabs[i].scene.current_layout != "Model"
|
||||
&& self.tabs[i].scene.active_viewport.is_some()
|
||||
@@ -3060,14 +3252,24 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
}
|
||||
M::SelectSetup(name) => {
|
||||
self.select_page_setup(&name);
|
||||
if self.print_all_options {
|
||||
self.plot_dialog.paper_space = true;
|
||||
self.plot_dialog.area = "Layout".into();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
M::SetCurrent => {
|
||||
if self.print_all_options {
|
||||
return Task::none();
|
||||
}
|
||||
self.apply_dialog_to_layout();
|
||||
self.command_line.push_info(crate::t!("Page setup applied to the layout.").as_ref());
|
||||
Task::none()
|
||||
}
|
||||
M::NewSetup => {
|
||||
if self.print_all_options {
|
||||
return Task::none();
|
||||
}
|
||||
// Create a setup from the current editor values, then start an
|
||||
// inline rename so the user can name it.
|
||||
let name = self.next_page_setup_name("Setup");
|
||||
@@ -3081,6 +3283,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
Task::none()
|
||||
}
|
||||
M::CopySetup => {
|
||||
if self.print_all_options {
|
||||
return Task::none();
|
||||
}
|
||||
// Duplicate the selected entry — a layout OR a named setup —
|
||||
// into a new standalone named page setup.
|
||||
let sel = self.plot_dialog.selected_setup.clone();
|
||||
@@ -3103,6 +3308,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
Task::none()
|
||||
}
|
||||
M::RenameStart(name) => {
|
||||
if self.print_all_options {
|
||||
return Task::none();
|
||||
}
|
||||
// Only standalone named setups can be renamed.
|
||||
if is_layout_entry(&name) || is_special_entry(&name) {
|
||||
return Task::none();
|
||||
@@ -3116,6 +3324,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
Task::none()
|
||||
}
|
||||
M::DeleteSetup => {
|
||||
if self.print_all_options {
|
||||
return Task::none();
|
||||
}
|
||||
let sel = self.plot_dialog.selected_setup.clone();
|
||||
if !sel.is_empty() && !is_layout_entry(&sel) {
|
||||
self.tabs[self.active_tab].scene.page_setup_delete(&sel);
|
||||
@@ -3153,7 +3364,30 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
self.plot_dialog.name_rename = false;
|
||||
Task::none()
|
||||
}
|
||||
M::Preview if self.print_all_options => Task::none(),
|
||||
M::Preview => self.on_plot_dlg_commit(true),
|
||||
M::Commit if self.print_all_options => {
|
||||
if self.plot_dialog.style_missing {
|
||||
self.command_line.push_error(crate::tf!(
|
||||
"Plot style table '{}' is not loaded.",
|
||||
self.plot_dialog.style_name
|
||||
).as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
self.plot_dialog.paper_space = true;
|
||||
self.plot_dialog.area = "Layout".into();
|
||||
self.sync_dialog_plot_runtime();
|
||||
self.save_config();
|
||||
self.print_all_options = false;
|
||||
self.print_all_options_prev = None;
|
||||
self.print_all_plot_style_prev = None;
|
||||
if let Some(previous) = self.print_all_plot_window_prev.take() {
|
||||
self.plot_window = previous;
|
||||
}
|
||||
self.active_modal = Some(crate::app::ModalKind::PrintAll);
|
||||
self.reset_modal_geometry();
|
||||
Task::none()
|
||||
}
|
||||
M::Commit => self.on_plot_dlg_commit(false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,21 @@ impl OpenCADStudio {
|
||||
/// changes, and the ribbon tool that launched the dialog is de-highlighted.
|
||||
fn close_active_modal(&mut self) {
|
||||
use super::ModalKind::*;
|
||||
if self.active_modal == Some(Plot) && self.print_all_options {
|
||||
if let Some(previous) = self.print_all_options_prev.take() {
|
||||
self.plot_dialog = previous;
|
||||
}
|
||||
if let Some(previous) = self.print_all_plot_style_prev.take() {
|
||||
self.active_plot_style = previous;
|
||||
}
|
||||
if let Some(previous) = self.print_all_plot_window_prev.take() {
|
||||
self.plot_window = previous;
|
||||
}
|
||||
self.print_all_options = false;
|
||||
self.active_modal = Some(PrintAll);
|
||||
self.reset_modal_geometry();
|
||||
return;
|
||||
}
|
||||
if matches!(
|
||||
self.active_modal,
|
||||
Some(TextStyle | DimStyle | TableStyle | MLeaderStyle | MlStyle)
|
||||
@@ -5461,6 +5476,73 @@ impl OpenCADStudio {
|
||||
}
|
||||
Message::PlotDialogOpen => self.on_plot_dialog_open(),
|
||||
Message::PlotDlg(m) => self.on_plot_dlg(m),
|
||||
Message::PrintAllOpen => self.on_print_all_open(),
|
||||
Message::PrintAllToggle(name) => {
|
||||
if let Some((_, selected)) = self
|
||||
.print_all_layouts
|
||||
.iter_mut()
|
||||
.find(|(layout, _)| layout == &name)
|
||||
{
|
||||
*selected = !*selected;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::PrintAllSelectAll => {
|
||||
for (_, selected) in &mut self.print_all_layouts {
|
||||
*selected = true;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::PrintAllSelectNone => {
|
||||
for (_, selected) in &mut self.print_all_layouts {
|
||||
*selected = false;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::PrintAllOptions => self.on_print_all_options(),
|
||||
Message::PrintAllPdf => {
|
||||
let i = self.active_tab;
|
||||
let stem = self.tabs[i]
|
||||
.current_path
|
||||
.as_deref()
|
||||
.and_then(|path| path.file_stem())
|
||||
.map(|name| format!("{}_layouts", name.to_string_lossy()))
|
||||
.unwrap_or_else(|| "drawing_layouts".into());
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let Some(window_id) = self.main_window else {
|
||||
return Task::done(Message::PrintAllPdfPath(None));
|
||||
};
|
||||
iced::window::run(window_id, move |parent| {
|
||||
crate::io::pdf_export::pick_pdf_path_owned(stem, parent)
|
||||
})
|
||||
.map(Message::PrintAllPdfPath)
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = stem;
|
||||
self.command_line.push_error(
|
||||
crate::t!("PDF export is not available in the web version.").as_ref(),
|
||||
);
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
Message::PrintAllPdfPath(None) => Task::none(),
|
||||
Message::PrintAllPdfPath(Some(path)) => self.on_print_all_pdf_path_some(path),
|
||||
Message::PrintAllPrint => self.on_print_all_print(),
|
||||
Message::PrintAllFinished(result) => {
|
||||
match result {
|
||||
Ok(message) => self.command_line.push_info(&message),
|
||||
Err(error) => {
|
||||
self.command_line.push_error(&error);
|
||||
if self.active_modal.is_none() {
|
||||
self.active_modal = Some(super::ModalKind::PrintAll);
|
||||
self.reset_modal_geometry();
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
// ── Plot / Export ─────────────────────────────────────────────
|
||||
Message::PlotExport => {
|
||||
|
||||
+20
-1
@@ -21,6 +21,7 @@ impl OpenCADStudio {
|
||||
Some(K::LayerStateManager) => crate::tr!("modal-layer-state-manager"),
|
||||
Some(K::LayerStateEditor) => crate::tr!("modal-edit-layer-state"),
|
||||
Some(K::Plot) => crate::tr!("modal-plot"),
|
||||
Some(K::PrintAll) => t!("Print All").into_owned(),
|
||||
Some(K::LayoutManager) => crate::tr!("modal-layout-manager"),
|
||||
Some(K::ScaleManager) => crate::tr!("modal-scale-manager"),
|
||||
Some(K::AnnoObjectScale) => crate::tr!("modal-annotation-object-scale"),
|
||||
@@ -238,9 +239,27 @@ impl OpenCADStudio {
|
||||
ex,
|
||||
760,
|
||||
540,
|
||||
|flow| crate::ui::window::plot::view_window(&self.plot_dialog, flow),
|
||||
|flow| {
|
||||
crate::ui::window::plot::view_window(
|
||||
&self.plot_dialog,
|
||||
self.print_all_options,
|
||||
flow,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
super::super::ModalKind::PrintAll => sized_flow(
|
||||
ex,
|
||||
520,
|
||||
420,
|
||||
|flow| {
|
||||
crate::ui::window::print_all::view_window(
|
||||
&self.print_all_layouts,
|
||||
self.plot_dialog.printer.as_deref(),
|
||||
flow,
|
||||
)
|
||||
},
|
||||
),
|
||||
super::super::ModalKind::LayoutManager => {
|
||||
let i = self.active_tab;
|
||||
let layouts = self.tabs[i].scene.layout_names();
|
||||
|
||||
Reference in new issue
Block a user