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:
Hakan Seven committed 2026-10-12 00:17:35 +03:00
commit fb474eb77c
10 files changed
+1274 -124

No files matched your search

+7
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
)
}