From 446a62841ae7f3459a15480349ca79adee03e704 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 13:35:36 -0800 Subject: [PATCH] SVG Options: Show Code under Export Selection shows the selection Show Code in the SVG Options dialog that File > Export Selection opens encoded the whole document at the first artboard, while OK writes the selected objects alone, cropped to them. The code view now encodes the same selection document OK writes, without the artboard options, and encodes it again when the selection changes. --- crates/ui-egui/src/dialogs/svg_options.rs | 70 +++++++++++++++++++---- 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/crates/ui-egui/src/dialogs/svg_options.rs b/crates/ui-egui/src/dialogs/svg_options.rs index b477bb35c0..19737e4c16 100644 --- a/crates/ui-egui/src/dialogs/svg_options.rs +++ b/crates/ui-egui/src/dialogs/svg_options.rs @@ -269,30 +269,45 @@ fn artboards(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { }); } -/// The SVG the current options write for the first chosen artboard (read-only, selectable), -/// encoded again only when the options or the document change. +/// The SVG the current options write for the first chosen artboard, or under Export Selection for +/// the selection alone, cropped to it (read-only, selectable), encoded again only when the +/// options, the document or the selection change. fn code_view(app: &VectorcraftApp, ui: &mut egui::Ui, d: &Dialog) { let t = Tokens::get(ui.ctx()); let Some(st) = app.session.active() else { return }; + let selection = d.bool(super::export_as::SELECTION_FIELD); let mut opts = options(d); - if let Some(o) = opts.as_object_mut() - && let Some(r) = o.remove("range") - { - // The first artboard of the range (or the first one). - let first = r.as_str().and_then(|r| r.split([',', '-', '\u{2013}']).next()).and_then(|n| n.trim().parse::().ok()); - o.insert("artboard".into(), json!(first.map_or(0, |n| n.saturating_sub(1)))); + if let Some(o) = opts.as_object_mut() { + if selection { + // Export Selection writes one crop, without artboards to choose. + for k in vectorcraft_engine::cmd::fileio::ARTBOARD_PARAMS.iter().chain(&["useArtboards"]) { + o.remove(*k); + } + } else if let Some(r) = o.remove("range") { + // The first artboard of the range (or the first one). + let first = r.as_str().and_then(|r| r.split([',', '-', '\u{2013}']).next()).and_then(|n| n.trim().parse::().ok()); + o.insert("artboard".into(), json!(first.map_or(0, |n| n.saturating_sub(1)))); + } } - // One cached text, replaced when the document or the options change. + // One cached text, replaced when the document, the options or the selection change. let key = egui::Id::unique("svg-code"); // The read roots in force and the document's folder decide whether the text may be shown. let roots = vectorcraft_engine::file_access::current().map(|r| r.describe()); - let sig = egui::Id::unique((st.uid, st.revision, opts.to_string(), roots, st.path.as_deref())).value(); + let selected = selection.then(|| st.selection.objects.clone()); + let sig = egui::Id::unique((st.uid, st.revision, opts.to_string(), roots, st.path.as_deref(), selected)).value(); let code: Arc = match ui.ctx().data(|m| m.get_temp::<(u64, Arc)>(key)).filter(|(s, _)| *s == sig) { Some((_, c)) => c, None => { const MAX: usize = 200_000; - let encoded = vectorcraft_engine::cmd::fileio::check_linked_scripts("document.export", "svg", st) - .and_then(|()| vectorcraft_engine::cmd::fileio::encode_all(&st.doc, "svg", &json!({ "svg": opts }))); + let encoded = vectorcraft_engine::cmd::fileio::check_linked_scripts("document.export", "svg", st).and_then(|()| { + let params = json!({ "svg": opts }); + if selection { + let (doc, _) = vectorcraft_engine::cmd::fileio::selection_doc(st, "document.exportSelection", false)?; + vectorcraft_engine::cmd::fileio::encode_all(&doc, "svg", ¶ms) + } else { + vectorcraft_engine::cmd::fileio::encode_all(&st.doc, "svg", ¶ms) + } + }); let mut text = match encoded { // Shown as text whatever the file's encoding. Ok(enc) => enc @@ -431,6 +446,37 @@ mod tests { let _ = std::fs::remove_dir_all(root); } + /// Show Code under Export Selection shows the SVG that OK writes: the selection alone, cropped + /// to it with what its effects draw, not the whole document. + #[test] + fn show_code_under_export_selection_shows_the_selection() { + let (mut app, written) = app("/tmp/selection.svg"); + app.run("shape.rectangle", json!({"x": 70, "y": 40, "width": 20, "height": 20})).unwrap(); + app.run("effect.apply", json!({"effect": "stylize.dropShadow"})).unwrap(); + app.run("document.exportSelection", Value::Null).unwrap(); + let d = app.ui.dialog.as_mut().expect("SVG Options opened"); + assert_eq!(d.kind, KIND); + assert!(d.bool(super::super::export_as::SELECTION_FIELD)); + d.fields.insert("showCode".into(), json!(true)); + // The text the code view shows after two frames in one context. + let ctx = egui::Context::default(); + theme::install_fonts(&ctx); + for _ in 0..2 { + let mut out = ctx.run_ui(Default::default(), |ui| super::super::show(&mut app, ui.ctx())); + out.textures_delta.clear(); + } + let code = ctx.data(|m| m.get_temp::<(u64, Arc)>(egui::Id::unique("svg-code"))).map(|(_, c)| c.to_string()).unwrap_or_default(); + super::super::confirm(&mut app).unwrap(); + let svg = text(&written, 0); + assert_eq!(written.borrow()[0].0, "/tmp/selection.svg"); + assert_eq!(code, svg, "Show Code shows what OK writes"); + // The shadow reaches past the 21-point square, and the other rectangle is left out. + assert!(!svg.contains("viewBox=\"0 0 120 80\"") && !svg.contains("viewBox=\"0 0 21 21\""), "{svg}"); + assert!(!svg.contains("width=\"40\""), "{svg}"); + assert_eq!(svg.matches("