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();
|
||||
|
||||
+99
-3
@@ -46,6 +46,15 @@ pub fn export_pdf(
|
||||
Err("PDF export is not available in the web version.".into())
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn export_pdf_pages(
|
||||
_pages: &[PdfPageInput],
|
||||
_path: &Path,
|
||||
_plot_style: Option<&PlotStyleTable>,
|
||||
) -> Result<(), String> {
|
||||
Err("PDF export is not available in the web version.".into())
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn pick_pdf_path_owned(_stem: String) -> Option<std::path::PathBuf> {
|
||||
None
|
||||
@@ -83,6 +92,22 @@ pub struct PlotGroupSplits {
|
||||
pub wipeouts: usize,
|
||||
}
|
||||
|
||||
/// Owned render data for one page in a multi-page PDF.
|
||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||
pub struct PdfPageInput {
|
||||
pub wires: std::sync::Arc<Vec<WireModel>>,
|
||||
pub hatches: Vec<HatchModel>,
|
||||
pub wipeouts: Vec<HatchModel>,
|
||||
pub paper_w: f64,
|
||||
pub paper_h: f64,
|
||||
pub offset_x: f64,
|
||||
pub offset_y: f64,
|
||||
pub rotation_deg: i32,
|
||||
pub scale: f32,
|
||||
pub clip: Option<(f32, f32, f32, f32)>,
|
||||
pub options: PdfPlotOptions,
|
||||
}
|
||||
|
||||
impl Default for PdfPlotOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -138,6 +163,21 @@ pub fn export_pdf(
|
||||
file.write_all(&bytes).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Export several independently sized pages into one PDF file.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn export_pdf_pages(
|
||||
pages: &[PdfPageInput],
|
||||
path: &Path,
|
||||
plot_style: Option<&PlotStyleTable>,
|
||||
) -> Result<(), String> {
|
||||
if pages.is_empty() {
|
||||
return Err("No pages were selected.".into());
|
||||
}
|
||||
let bytes = build_pdf_pages(pages, plot_style);
|
||||
let mut file = std::fs::File::create(path).map_err(|e| e.to_string())?;
|
||||
file.write_all(&bytes).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Show a parented PDF save-file dialog and return the chosen path.
|
||||
///
|
||||
/// The parent comes from `iced::window::run`, keeping the portal request tied
|
||||
@@ -181,6 +221,65 @@ fn build_pdf(
|
||||
options: PdfPlotOptions,
|
||||
) -> Vec<u8> {
|
||||
let mut doc = PdfDocument::new("Open CAD Studio Export");
|
||||
append_pdf_page(
|
||||
&mut doc,
|
||||
wires,
|
||||
hatches,
|
||||
wipeouts,
|
||||
paper_w,
|
||||
paper_h,
|
||||
ox,
|
||||
oy,
|
||||
rotation_deg,
|
||||
scale,
|
||||
clip,
|
||||
plot_style,
|
||||
options,
|
||||
);
|
||||
let mut warnings = Vec::new();
|
||||
doc.save(&PdfSaveOptions::default(), &mut warnings)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn build_pdf_pages(pages: &[PdfPageInput], plot_style: Option<&PlotStyleTable>) -> Vec<u8> {
|
||||
let mut doc = PdfDocument::new("Open CAD Studio Export");
|
||||
for page in pages {
|
||||
append_pdf_page(
|
||||
&mut doc,
|
||||
&page.wires,
|
||||
&page.hatches,
|
||||
&page.wipeouts,
|
||||
page.paper_w as f32,
|
||||
page.paper_h as f32,
|
||||
page.offset_x,
|
||||
page.offset_y,
|
||||
page.rotation_deg,
|
||||
page.scale,
|
||||
page.clip,
|
||||
plot_style,
|
||||
page.options,
|
||||
);
|
||||
}
|
||||
let mut warnings = Vec::new();
|
||||
doc.save(&PdfSaveOptions::default(), &mut warnings)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn append_pdf_page(
|
||||
doc: &mut PdfDocument,
|
||||
wires: &[WireModel],
|
||||
hatches: &[HatchModel],
|
||||
wipeouts: &[HatchModel],
|
||||
paper_w: f32,
|
||||
paper_h: f32,
|
||||
ox: f64,
|
||||
oy: f64,
|
||||
rotation_deg: i32,
|
||||
scale: f32,
|
||||
clip: Option<(f32, f32, f32, f32)>,
|
||||
plot_style: Option<&PlotStyleTable>,
|
||||
options: PdfPlotOptions,
|
||||
) {
|
||||
let mut ops: Vec<Op> = Vec::new();
|
||||
|
||||
// White page background.
|
||||
@@ -474,9 +573,6 @@ fn build_pdf(
|
||||
|
||||
let page = PdfPage::new(Mm(paper_w), Mm(paper_h), ops);
|
||||
doc.pages.push(page);
|
||||
|
||||
let mut warnings = Vec::new();
|
||||
doc.save(&PdfSaveOptions::default(), &mut warnings)
|
||||
}
|
||||
|
||||
/// Build a PDF dash array (in points) from a WireModel linetype pattern.
|
||||
|
||||
@@ -18,10 +18,10 @@ pub fn paper_space_tools() -> Vec<ToolDef> {
|
||||
event: ModuleEvent::Command("PAGESETUP".to_string()),
|
||||
},
|
||||
ToolDef {
|
||||
id: "PLOT",
|
||||
label: "Export PDF",
|
||||
id: "PRINTALL",
|
||||
label: "Print All",
|
||||
icon: IconKind::Svg(include_bytes!("../../../assets/icons/plot.svg")),
|
||||
event: ModuleEvent::Command("PLOT".to_string()),
|
||||
event: ModuleEvent::Command("PRINTALL".to_string()),
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -57,10 +57,10 @@ impl CadModule for LayoutModule {
|
||||
}
|
||||
.into(),
|
||||
ToolDef {
|
||||
id: "PLOT",
|
||||
label: "Export PDF",
|
||||
id: "PRINTALL",
|
||||
label: "Print All",
|
||||
icon: IconKind::Svg(include_bytes!("../../../assets/icons/plot.svg")),
|
||||
event: ModuleEvent::Command("PLOT".to_string()),
|
||||
event: ModuleEvent::Command("PRINTALL".to_string()),
|
||||
}
|
||||
.into(),
|
||||
],
|
||||
|
||||
@@ -2,6 +2,7 @@ pub mod about;
|
||||
pub mod layout_manager;
|
||||
pub mod layer_state_manager;
|
||||
pub mod plot;
|
||||
pub mod print_all;
|
||||
pub mod plugin_manager;
|
||||
pub mod shortcuts;
|
||||
pub mod layers;
|
||||
|
||||
+49
-31
@@ -490,11 +490,18 @@ fn choices(items: &[&str]) -> Vec<PlotChoice> {
|
||||
|
||||
pub fn view_window(
|
||||
s: &PlotDialogState,
|
||||
print_all_options: bool,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'_, Message> {
|
||||
let width = sizing.width;
|
||||
let height = sizing.height;
|
||||
let action = if s.to_file { t!("Export PDF") } else { t!("Print") };
|
||||
let action = if print_all_options {
|
||||
t!("Apply")
|
||||
} else if s.to_file {
|
||||
t!("Export PDF")
|
||||
} else {
|
||||
t!("Print")
|
||||
};
|
||||
let is_special = s.selected_setup == SETUP_NONE || s.selected_setup == SETUP_PREV;
|
||||
let sel_is_layout = s.selected_setup.len() >= 2
|
||||
&& s.selected_setup.starts_with('*')
|
||||
@@ -578,23 +585,26 @@ pub fn view_window(
|
||||
left: 12.0,
|
||||
});
|
||||
|
||||
let mut new_button = button(text(t!("New")).size(11))
|
||||
.style(btn(false))
|
||||
.padding([4, 12]);
|
||||
if !print_all_options {
|
||||
new_button = new_button.on_press(Message::PlotDlg(PlotDlgMsg::NewSetup));
|
||||
}
|
||||
let mut copy_button = button(text(t!("Copy")).size(11))
|
||||
.style(btn(false))
|
||||
.padding([4, 12]);
|
||||
if can_copy {
|
||||
if can_copy && !print_all_options {
|
||||
copy_button = copy_button.on_press(Message::PlotDlg(PlotDlgMsg::CopySetup));
|
||||
}
|
||||
let mut delete_button = button(text(t!("Delete")).size(11))
|
||||
.style(btn(false))
|
||||
.padding([4, 12]);
|
||||
if is_named {
|
||||
if is_named && !print_all_options {
|
||||
delete_button = delete_button.on_press(Message::PlotDlg(PlotDlgMsg::DeleteSetup));
|
||||
}
|
||||
let left_bar = row![
|
||||
button(text(t!("New")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::NewSetup))
|
||||
.style(btn(false))
|
||||
.padding([4, 12]),
|
||||
new_button,
|
||||
copy_button,
|
||||
delete_button,
|
||||
]
|
||||
@@ -672,8 +682,12 @@ pub fn view_window(
|
||||
paper_note,
|
||||
].spacing(7));
|
||||
|
||||
let mut area_options = choices(&["Extents", "Display", "Window"]);
|
||||
if s.paper_space {
|
||||
let mut area_options = if print_all_options {
|
||||
choices(&["Layout"])
|
||||
} else {
|
||||
choices(&["Extents", "Display", "Window"])
|
||||
};
|
||||
if s.paper_space && !print_all_options {
|
||||
area_options.insert(0, PlotChoice::localized("Layout"));
|
||||
}
|
||||
let mut area_row = row![
|
||||
@@ -690,7 +704,7 @@ pub fn view_window(
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center);
|
||||
if s.area == "Window" {
|
||||
if s.area == "Window" && !print_all_options {
|
||||
area_row = area_row.push(
|
||||
button(text(t!("Pick…")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::PickWindow))
|
||||
@@ -877,27 +891,31 @@ pub fn view_window(
|
||||
let body = row![list_panel, vsep(height), detail]
|
||||
.width(width)
|
||||
.height(height);
|
||||
let toolbar = container(
|
||||
row![
|
||||
left_bar,
|
||||
Space::new().width(width),
|
||||
button(text(t!("Set current")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::SetCurrent))
|
||||
.style(btn(false))
|
||||
.padding([4, 12]),
|
||||
Space::new().width(6),
|
||||
button(text(t!("Preview")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::Preview))
|
||||
.style(btn(false))
|
||||
.padding([4, 12]),
|
||||
Space::new().width(6),
|
||||
button(text(action).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::Commit))
|
||||
.style(btn(true))
|
||||
.padding([4, 18]),
|
||||
]
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
let mut toolbar_row = row![left_bar, Space::new().width(width)].align_y(iced::Center);
|
||||
if !print_all_options {
|
||||
toolbar_row = toolbar_row
|
||||
.push(
|
||||
button(text(t!("Set current")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::SetCurrent))
|
||||
.style(btn(false))
|
||||
.padding([4, 12]),
|
||||
)
|
||||
.push(Space::new().width(6))
|
||||
.push(
|
||||
button(text(t!("Preview")).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::Preview))
|
||||
.style(btn(false))
|
||||
.padding([4, 12]),
|
||||
)
|
||||
.push(Space::new().width(6));
|
||||
}
|
||||
toolbar_row = toolbar_row.push(
|
||||
button(text(action).size(11))
|
||||
.on_press(Message::PlotDlg(PlotDlgMsg::Commit))
|
||||
.style(btn(true))
|
||||
.padding([4, 18]),
|
||||
);
|
||||
let toolbar = container(toolbar_row)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(theme.palette().background.weak.color)),
|
||||
..Default::default()
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
use crate::app::Message;
|
||||
use crate::t;
|
||||
use iced::widget::{button, checkbox, column, container, row, scrollable, text, Space};
|
||||
use iced::{Background, Border, Element, Length, Theme};
|
||||
|
||||
fn button_style(primary: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
|
||||
move |theme, status| {
|
||||
if primary {
|
||||
button::primary(theme, status)
|
||||
} else {
|
||||
button::secondary(theme, status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn muted(theme: &Theme) -> iced::widget::text::Style {
|
||||
iced::widget::text::Style {
|
||||
color: Some(theme.palette().background.base.text.scale_alpha(0.68)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_window<'a>(
|
||||
layouts: &'a [(String, bool)],
|
||||
printer: Option<&'a str>,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'a, Message> {
|
||||
let selected = layouts.iter().filter(|(_, checked)| *checked).count();
|
||||
let rows: Vec<Element<'a, Message>> = layouts
|
||||
.iter()
|
||||
.map(|(name, checked)| {
|
||||
let layout = name.clone();
|
||||
container(
|
||||
checkbox(*checked)
|
||||
.label(name.clone())
|
||||
.on_toggle(move |_| Message::PrintAllToggle(layout.clone()))
|
||||
.size(16)
|
||||
.text_size(12),
|
||||
)
|
||||
.padding([6, 8])
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
let list: Element<'a, Message> = if rows.is_empty() {
|
||||
container(text(t!("No paper layouts are available.")).size(12).style(muted))
|
||||
.center_x(Length::Fill)
|
||||
.center_y(Length::Fill)
|
||||
.into()
|
||||
} else {
|
||||
scrollable(column(rows).spacing(2)).height(sizing.height).into()
|
||||
};
|
||||
let printer = printer
|
||||
.filter(|name| !name.is_empty())
|
||||
.map(str::to_owned)
|
||||
.unwrap_or_else(|| t!("Default printer").into_owned());
|
||||
let summary = if selected == 1 {
|
||||
t!("1 layout selected").into_owned()
|
||||
} else {
|
||||
format!("{selected} {}", t!("layouts selected"))
|
||||
};
|
||||
|
||||
let mut pdf = button(text(t!("PDF")).size(11))
|
||||
.style(button_style(false))
|
||||
.padding([5, 16]);
|
||||
let mut print = button(text(t!("Print")).size(11))
|
||||
.style(button_style(true))
|
||||
.padding([5, 18]);
|
||||
if selected > 0 {
|
||||
pdf = pdf.on_press(Message::PrintAllPdf);
|
||||
print = print.on_press(Message::PrintAllPrint);
|
||||
}
|
||||
|
||||
let list_panel = container(list)
|
||||
.width(sizing.width)
|
||||
.height(sizing.height)
|
||||
.padding(4)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(theme.palette().background.weak.color)),
|
||||
border: Border {
|
||||
color: theme.palette().background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 4.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
container(
|
||||
column![
|
||||
text(t!("Select the layouts to output. Each page is printed in Layout mode."))
|
||||
.size(12),
|
||||
row![
|
||||
button(text(t!("Select all")).size(11))
|
||||
.on_press(Message::PrintAllSelectAll)
|
||||
.style(button_style(false))
|
||||
.padding([4, 10]),
|
||||
button(text(t!("Select none")).size(11))
|
||||
.on_press(Message::PrintAllSelectNone)
|
||||
.style(button_style(false))
|
||||
.padding([4, 10]),
|
||||
Space::new().width(Length::Fill),
|
||||
text(summary).size(11).style(muted),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center),
|
||||
list_panel,
|
||||
row![
|
||||
text(format!("{}: {printer}", t!("Printer")))
|
||||
.size(11)
|
||||
.style(muted),
|
||||
Space::new().width(Length::Fill),
|
||||
button(text(t!("Options…")).size(11))
|
||||
.on_press(Message::PrintAllOptions)
|
||||
.style(button_style(false))
|
||||
.padding([5, 12]),
|
||||
pdf,
|
||||
print,
|
||||
]
|
||||
.spacing(7)
|
||||
.align_y(iced::Center),
|
||||
]
|
||||
.spacing(10),
|
||||
)
|
||||
.padding(14)
|
||||
.width(sizing.width)
|
||||
.height(sizing.height)
|
||||
.into()
|
||||
}
|
||||
Reference in new issue
Block a user