diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index 1cb8a4f38..2a07cd55a 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -959,6 +959,12 @@ pub struct Document { /// engine sets it from the selection's ([`Selection::new_art`]). Editing state, never saved. #[serde(skip)] pub new_art: NewArtIds, + /// The highest artboard id given out ([`Document::next_artboard_id`]) or deleted from the + /// document ([`Document::retire_artboard_id`]): a new artboard takes a higher one. Shared by + /// every copy of the document, the undo history's included, so an undo does not lower it. + /// Never saved. + #[serde(skip)] + pub retired_artboard_id: RetiredArtboardId, /// Some object is a dimension (the Dimension tool): edits refresh dimensions only while this /// is set, so documents without any pay nothing for them. #[serde(default, skip_serializing_if = "std::ops::Not::not")] @@ -968,6 +974,27 @@ pub struct Document { index: id_index::IdIndex, } +/// [`Document::retired_artboard_id`]: not document content, so two documents are equal whatever it +/// holds. A clone shares the value with the document it was cloned from. +#[derive(Clone, Debug, Default)] +pub struct RetiredArtboardId(Arc); + +impl RetiredArtboardId { + pub fn get(&self) -> u32 { + self.0.load(std::sync::atomic::Ordering::Relaxed) + } + /// Raise the value to `id` when `id` is higher. + pub fn raise(&self, id: u32) { + self.0.fetch_max(id, std::sync::atomic::Ordering::Relaxed); + } +} + +impl PartialEq for RetiredArtboardId { + fn eq(&self, _: &Self) -> bool { + true + } +} + fn ppi72() -> f64 { 72.0 } @@ -1052,6 +1079,7 @@ impl Document { puppet, new_art, dimensions, + retired_artboard_id: _, index: _, } = self; *version == other.version @@ -1225,6 +1253,7 @@ impl Document { puppet: None, new_art: Default::default(), dimensions: false, + retired_artboard_id: Default::default(), index: Default::default(), }; let id = d.alloc_id(); @@ -1940,8 +1969,18 @@ impl Document { self.guides.iter().filter(|g| g.artboard == Some(from)).map(|g| Guide { artboard: Some(to), ..g.moved(d) }).collect(); self.guides.extend(copies); } + /// An id for a new artboard: one no artboard has, above the ids given out before and the ids of + /// the artboards deleted from the document ([`Document::retired_artboard_id`]). The id counts + /// as given out, so an artboard made after an undo does not take the id of one the undo took + /// away. pub fn next_artboard_id(&self) -> u32 { - free_id(self.artboards.iter().map(|a| a.id)) + let id = free_id(self.artboards.iter().map(|a| a.id).chain([self.retired_artboard_id.get()])); + self.retired_artboard_id.raise(id); + id + } + /// Record that the artboard with `id` was deleted: no later artboard takes its id. + pub fn retire_artboard_id(&self, id: u32) { + self.retired_artboard_id.raise(id); } pub fn pattern(&self, name: &str) -> Option<&PatternDef> { self.patterns.iter().find(|p| p.name == name) diff --git a/crates/engine/src/cmd/fileio/mod.rs b/crates/engine/src/cmd/fileio/mod.rs index bea905946..18314cc2e 100644 --- a/crates/engine/src/cmd/fileio/mod.rs +++ b/crates/engine/src/cmd/fileio/mod.rs @@ -74,8 +74,8 @@ pub use native::with_compression_pref; pub use pdfimport::{LoadOptions, page_document}; pub(crate) use save::job_for; pub use save::{ - SAVE_FORMATS, SaveJob, SaveMode, SavePlan, StreamWrite, TEMPLATE_FORMATS, export_folder, save_filters, save_format, save_job, save_plan, - save_with, stamp_save_dates, template_filters, templates_dialog_folder, templates_folder, + SAVE_FORMATS, SaveJob, SaveMode, SavePlan, StreamWrite, TEMPLATE_FORMATS, export_folder, remembered_save_options, save_filters, save_format, + save_job, save_plan, save_with, stamp_save_dates, template_filters, templates_dialog_folder, templates_folder, }; pub use screens::{ PRESET_KEYS as SCREEN_EXPORT_PRESET_KEYS, PRESETS as SCREEN_PRESETS, ScreenExport, ScreenExportPreset, ScreenSize, diff --git a/crates/engine/src/cmd/fileio/save.rs b/crates/engine/src/cmd/fileio/save.rs index 170b55f88..bc0171e19 100644 --- a/crates/engine/src/cmd/fileio/save.rs +++ b/crates/engine/src/cmd/fileio/save.rs @@ -14,7 +14,8 @@ use vectorcraft_doc::Document; use super::super::*; use super::{ - ArtboardPick, Encoded, Format, Loaded, encode_all, file_stem, format, format_for_name, load, read_file, with_compression_pref, write_encoded, + ARTBOARD_PARAMS, ArtboardPick, Encoded, Format, Loaded, encode_all, file_stem, format, format_for_name, load, read_file, with_compression_pref, + write_encoded, }; use crate::{DocState, Prefs}; @@ -253,7 +254,7 @@ pub fn save_plan(s: &Session, mode: SaveMode, p: &Value) -> Result { } } // None given: the ones the document was last saved with in this format. - let options = if given.is_empty() && format.id == st.format { st.save_options.clone() } else { given }; + let options = if given.is_empty() && format.id == st.format { remembered_save_options(st) } else { given }; // Save writes the document's own file; the other modes only where they are told to. let path = path.or_else(|| (mode == SaveMode::Save && !st.converted && !st.recovered && format.id == st.format).then(|| st.path.clone()).flatten()); @@ -275,6 +276,99 @@ pub fn save_plan(s: &Session, mode: SaveMode, p: &Value) -> Result { Ok(SavePlan { mode, path, format, options, name, folder, modified }) } +/// The options `st` was last saved with, their artboards (`artboard`, `artboards` and `range`) +/// following the artboards they named ([`DocState::save_artboards`]): an artboard deleted since is +/// left out, and one that moved is named by its new number. When none of the artboards the save +/// used is left, the pick is left out, and a save without options writes the artboards it writes +/// when none is named. `range` `"all"` stays as it is. Artboards given to a save are still checked. +pub fn remembered_save_options(st: &DocState) -> Map { + let mut options = st.save_options.clone(); + let ids: Vec = st.doc.artboards.iter().map(|a| a.id).collect(); + // The encoders refuse a document without artboards themselves. + if ids.is_empty() { + return options; + } + // Artboard `i` of the save, where it is now (by number when the save's artboards are unknown). + let then = &st.save_artboards; + let count = if then.is_empty() { ids.len() } else { then.len() }; + let now = |i: usize| match then.get(i) { + Some(id) => ids.iter().position(|x| x == id), + None => (then.is_empty() && i < ids.len()).then_some(i), + }; + // Each pick that names artboards: `Some` of where they are now (empty: all gone). + let mut moved: Vec<(&str, Option>)> = vec![]; + for k in ARTBOARD_PARAMS { + let named = match (k, options.get(k)) { + ("range", Some(Value::String(r))) if r.trim().is_empty() || r.trim().eq_ignore_ascii_case("all") => continue, + ("range", Some(Value::String(r))) => match vectorcraft_doc::range::parse_range(r, count) { + Ok(v) => v, + Err(_) => { + moved.push((k, Some(vec![]))); + continue; + } + }, + ("artboards", Some(v @ Value::Array(_))) => match serde_json::from_value::>(v.clone()) { + Ok(v) => v, + Err(_) => continue, + }, + ("artboard", Some(v)) => match v.as_u64().and_then(|i| usize::try_from(i).ok()) { + Some(i) => vec![i], + None => continue, + }, + _ => continue, + }; + let there: Vec = named.iter().filter_map(|i| now(*i)).collect(); + // A range that reads differently against the artboards there are now (`"2-"` once one + // was added) is written out as the artboards it named. + let reads = match (k, options.get(k)) { + ("range", Some(Value::String(r))) => vectorcraft_doc::range::parse_range(r, ids.len()).ok(), + _ => Some(named), + }; + moved.push((k, (reads.as_ref() != Some(&there)).then_some(there))); + } + // The pick a save uses (as `ArtboardPick::resolve` reads it: `range`, then `artboards`, then + // `artboard`) is gone: so is the rest. + let used = ["range", "artboards", "artboard"].into_iter().find(|k| match options.get(*k) { + Some(Value::String(r)) => !r.trim().is_empty(), + Some(v) => !v.is_null(), + None => false, + }); + let gone = |k: &str| moved.iter().any(|(m, there)| *m == k && there.as_ref().is_some_and(Vec::is_empty)); + if used.is_some_and(gone) { + for k in ARTBOARD_PARAMS { + options.remove(k); + } + return options; + } + for (k, there) in moved { + let Some(there) = there else { continue }; + let value = match (k, there.first()) { + (_, None) => None, + ("range", Some(_)) => Some(json!(range_text(&there))), + ("artboards", Some(_)) => Some(json!(there)), + (_, Some(i)) => Some(json!(i)), + }; + match value { + Some(v) => _ = options.insert(k.into(), v), + None => _ = options.remove(k), + } + } + options +} + +/// 0-based artboard indices as a 1-based range in the same order, runs joined (`"1-2, 5"`). +fn range_text(picked: &[usize]) -> String { + let mut runs: Vec<(usize, usize)> = vec![]; + for &i in picked { + match runs.last_mut() { + Some((_, end)) if *end + 1 == i => *end = i, + _ => runs.push((i, i)), + } + } + let run = |&(a, b): &(usize, usize)| if a == b { format!("{}", a + 1) } else { format!("{}-{}", a + 1, b + 1) }; + runs.iter().map(run).collect::>().join(", ") +} + /// Does a save in `f` read option `key`? PDF (and a .ai file, a PDF) also reads its General /// settings (`createLayers`…) and presets. fn reads_option(f: &Format, key: &str) -> bool { @@ -664,6 +758,7 @@ impl SaveJob { st.path = Some(path); st.format = self.plan.format.id; st.save_options = self.plan.options; + st.save_artboards = self.snapshot.artboards.iter().map(|a| a.id).collect(); st.converted = false; st.recovered = false; if let Some(at) = self.stamped { @@ -756,14 +851,14 @@ fn format_options(s: &mut Session, p: &Value) -> Result { let f = format(id) .filter(|f| f.write || SAVE_FORMATS.contains(&f.id) || TEMPLATE_FORMATS.contains(&f.id)) .ok_or_else(|| bad(C, format!("`{id}` is no writable format (see document.formats)")))?; - let saved = st.filter(|d| d.format == f.id).map(|d| &d.save_options); + let saved = st.filter(|d| d.format == f.id).map(remembered_save_options); // A native save compresses as Use Compression says, unless told otherwise. let prefs = with_compression_pref(&s.prefs, &json!({})); let mut v = f.to_json(); if let Some(options) = v["options"].as_object_mut() { for (name, o) in options.iter_mut() { let default = prefs.get(name).filter(|_| matches!(f.id, "vectorcraft" | "template")).unwrap_or(&o["default"]); - o["value"] = saved.and_then(|m| m.get(name)).unwrap_or(default).clone(); + o["value"] = saved.as_ref().and_then(|m| m.get(name)).unwrap_or(default).clone(); } } v["saveFormats"] = diff --git a/crates/engine/src/cmd/layer.rs b/crates/engine/src/cmd/layer.rs index 5c31ca009..c90ac89da 100644 --- a/crates/engine/src/cmd/layer.rs +++ b/crates/engine/src/cmd/layer.rs @@ -866,6 +866,7 @@ fn artboard_delete(s: &mut Session, p: &Value) -> Result { // The last first, so the others keep their places; their guides go with them. for &i in indices.iter().rev() { let id = d.artboards.remove(i).id; + d.retire_artboard_id(id); d.retain_guides(sel, |_, g| g.artboard != Some(id)); } Ok(()) diff --git a/crates/engine/src/cmd/menucmds.rs b/crates/engine/src/cmd/menucmds.rs index fe0cb1c52..c9ff3f455 100644 --- a/crates/engine/src/cmd/menucmds.rs +++ b/crates/engine/src/cmd/menucmds.rs @@ -969,10 +969,12 @@ fn convert_to_artboards(s: &mut Session, _: &Value) -> Result { if b.width() <= 0.0 || b.height() <= 0.0 { continue; } + // Named one past the highest artboard id the document has, whatever id it is given. + let number = d.artboards.iter().map(|a| a.id).max().map_or(1, |m| m.saturating_add(1)); let aid = d.next_artboard_id(); d.artboards.push(vectorcraft_doc::Artboard { id: aid, - name: format!("Artboard {aid}"), + name: format!("Artboard {number}"), rect: b, locked: false, show_center_mark: false, diff --git a/crates/engine/src/cmd/panelcmds.rs b/crates/engine/src/cmd/panelcmds.rs index b99e6d9b0..d48b76492 100644 --- a/crates/engine/src/cmd/panelcmds.rs +++ b/crates/engine/src/cmd/panelcmds.rs @@ -208,6 +208,7 @@ pub(crate) fn artboard_copy(s: &mut Session, p: &Value, cut: bool) -> Result, + /// The ids of the document's artboards, in order, when `save_options` were stored: the + /// artboards their `artboard`, `artboards` and `range` named by number then + /// ([`cmd::fileio::remembered_save_options`]). + pub save_artboards: Vec, /// Opened from an older native file (former name or format version): the title says /// "[Converted]" and Save asks for a new name instead of overwriting it. pub converted: bool, @@ -569,6 +573,7 @@ impl DocState { transparency_grid, format: "vectorcraft", save_options: Default::default(), + save_artboards: vec![], converted: false, view, recovered: false, @@ -1682,6 +1687,17 @@ impl Session { st.revision = old.revision + 1; st.format = old.format; st.save_options = old.save_options.clone(); + // The remembered artboard pick follows the artboards it named when the file read back has + // the artboards the document had at the save, in the same order and with the same ids (a + // native file keeps them; a PDF's pages get new ids in page order); otherwise it is left + // out. + if old.save_artboards.is_empty() || st.doc.artboards.iter().map(|a| a.id).eq(old.save_artboards.iter().copied()) { + st.save_artboards = old.save_artboards.clone(); + } else { + for k in cmd::fileio::ARTBOARD_PARAMS { + st.save_options.remove(k); + } + } st.converted = old.converted; st.imported_from = old.imported_from.clone(); st.import_losses = old.import_losses.clone(); diff --git a/crates/engine/src/tests_menucmds.rs b/crates/engine/src/tests_menucmds.rs index 115665ac6..c0fb6de45 100644 --- a/crates/engine/src/tests_menucmds.rs +++ b/crates/engine/src/tests_menucmds.rs @@ -977,6 +977,24 @@ fn convert_to_artboards_and_rearrange() { assert_eq!(bounds(&s, b), Rect::new(50.0, 675.0, 60.0, 685.0)); } +/// Convert to Artboards names an artboard one past the highest artboard id the document has, also +/// after a delete of the last artboard or an undo of a new one. +#[test] +fn convert_to_artboards_names_by_the_artboards_the_document_has() { + let name_after = |change: &dyn Fn(&mut Session)| { + let mut s = session(); + s.execute("artboard.new", &json!({})).unwrap(); + s.execute("artboard.new", &json!({})).unwrap(); + change(&mut s); + let a = rect(&mut s, 3000.0, 0.0, 200.0, 100.0); + sel(&mut s, &[a]); + s.execute("artboard.convertToArtboards", &json!({})).unwrap(); + s.doc().unwrap().doc.artboards.last().map(|b| b.name.clone()).unwrap() + }; + assert_eq!(name_after(&|s| _ = s.execute("artboard.delete", &json!({"index": 2})).unwrap()), "Artboard 3"); + assert_eq!(name_after(&|s| _ = s.execute("edit.undo", &json!({})).unwrap()), "Artboard 3"); +} + /// Rearrange All Artboards refuses `columns` that are not a whole number (a fraction, a negative number or a string), /// and the artboards stay where they were; 1.0, as the Rearrange All Artboards dialog sends a count, is one column. #[test] diff --git a/crates/engine/src/tests_saveoptions.rs b/crates/engine/src/tests_saveoptions.rs index f0adc4322..9c5bb66a9 100644 --- a/crates/engine/src/tests_saveoptions.rs +++ b/crates/engine/src/tests_saveoptions.rs @@ -188,6 +188,130 @@ fn ai_without_pdf_content_and_compression() { assert!(loose.len() > packed.len(), "{} vs {}", loose.len(), packed.len()); } +#[test] +fn a_remembered_artboard_the_document_no_longer_has_is_dropped() { + let dir = Folder::new("remembered-artboards"); + for (name, p) in [ + ("legacy.ai", json!({"aiVersion": "7", "range": "3"})), + ("separate.ai", json!({"separateArtboards": true, "range": "3"})), + ("separate.vectorcraft", json!({"separateArtboards": true, "range": "1, 3"})), + ("doc.pdf", json!({"range": "3"})), + ("doc.svg", json!({"artboard": 2})), + ] { + let mut s = three_boards(); + let mut first = p.clone(); + first["path"] = json!(dir.file(name)); + s.execute("file.saveAs", &first).unwrap_or_else(|e| panic!("{name}: {e}")); + s.execute("artboard.delete", &json!({"index": 2})).unwrap(); + let v = s.execute("file.formatOptions", &json!({})).unwrap(); + for k in ["artboard", "range"].iter().filter(|k| p.get(**k).is_some()) { + assert_ne!(v["options"][*k]["value"], p[*k], "{name}: Save no longer uses {k}"); + } + s.execute("document.save", &json!({})).unwrap_or_else(|e| panic!("{name}: {e}")); + // Given to the save, it is still refused. + let e = s.execute("document.save", &p).unwrap_err().to_string(); + assert!(e.contains("from 1 to 2") || e.contains("no artboard 2"), "{name}: {e}"); + } + // A remembered artboard that is still there is kept. + let mut s = three_boards(); + s.execute("file.saveAs", &json!({"path": dir.file("kept.ai"), "aiVersion": "7", "range": "2"})).unwrap(); + s.execute("artboard.delete", &json!({"index": 2})).unwrap(); + let v = s.execute("file.formatOptions", &json!({})).unwrap(); + assert_eq!(v["options"]["range"]["value"], json!("2")); + let r = s.execute("document.save", &json!({})).unwrap(); + assert!(r["path"].as_str().is_some_and(|p| p.ends_with("kept.ai")), "{r}"); +} + +#[test] +fn a_remembered_artboard_pick_follows_the_artboards_it_named() { + let dir = Folder::new("remembered-pick"); + // Save each artboard of A, B and C to a file with `range`, change the artboards, then save + // without options: the range Save now uses and the artboards it writes, by file name. + let save = |range: &str, change: &[(&str, Value)]| -> (Value, Vec) { + let mut s = three_boards(); + let path = dir.file("x.ai"); + s.execute("file.saveAs", &json!({"path": path, "separateArtboards": true, "range": range})).unwrap(); + for (cmd, p) in change { + s.execute(cmd, p).unwrap(); + } + let range = s.execute("file.formatOptions", &json!({})).unwrap()["options"]["range"]["value"].clone(); + let r = s.execute("document.save", &json!({})).unwrap(); + let files = r["files"].as_array().unwrap().iter().skip(1).map(|f| f.as_str().unwrap().rsplit(['/', '\\']).next().unwrap().to_string()); + (range, files.collect()) + }; + let delete = |i: usize| ("artboard.delete", json!({"index": i})); + assert_eq!(save("1, 3", &[delete(2)]), (json!("1"), vec!["x-A.ai".to_string()])); + assert_eq!(save("1, 3", &[delete(0)]), (json!("2"), vec!["x-C.ai".to_string()])); + assert_eq!(save("1-3", &[delete(1)]), (json!("1-2"), vec!["x-A.ai".to_string(), "x-C.ai".to_string()])); + let (range, mut files) = save("1, 3", &[("artboard.reorder", json!({"index": 2, "to": 0}))]); + files.sort(); + assert_eq!((range, files), (json!("2, 1"), vec!["x-A.ai".to_string(), "x-C.ai".to_string()])); + // None of them left: every artboard, as when none is named. + assert_eq!(save("1, 3", &[delete(2), delete(0)]), (Value::Null, vec!["x-B.ai".to_string()])); + assert_eq!(save("all", &[delete(0)]), (json!("all"), vec!["x-B.ai".to_string(), "x-C.ai".to_string()])); + // An artboard made after one was deleted is not taken for it. + let new = ("artboard.new", json!({"x": 600, "y": 0, "width": 100, "height": 100, "name": "D"})); + let all = ["x-A.ai", "x-B.ai", "x-D.ai"].map(String::from).to_vec(); + assert_eq!(save("3", &[delete(2), new.clone()]), (Value::Null, all)); + assert_eq!(save("1, 3", &[delete(2), new.clone()]), (json!("1"), vec!["x-A.ai".to_string()])); + // A range open at its end keeps to the artboards it named. + assert_eq!(save("2-", std::slice::from_ref(&new)), (json!("2-3"), vec!["x-B.ai".to_string(), "x-C.ai".to_string()])); + // Nor is one made after undoing the artboard (and the three rectangles made after it). + let undo = ("edit.undo", json!({})); + let undone = [undo.clone(), undo.clone(), undo.clone(), undo, new]; + assert_eq!(save("1, 3", &undone), (json!("1"), vec!["x-A.ai".to_string()])); + + // The other saves that remember a pick: a PDF's range, an SVG's artboard, a legacy .ai's + // range and a native file's separate files. + for (name, p, gone, after) in [ + ("doc.pdf", json!({"range": "1, 3"}), 0, json!({"range": "2"})), + // `range` wins over `artboard`, which a save then does not use. + ("unused.pdf", json!({"artboard": 0, "range": "3"}), 0, json!({"range": "2"})), + ("doc.svg", json!({"artboard": 2}), 0, json!({"artboard": 1})), + ("legacy.ai", json!({"aiVersion": "7", "range": "1, 3"}), 2, json!({"range": "1"})), + ("doc.vectorcraft", json!({"separateArtboards": true, "range": "1, 3"}), 0, json!({"range": "2"})), + ] { + let mut s = three_boards(); + let mut first = p.clone(); + first["path"] = json!(dir.file(name)); + s.execute("file.saveAs", &first).unwrap(); + s.execute("artboard.delete", &json!({"index": gone})).unwrap(); + let v = s.execute("file.formatOptions", &json!({})).unwrap(); + for (k, value) in after.as_object().unwrap() { + assert_eq!(&v["options"][k]["value"], value, "{name}: {k}"); + } + s.execute("document.save", &json!({})).unwrap_or_else(|e| panic!("{name}: {e}")); + if name.ends_with(".pdf") { + let bytes = std::fs::read(dir.file(name)).unwrap(); + let info = s.execute("document.pdfInfo", &json!({"dataBase64": vectorcraft_format::base64_encode(&bytes)})).unwrap(); + assert_eq!(info["pages"], 1, "only the artboard that was 3"); + } + } + + // File > Revert: a PDF holds only the pages it was saved with, so its pick is left out; a + // native file keeps every artboard, and the pick stays. + let mut s = three_boards(); + s.execute("file.saveAs", &json!({"path": dir.file("revert.pdf"), "range": "1, 3"})).unwrap(); + s.execute("shape.rectangle", &json!({"x": 10, "y": 50, "width": 30, "height": 30})).unwrap(); + s.execute("file.revert", &json!({})).unwrap(); + assert_eq!(s.doc().unwrap().doc.artboards.len(), 2); + assert_eq!(s.execute("file.formatOptions", &json!({})).unwrap()["options"]["range"]["value"], Value::Null); + // A PDF without the editable document reads back with new ids in page order: after a reorder, + // the pick is left out, not taken for another reorder. + let mut s = three_boards(); + s.execute("artboard.reorder", &json!({"index": 2, "to": 0})).unwrap(); + s.execute("file.saveAs", &json!({"path": dir.file("reordered.pdf"), "range": "1-3", "preserveEditing": false})).unwrap(); + s.execute("shape.rectangle", &json!({"x": 10, "y": 50, "width": 30, "height": 30})).unwrap(); + s.execute("file.revert", &json!({})).unwrap(); + assert_eq!(s.doc().unwrap().doc.artboards.iter().map(|a| a.id).collect::>(), [1, 2, 3]); + assert_eq!(s.execute("file.formatOptions", &json!({})).unwrap()["options"]["range"]["value"], Value::Null); + let mut s = three_boards(); + s.execute("file.saveAs", &json!({"path": dir.file("revert.vectorcraft"), "separateArtboards": true, "range": "1, 3"})).unwrap(); + s.execute("artboard.delete", &json!({"index": 0})).unwrap(); + s.execute("file.revert", &json!({})).unwrap(); + assert_eq!(s.execute("file.formatOptions", &json!({})).unwrap()["options"]["range"]["value"], json!("1, 3")); +} + #[test] fn format_options_list_the_save_options() { let mut s = three_boards(); diff --git a/crates/ui-egui/src/dialogs/svg_options.rs b/crates/ui-egui/src/dialogs/svg_options.rs index 0fd6998b0..611d1f8ff 100644 --- a/crates/ui-egui/src/dialogs/svg_options.rs +++ b/crates/ui-egui/src/dialogs/svg_options.rs @@ -92,7 +92,7 @@ pub fn open(app: &mut VectorcraftApp, mode: Mode, path: Option<&str>) { .session .active() .filter(|st| mode != Mode::Export && matches!(st.format, "svg" | "svgz")) - .map(|st| Value::Object(st.save_options.clone())); + .map(|st| Value::Object(vectorcraft_engine::cmd::fileio::remembered_save_options(st))); for last in [&app.ui.svg_options].into_iter().chain(saved.as_ref()) { if let Some(o) = last.as_object() { fields.extend(o.iter().map(|(k, v)| (k.clone(), v.clone()))); @@ -416,6 +416,18 @@ mod tests { assert!(vectorcraft_format::sniff(&written.borrow()[3].1)); } + #[test] + fn save_as_svg_leaves_out_a_remembered_artboard_the_document_no_longer_has() { + let (mut app, written) = app("/tmp/doc.svg"); + app.run("file.saveAs", json!({"path": "/tmp/doc.svg", "svg": {"artboard": 1}})).unwrap(); + assert_eq!(app.session.active().unwrap().save_options["artboard"], 1); + app.run("artboard.delete", json!({"index": 1})).unwrap(); + app.run("file.saveAs", Value::Null).unwrap(); + assert!(app.ui.dialog.as_ref().unwrap().fields.get("artboard").is_none()); + super::super::confirm(&mut app).unwrap(); + assert_eq!((written.borrow().len(), written.borrow()[1].0.as_str()), (2, "/tmp/doc.svg")); + } + #[test] fn profile_encoding_and_embedded_fonts_reach_the_file() { let (mut app, written) = app("/tmp/tiny.svg");