forked from 1b-agent/opencadstudio-upstream
Merge pull request #1701 from hvfrancesco/main
added SVG export feature # Conflicts: # src/io/mod.rs # src/io/pdf_export.rs
This commit is contained in:
commit
fb474eb77c
10 files changed
+1274
-124
No files matched your search
@@ -970,6 +970,13 @@ impl OpenCADStudio {
|
||||
"EXPORT" | "EXPORTPDF" => {
|
||||
return Some(Task::done(Message::PlotExport));
|
||||
}
|
||||
// EXPORTSVG — the same direct export, but the save dialog
|
||||
// proposes the SVG device's defaults (`stem.svg`).
|
||||
"EXPORTSVG" => {
|
||||
self.plot_dialog.to_file = true;
|
||||
self.plot_dialog.file_svg = true;
|
||||
return Some(Task::done(Message::PlotExport));
|
||||
}
|
||||
// PLOTSTYLE — load or clear CTB/STB plot style table
|
||||
cmd if cmd == "PLOTSTYLE" || cmd.starts_with("PLOTSTYLE ") => {
|
||||
let sub = cmd
|
||||
|
||||
@@ -486,6 +486,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
// Standard aliases for existing commands.
|
||||
"BMAKE",
|
||||
"EXPORTPDF",
|
||||
"EXPORTSVG",
|
||||
"PRINTALL",
|
||||
"DDIM",
|
||||
// Inquiry: list the whole drawing database.
|
||||
|
||||
+43
-17
@@ -510,8 +510,11 @@ impl OpenCADStudio {
|
||||
use crate::io::pdf_export::PdfPageInput;
|
||||
|
||||
let path = string(req, "path")?;
|
||||
if !path.to_ascii_lowercase().ends_with(".pdf") {
|
||||
return Err(failure("invalid_path", "plot writes .pdf files"));
|
||||
let lower = path.to_ascii_lowercase();
|
||||
// SVG shares the plot page with PDF: the writer follows the extension.
|
||||
let is_svg = lower.ends_with(".svg");
|
||||
if !is_svg && !lower.ends_with(".pdf") {
|
||||
return Err(failure("invalid_path", "plot writes .pdf or .svg files"));
|
||||
}
|
||||
self.stamp_plot_fields();
|
||||
let i = self.active_tab;
|
||||
@@ -740,9 +743,16 @@ impl OpenCADStudio {
|
||||
self.tabs[i].scene.camera_generation = original_camera_generation;
|
||||
let pages = built.map_err(|e| failure("plot_failed", e))?;
|
||||
if req["per_page"].as_bool().unwrap_or(false) {
|
||||
// One PDF per layout: <stem>-<Layout>.pdf beside the requested
|
||||
// One file per layout: <stem>-<Layout>.pdf/.svg beside the requested
|
||||
// path (sheet-by-sheet delivery).
|
||||
let stem = path.strip_suffix(".pdf").unwrap_or(path).to_owned();
|
||||
let stem = path
|
||||
.strip_suffix(".pdf")
|
||||
.or_else(|| path.strip_suffix(".PDF"))
|
||||
.or_else(|| path.strip_suffix(".svg"))
|
||||
.or_else(|| path.strip_suffix(".SVG"))
|
||||
.unwrap_or(path)
|
||||
.to_owned();
|
||||
let ext = if is_svg { "svg" } else { "pdf" };
|
||||
let mut files = Vec::with_capacity(pages.len());
|
||||
for (name, page) in targets.iter().zip(&pages) {
|
||||
let safe: String = name
|
||||
@@ -755,13 +765,21 @@ impl OpenCADStudio {
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let file = format!("{stem}-{safe}.pdf");
|
||||
crate::io::pdf_export::export_pdf_pages(
|
||||
std::slice::from_ref(page),
|
||||
std::path::Path::new(&file),
|
||||
loaded_style.as_ref(),
|
||||
)
|
||||
.map_err(|e| failure("plot_failed", e))?;
|
||||
let file = format!("{stem}-{safe}.{ext}");
|
||||
let result = if is_svg {
|
||||
crate::io::svg_export::export_svg_pages(
|
||||
std::slice::from_ref(page),
|
||||
std::path::Path::new(&file),
|
||||
loaded_style.as_ref(),
|
||||
)
|
||||
} else {
|
||||
crate::io::pdf_export::export_pdf_pages(
|
||||
std::slice::from_ref(page),
|
||||
std::path::Path::new(&file),
|
||||
loaded_style.as_ref(),
|
||||
)
|
||||
};
|
||||
result.map_err(|e| failure("plot_failed", e))?;
|
||||
files.push(json!({"layout": name, "path": file}));
|
||||
}
|
||||
self.set_control_result(json!({ "files": files, "pages": pages.len() }));
|
||||
@@ -770,12 +788,20 @@ impl OpenCADStudio {
|
||||
// Pages may drop the style when a stored page setup overwrites the
|
||||
// dialog's style fields, so the explicit request style rides along as
|
||||
// the export-level fallback.
|
||||
crate::io::pdf_export::export_pdf_pages(
|
||||
&pages,
|
||||
std::path::Path::new(path),
|
||||
loaded_style.as_ref(),
|
||||
)
|
||||
.map_err(|e| failure("plot_failed", e))?;
|
||||
let result = if is_svg {
|
||||
crate::io::svg_export::export_svg_pages(
|
||||
&pages,
|
||||
std::path::Path::new(path),
|
||||
loaded_style.as_ref(),
|
||||
)
|
||||
} else {
|
||||
crate::io::pdf_export::export_pdf_pages(
|
||||
&pages,
|
||||
std::path::Path::new(path),
|
||||
loaded_style.as_ref(),
|
||||
)
|
||||
};
|
||||
result.map_err(|e| failure("plot_failed", e))?;
|
||||
self.set_control_result(json!({
|
||||
"path": path,
|
||||
"pages": pages.len(),
|
||||
|
||||
+51
-3
@@ -3877,7 +3877,17 @@ impl OpenCADStudio {
|
||||
};
|
||||
let worker_path = path.clone();
|
||||
let work = move || {
|
||||
crate::io::pdf_export::export_pdf(&page, &worker_path)
|
||||
// SVG shares the plot page with PDF: pick the writer from the
|
||||
// chosen extension.
|
||||
let result = if worker_path
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("svg"))
|
||||
{
|
||||
crate::io::svg_export::export_svg(&page, &worker_path)
|
||||
} else {
|
||||
crate::io::pdf_export::export_pdf(&page, &worker_path)
|
||||
};
|
||||
result
|
||||
.map(|_| crate::tf!("Exported: {}", worker_path.display()).into_owned())
|
||||
.map_err(|e| crate::tf!("Export failed: {e}").into_owned())
|
||||
};
|
||||
@@ -3903,7 +3913,15 @@ impl OpenCADStudio {
|
||||
let worker_path = path.clone();
|
||||
self.close_active_modal();
|
||||
let work = move || {
|
||||
crate::io::pdf_export::export_pdf(&page, &worker_path)
|
||||
let result = if worker_path
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("svg"))
|
||||
{
|
||||
crate::io::svg_export::export_svg(&page, &worker_path)
|
||||
} else {
|
||||
crate::io::pdf_export::export_pdf(&page, &worker_path)
|
||||
};
|
||||
result
|
||||
.map(|_| {
|
||||
crate::tf!(
|
||||
"Plotted window to {}",
|
||||
@@ -4894,7 +4912,7 @@ impl OpenCADStudio {
|
||||
msg: crate::ui::window::plot::PlotDlgMsg,
|
||||
) -> Task<Message> {
|
||||
use crate::ui::window::plot::{
|
||||
PlotDlgMsg as M, PlotFlag, OUT_DEFAULT, OUT_PDF, STYLE_NONE,
|
||||
PlotDlgMsg as M, PlotFlag, OUT_DEFAULT, OUT_PDF, OUT_SVG, STYLE_NONE,
|
||||
};
|
||||
match msg {
|
||||
M::Close => {
|
||||
@@ -4910,11 +4928,17 @@ impl OpenCADStudio {
|
||||
self.plot_dialog.printer_editor = None;
|
||||
if s == OUT_PDF {
|
||||
self.plot_dialog.to_file = true;
|
||||
self.plot_dialog.file_svg = false;
|
||||
} else if s == OUT_SVG {
|
||||
self.plot_dialog.to_file = true;
|
||||
self.plot_dialog.file_svg = true;
|
||||
} else if s == OUT_DEFAULT {
|
||||
self.plot_dialog.to_file = false;
|
||||
self.plot_dialog.file_svg = false;
|
||||
self.plot_dialog.printer = None;
|
||||
} else {
|
||||
self.plot_dialog.to_file = false;
|
||||
self.plot_dialog.file_svg = false;
|
||||
self.plot_dialog.printer = Some(s);
|
||||
}
|
||||
self.save_config();
|
||||
@@ -5405,6 +5429,7 @@ impl OpenCADStudio {
|
||||
let is_model = self.tabs[self.active_tab].scene.current_layout == "Model";
|
||||
let d = &mut self.plot_dialog;
|
||||
d.to_file = true;
|
||||
d.file_svg = false;
|
||||
let a4 = crate::io::paper_catalog::default_paper();
|
||||
d.paper = a4.canonical.to_string();
|
||||
d.orientation = "Landscape".into();
|
||||
@@ -5627,14 +5652,17 @@ impl OpenCADStudio {
|
||||
match crate::io::plot_device::PlotDevice::from_stored_name(&ps.printer_name).0 {
|
||||
crate::io::plot_device::PlotDevice::Pdf => {
|
||||
d.to_file = true;
|
||||
d.file_svg = false;
|
||||
d.printer = None;
|
||||
}
|
||||
crate::io::plot_device::PlotDevice::Printer(name) => {
|
||||
d.to_file = false;
|
||||
d.file_svg = false;
|
||||
d.printer = Some(name);
|
||||
}
|
||||
crate::io::plot_device::PlotDevice::None => {
|
||||
d.to_file = false;
|
||||
d.file_svg = false;
|
||||
d.printer = None;
|
||||
}
|
||||
}
|
||||
@@ -6862,6 +6890,26 @@ mod plot_paper_tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svg_device_is_session_state_and_stores_as_pdf() {
|
||||
let mut app = app_with_printer_named_sheet();
|
||||
let _ = app.on_plot_dialog_open();
|
||||
// The SVG device behaves like file output…
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::Printer(crate::ui::window::plot::OUT_SVG.into()));
|
||||
assert!(app.plot_dialog.to_file);
|
||||
assert!(app.plot_dialog.file_svg);
|
||||
// …but switching back to PDF (or a printer) clears it.
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::Printer(crate::ui::window::plot::OUT_PDF.into()));
|
||||
assert!(app.plot_dialog.to_file);
|
||||
assert!(!app.plot_dialog.file_svg);
|
||||
// The drawing never learns about SVG: page setups only know printers
|
||||
// and PDF drivers, so an SVG plot stores the PDF driver spelling.
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::Printer(crate::ui::window::plot::OUT_SVG.into()));
|
||||
let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent);
|
||||
let ps = app.tabs[app.active_tab].scene.plot_settings_for("Layout1").unwrap();
|
||||
assert_eq!(ps.printer_name, "DWG To PDF.pc3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_pdf_label_is_upgraded_on_save() {
|
||||
let mut app = app_with_printer_named_sheet();
|
||||
|
||||
+12
-8
@@ -9899,14 +9899,14 @@ impl OpenCADStudio {
|
||||
return Task::done(Message::PrintAllPdfPath(None));
|
||||
};
|
||||
iced::window::run(window_id, move |parent| {
|
||||
crate::io::pdf_export::pick_pdf_path_owned(stem, parent)
|
||||
crate::io::pdf_export::pick_plot_path_owned(stem, parent, false)
|
||||
})
|
||||
.map(Message::PrintAllPdfPath)
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Task::perform(
|
||||
crate::io::pdf_export::pick_pdf_path_async(stem),
|
||||
crate::io::pdf_export::pick_plot_path_async(stem, false),
|
||||
Message::PrintAllPdfPath,
|
||||
)
|
||||
}
|
||||
@@ -9945,27 +9945,30 @@ impl OpenCADStudio {
|
||||
.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());
|
||||
// The save dialog follows the dialog's file device, so the
|
||||
// SVG choice proposes `stem.svg` up front.
|
||||
let svg = self.plot_dialog.file_svg;
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "windows")))]
|
||||
{
|
||||
let Some(window_id) = self.main_window else {
|
||||
return Task::done(Message::PlotExportPath(None));
|
||||
};
|
||||
iced::window::run(window_id, move |parent| {
|
||||
crate::io::pdf_export::pick_pdf_path_owned(stem, parent)
|
||||
crate::io::pdf_export::pick_plot_path_owned(stem, parent, svg)
|
||||
})
|
||||
.map(Message::PlotExportPath)
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Task::perform(
|
||||
crate::io::pdf_export::pick_pdf_path_async(stem),
|
||||
crate::io::pdf_export::pick_plot_path_async(stem, svg),
|
||||
Message::PlotExportPath,
|
||||
)
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
Task::perform(
|
||||
crate::io::pdf_export::pick_pdf_path_owned(stem),
|
||||
crate::io::pdf_export::pick_plot_path_owned(stem),
|
||||
Message::PlotExportPath,
|
||||
)
|
||||
}
|
||||
@@ -9981,27 +9984,28 @@ impl OpenCADStudio {
|
||||
.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());
|
||||
let svg = self.plot_dialog.file_svg;
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "windows")))]
|
||||
{
|
||||
let Some(window_id) = self.main_window else {
|
||||
return Task::done(Message::PlotWindowExportPath(None));
|
||||
};
|
||||
iced::window::run(window_id, move |parent| {
|
||||
crate::io::pdf_export::pick_pdf_path_owned(stem, parent)
|
||||
crate::io::pdf_export::pick_plot_path_owned(stem, parent, svg)
|
||||
})
|
||||
.map(Message::PlotWindowExportPath)
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Task::perform(
|
||||
crate::io::pdf_export::pick_pdf_path_async(stem),
|
||||
crate::io::pdf_export::pick_plot_path_async(stem, svg),
|
||||
Message::PlotWindowExportPath,
|
||||
)
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
Task::perform(
|
||||
crate::io::pdf_export::pick_pdf_path_owned(stem),
|
||||
crate::io::pdf_export::pick_plot_path_owned(stem),
|
||||
Message::PlotWindowExportPath,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ pub mod ole_embed;
|
||||
pub mod paper_catalog;
|
||||
pub mod patterns;
|
||||
pub mod pdf_export;
|
||||
pub mod plot;
|
||||
pub mod plot_device;
|
||||
pub mod plot_style;
|
||||
pub mod print_to_printer;
|
||||
@@ -22,6 +23,7 @@ pub mod recovery;
|
||||
pub mod single_instance;
|
||||
pub mod step;
|
||||
pub mod stl;
|
||||
pub mod svg_export;
|
||||
pub mod thumbnail;
|
||||
pub mod update_check;
|
||||
pub mod visual_styles;
|
||||
|
||||
+44
-93
@@ -12,7 +12,6 @@ use crate::io::plot_style::PlotStyleTable;
|
||||
use crate::scene::model::hatch_model::HatchModel;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::scene::model::hatch_model::HatchPattern;
|
||||
use crate::scene::model::image_model::ImageModel;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::scene::model::wire_model::SearchableTextRun;
|
||||
use crate::scene::WireModel;
|
||||
@@ -25,27 +24,13 @@ use printpdf::{
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||
pub struct PlotWire {
|
||||
pub wire: WireModel,
|
||||
pub draw_depth: f32,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for PlotWire {
|
||||
type Target = WireModel;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.wire
|
||||
}
|
||||
}
|
||||
|
||||
/// Decoded image geometry plus inherited block/viewport clip boundaries.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PlotImage {
|
||||
pub image: ImageModel,
|
||||
pub clips: Vec<Vec<[f64; 2]>>,
|
||||
}
|
||||
// Shared plot model: `PlotWire`, `PlotImage`, `PlotContent`, `PlotGroupSplits`
|
||||
// and friends live in `super::plot`. The `Pdf*` names remain as aliases so
|
||||
// the scene, the print pipeline and every other call site keep compiling.
|
||||
pub use super::plot::{
|
||||
PlotContent, PlotGroupSplits, PlotImage, PlotOptions as PdfPlotOptions,
|
||||
PlotPage as PdfPageInput, PlotWire,
|
||||
};
|
||||
|
||||
// The web build has no `printpdf` (it pulls a wasm-incompatible `memchr` via
|
||||
// lopdf → nom_locate) and no filesystem, so PDF export is native-only; the web
|
||||
@@ -65,7 +50,7 @@ pub fn export_pdf_pages(
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn pick_pdf_path_owned(_stem: String) -> Option<std::path::PathBuf> {
|
||||
pub async fn pick_plot_path_owned(_stem: String) -> Option<std::path::PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -82,63 +67,6 @@ const LW_PX_TO_PT: f32 = MM_TO_PT / (96.0 / 25.4);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const SCREEN_DOT_MM: f32 = 25.4 / 96.0;
|
||||
|
||||
/// Output controls shared by preview, PDF export, and printer rendering.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PdfPlotOptions {
|
||||
pub object_lineweights: bool,
|
||||
pub scale_lineweights: bool,
|
||||
pub transparency: bool,
|
||||
pub stamp: bool,
|
||||
pub merge_lines: bool,
|
||||
}
|
||||
|
||||
/// End indexes of the first paper/model render group in each flat input list.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct PlotGroupSplits {
|
||||
pub wires: usize,
|
||||
pub hatches: usize,
|
||||
pub wipeouts: usize,
|
||||
pub images: usize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PlotContent {
|
||||
pub wires: std::sync::Arc<Vec<PlotWire>>,
|
||||
pub hatches: Vec<HatchModel>,
|
||||
pub wipeouts: Vec<HatchModel>,
|
||||
pub images: Vec<PlotImage>,
|
||||
pub group_splits: PlotGroupSplits,
|
||||
}
|
||||
|
||||
/// Owned geometry and settings for one PDF page.
|
||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||
pub struct PdfPageInput {
|
||||
pub content: PlotContent,
|
||||
/// Page dimensions in mm, after any 90/270-degree rotation.
|
||||
pub paper_w: f64,
|
||||
pub paper_h: f64,
|
||||
/// Absolute-world offsets stay f64 to preserve local detail at UTM coordinates.
|
||||
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,
|
||||
pub plot_style: Option<PlotStyleTable>,
|
||||
}
|
||||
|
||||
impl Default for PdfPlotOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
object_lineweights: true,
|
||||
scale_lineweights: false,
|
||||
transparency: false,
|
||||
stamp: false,
|
||||
merge_lines: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Public entry point ────────────────────────────────────────────────────
|
||||
|
||||
/// Export one page to a PDF file.
|
||||
@@ -381,28 +309,40 @@ fn image_triangle_matrix(p: [[f32; 2]; 3], uv: [[f32; 2]; 3]) -> Option<[f32; 6]
|
||||
])
|
||||
}
|
||||
|
||||
/// Show a parented PDF save-file dialog and return the chosen path.
|
||||
/// Show a parented plot save-file dialog and return the chosen path.
|
||||
///
|
||||
/// The parent comes from `iced::window::run`, keeping the portal request tied
|
||||
/// to the visible app window on Wayland instead of silently resolving to
|
||||
/// `None` on desktops that reject a parentless save dialog (#537).
|
||||
/// `svg` selects the SVG device's defaults (name, title, first filter).
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "windows")))]
|
||||
pub fn pick_pdf_path_owned(
|
||||
pub fn pick_plot_path_owned(
|
||||
stem: String,
|
||||
parent: &dyn iced::window::Window,
|
||||
svg: bool,
|
||||
) -> Option<std::path::PathBuf> {
|
||||
let path = crate::sys::blocking_file_dialog()
|
||||
.set_parent(parent)
|
||||
.set_title(crate::t!("Export as PDF").as_ref())
|
||||
.set_file_name(&format!("{stem}.pdf"))
|
||||
.add_filter(crate::t!("PDF Files").as_ref(), &["pdf"])
|
||||
let dialog = crate::sys::blocking_file_dialog().set_parent(parent);
|
||||
let dialog = if svg {
|
||||
dialog
|
||||
.set_title(crate::t!("Export as SVG").as_ref())
|
||||
.set_file_name(&format!("{stem}.svg"))
|
||||
.add_filter(crate::t!("SVG Files").as_ref(), &["svg"])
|
||||
.add_filter(crate::t!("PDF Files").as_ref(), &["pdf"])
|
||||
} else {
|
||||
dialog
|
||||
.set_title(crate::t!("Export as PDF").as_ref())
|
||||
.set_file_name(&format!("{stem}.pdf"))
|
||||
.add_filter(crate::t!("PDF Files").as_ref(), &["pdf"])
|
||||
.add_filter(crate::t!("SVG Files").as_ref(), &["svg"])
|
||||
};
|
||||
let path = dialog
|
||||
.add_filter(crate::t!("All Files").as_ref(), &["*"])
|
||||
.save_file()?;
|
||||
crate::config::remember_dialog_dir(&path);
|
||||
Some(path)
|
||||
}
|
||||
|
||||
/// Windows: pick the PDF destination with the async dialog on a worker
|
||||
/// Windows: pick the plot destination with the async dialog on a worker
|
||||
/// thread. The parented blocking dialog ran `IFileDialog::Show` on the UI
|
||||
/// thread inside the window callback, and when the target name already
|
||||
/// existed the overwrite-confirmation popup is a second nested modal that
|
||||
@@ -410,11 +350,22 @@ pub fn pick_pdf_path_owned(
|
||||
/// backend runs the dialog off-thread; it is the same pattern the DWG
|
||||
/// Save As flow uses, whose confirm popup works.
|
||||
#[cfg(all(not(target_arch = "wasm32"), target_os = "windows"))]
|
||||
pub async fn pick_pdf_path_async(stem: String) -> Option<std::path::PathBuf> {
|
||||
let handle = crate::sys::file_dialog()
|
||||
.set_title(crate::t!("Export as PDF").as_ref())
|
||||
.set_file_name(format!("{stem}.pdf"))
|
||||
.add_filter(crate::t!("PDF Files").as_ref(), &["pdf"])
|
||||
pub async fn pick_plot_path_async(stem: String, svg: bool) -> Option<std::path::PathBuf> {
|
||||
let dialog = crate::sys::file_dialog();
|
||||
let dialog = if svg {
|
||||
dialog
|
||||
.set_title(crate::t!("Export as SVG").as_ref())
|
||||
.set_file_name(format!("{stem}.svg"))
|
||||
.add_filter(crate::t!("SVG Files").as_ref(), &["svg"])
|
||||
.add_filter(crate::t!("PDF Files").as_ref(), &["pdf"])
|
||||
} else {
|
||||
dialog
|
||||
.set_title(crate::t!("Export as PDF").as_ref())
|
||||
.set_file_name(format!("{stem}.pdf"))
|
||||
.add_filter(crate::t!("PDF Files").as_ref(), &["pdf"])
|
||||
.add_filter(crate::t!("SVG Files").as_ref(), &["svg"])
|
||||
};
|
||||
let handle = dialog
|
||||
.add_filter(crate::t!("All Files").as_ref(), &["*"])
|
||||
.save_file()
|
||||
.await?;
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// Shared plot model — the paper-space geometry every plot backend consumes.
|
||||
//
|
||||
// `PdfPageInput`/`PdfPlotOptions` used to live in `pdf_export`, which made the
|
||||
// scene, the print pipeline and now the SVG exporter all import from the PDF
|
||||
// backend. The structs themselves are backend-agnostic (wires, hatches,
|
||||
// images, page size/offset/clip), so they live here; `pdf_export` re-exports
|
||||
// the historical `Pdf*` names as aliases so existing call sites keep working.
|
||||
|
||||
use crate::io::plot_style::PlotStyleTable;
|
||||
use crate::scene::WireModel;
|
||||
use crate::scene::model::hatch_model::HatchModel;
|
||||
use crate::scene::model::image_model::ImageModel;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||
pub struct PlotWire {
|
||||
pub wire: WireModel,
|
||||
pub draw_depth: f32,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for PlotWire {
|
||||
type Target = WireModel;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.wire
|
||||
}
|
||||
}
|
||||
|
||||
/// Decoded image geometry plus inherited block/viewport clip boundaries.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PlotImage {
|
||||
pub image: ImageModel,
|
||||
pub clips: Vec<Vec<[f64; 2]>>,
|
||||
}
|
||||
|
||||
/// Output controls shared by preview, plot exports, and printer rendering.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PlotOptions {
|
||||
pub object_lineweights: bool,
|
||||
pub scale_lineweights: bool,
|
||||
pub transparency: bool,
|
||||
pub stamp: bool,
|
||||
pub merge_lines: bool,
|
||||
}
|
||||
|
||||
/// End indexes of the first paper/model render group in each flat input list.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct PlotGroupSplits {
|
||||
pub wires: usize,
|
||||
pub hatches: usize,
|
||||
pub wipeouts: usize,
|
||||
pub images: usize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PlotContent {
|
||||
pub wires: std::sync::Arc<Vec<PlotWire>>,
|
||||
pub hatches: Vec<HatchModel>,
|
||||
pub wipeouts: Vec<HatchModel>,
|
||||
pub images: Vec<PlotImage>,
|
||||
pub group_splits: PlotGroupSplits,
|
||||
}
|
||||
|
||||
/// Owned geometry and settings for one plot page.
|
||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||
pub struct PlotPage {
|
||||
pub content: PlotContent,
|
||||
/// Page dimensions in mm, after any 90/270-degree rotation.
|
||||
pub paper_w: f64,
|
||||
pub paper_h: f64,
|
||||
/// Absolute-world offsets stay f64 to preserve local detail at UTM coordinates.
|
||||
pub offset_x: f64,
|
||||
pub offset_y: f64,
|
||||
pub rotation_deg: i32,
|
||||
pub scale: f32,
|
||||
pub clip: Option<(f32, f32, f32, f32)>,
|
||||
pub options: PlotOptions,
|
||||
pub plot_style: Option<PlotStyleTable>,
|
||||
}
|
||||
|
||||
impl Default for PlotOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
object_lineweights: true,
|
||||
scale_lineweights: false,
|
||||
transparency: false,
|
||||
stamp: false,
|
||||
merge_lines: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+16
-3
@@ -21,6 +21,7 @@ use std::fmt;
|
||||
/// Sentinel entries in the printer dropdown (not real printer names).
|
||||
pub const OUT_DEFAULT: &str = "System default printer";
|
||||
pub const OUT_PDF: &str = "Save to PDF file…";
|
||||
pub const OUT_SVG: &str = "Save to SVG file…";
|
||||
|
||||
/// Top-of-list entries: no page setup (defaults + PDF), and the last-used
|
||||
/// settings captured when the dialog opened.
|
||||
@@ -483,8 +484,13 @@ pub struct PlotDialogState {
|
||||
pub page_setup_on_new_layout: bool,
|
||||
/// Chosen printer name, or `None` for the system default.
|
||||
pub printer: Option<String>,
|
||||
/// Output goes to a PDF file instead of a printer.
|
||||
/// Output goes to a file instead of a printer.
|
||||
pub to_file: bool,
|
||||
/// File output is SVG instead of PDF (only meaningful when `to_file`).
|
||||
/// Session state, like the printer choice: never written into the
|
||||
/// drawing's page setup, which only knows printers and PDF drivers.
|
||||
#[serde(default)]
|
||||
pub file_svg: bool,
|
||||
pub paper: String,
|
||||
#[serde(skip)]
|
||||
pub paper_width_mm: f64,
|
||||
@@ -594,6 +600,7 @@ impl Default for PlotDialogState {
|
||||
page_setup_on_new_layout: false,
|
||||
printer: None,
|
||||
to_file: false,
|
||||
file_svg: false,
|
||||
paper: paper_catalog::default_paper().canonical.to_string(),
|
||||
paper_width_mm: 297.0,
|
||||
paper_height_mm: 210.0,
|
||||
@@ -650,6 +657,7 @@ impl PlotDialogState {
|
||||
pub fn copy_settings_from(&mut self, o: &PlotDialogState) {
|
||||
self.printer = o.printer.clone();
|
||||
self.to_file = o.to_file;
|
||||
self.file_svg = o.file_svg;
|
||||
self.paper = o.paper.clone();
|
||||
self.paper_width_mm = o.paper_width_mm;
|
||||
self.paper_height_mm = o.paper_height_mm;
|
||||
@@ -1146,7 +1154,11 @@ pub fn view_window(
|
||||
let action = if print_all_options {
|
||||
t!("Apply")
|
||||
} else if s.to_file {
|
||||
t!("Export PDF")
|
||||
if s.file_svg {
|
||||
t!("Export SVG")
|
||||
} else {
|
||||
t!("Export PDF")
|
||||
}
|
||||
} else {
|
||||
t!("Print")
|
||||
};
|
||||
@@ -1280,8 +1292,9 @@ pub fn view_window(
|
||||
let mut printer_opts = vec![default_entry.clone()];
|
||||
printer_opts.extend(s.printers.iter().cloned().map(PlotChoice::raw));
|
||||
printer_opts.push(PlotChoice::localized(OUT_PDF));
|
||||
printer_opts.push(PlotChoice::localized(OUT_SVG));
|
||||
let printer_sel = if s.to_file {
|
||||
Some(PlotChoice::localized(OUT_PDF))
|
||||
Some(PlotChoice::localized(if s.file_svg { OUT_SVG } else { OUT_PDF }))
|
||||
} else {
|
||||
Some(match &s.printer {
|
||||
Some(printer) => PlotChoice::raw(printer.clone()),
|
||||
|
||||
Reference in new issue
Block a user