From a353ad3e7ad8f3bc528153a1be79da83a6639e21 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Thu, 29 Oct 2026 19:30:59 +0700 Subject: [PATCH] Fix #4523: a new document's VectorCraft Options start as the last native save used them Preview, Embed ICC Profiles, Create PDF-Compatible File and the other VectorCraft Options went back to their defaults for every document not saved yet. A native Save or Save As now records the options it used in the preferences (not the artboards it picked, nor Use Compression, which its preference decides), and file.formatOptions offers them for a document not yet saved as a native file. A document saved before keeps its own. --- crates/engine/src/cmd/fileio/save.rs | 12 ++++++++++-- crates/engine/src/prefs.rs | 7 +++++++ crates/engine/src/tests/save.rs | 16 ++++++++++++++++ docs/mcp.md | 3 ++- 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/cmd/fileio/save.rs b/crates/engine/src/cmd/fileio/save.rs index da1a4f5ac..6a573a40a 100644 --- a/crates/engine/src/cmd/fileio/save.rs +++ b/crates/engine/src/cmd/fileio/save.rs @@ -867,6 +867,11 @@ impl SaveJob { /// its own also ends its Data Recovery copy. pub fn complete(self, s: &mut Session, uid: u64) { let saved = self.plan.retargets() && self.plan.path.is_some(); + if saved && self.plan.format.id == "vectorcraft" { + let mut last = self.plan.options.clone(); + last.retain(|k, _| !ARTBOARD_PARAMS.contains(&k.as_str()) && !matches!(k.as_str(), "separateArtboards" | "compress")); + s.prefs.native_save_options = last; + } // The other open documents that place this one show the new version. let written = self.plan.path.clone().filter(|_| matches!(self.plan.format.id, "vectorcraft" | "template")); if let Some(st) = s.document_mut(uid) { @@ -932,7 +937,10 @@ 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(remembered_save_options); + // A document not yet saved as a native file starts from the native options used last. + let saved = st.filter(|d| d.format == f.id).map(remembered_save_options).filter(|m| !m.is_empty()).or_else(|| { + (f.id == "vectorcraft" && !s.prefs.native_save_options.is_empty()).then(|| s.prefs.native_save_options.clone()) + }); // 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(); @@ -1008,7 +1016,7 @@ pub(super) fn specs() -> Vec { "Format Options", [], None, - "{format?: a writable format id or ait (default: the document's own)} → {id, label, extensions, mime, read, write, raster, options: {name: {type, default, description, value}}, saveFormats: [{id, label, extensions}]}; value = the document's option as last saved in that format, else the default", + "{format?: a writable format id or ait (default: the document's own)} → {id, label, extensions, mime, read, write, raster, options: {name: {type, default, description, value}}, saveFormats: [{id, label, extensions}]}; value = the document's option as last saved in that format, else (native, for a document not yet saved as one) as the last native save used it, else the default", always, format_options ), diff --git a/crates/engine/src/prefs.rs b/crates/engine/src/prefs.rs index cb3bea46e..5f2a8e8d4 100644 --- a/crates/engine/src/prefs.rs +++ b/crates/engine/src/prefs.rs @@ -247,6 +247,12 @@ pub struct Prefs { /// Preferences dialog field: resetting the preferences keeps it; `webExport.presets.*` edit it. #[serde(skip_serializing_if = "Vec::is_empty")] pub web_export_presets: Vec, + /// The VectorCraft Options a native Save or Save As used last (Preview, Embed ICC Profiles, + /// Create PDF-Compatible File…; not the artboards it picked, nor Use Compression, which its + /// preference decides): `file.formatOptions` offers them for a document not yet saved as a + /// native file. Not a Preferences dialog field. + #[serde(skip_serializing_if = "serde_json::Map::is_empty")] + pub native_save_options: serde_json::Map, /// Save for Web: the settings the dialog opens on (`webExport.settings`); none until set. #[serde(skip_serializing_if = "Option::is_none")] pub web_export_settings: Option, @@ -442,6 +448,7 @@ impl Default for Prefs { append_converted: true, use_compression: false, web_export_presets: vec![], + native_save_options: serde_json::Map::new(), web_export_settings: None, print_presets: vec![], constrain_proportions: false, diff --git a/crates/engine/src/tests/save.rs b/crates/engine/src/tests/save.rs index e4828375e..9206e7b22 100644 --- a/crates/engine/src/tests/save.rs +++ b/crates/engine/src/tests/save.rs @@ -99,6 +99,22 @@ fn save_as_takes_on_path_format_and_options() { let _ = std::fs::remove_dir_all(d); } +/// #4523: a new document's VectorCraft Options start as the last native save used them (not its +/// artboard range, nor Use Compression, which its preference decides). +#[test] +fn a_new_document_offers_the_native_options_used_last() { + let d = dir("lastnative"); + let mut s = session(); + let opts = json!({"preview": true, "embedProfiles": false, "pdfCompatible": true, "separateArtboards": true, "range": "1"}); + s.execute("file.saveAs", &json!({"path": path(&d, "a.vectorcraft"), "options": opts})).unwrap(); + s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); + let o = s.execute("file.formatOptions", &json!({"format": "vectorcraft"})).unwrap(); + let value = |k: &str| o["options"][k]["value"].clone(); + assert_eq!((value("preview"), value("embedProfiles"), value("pdfCompatible")), (json!(true), json!(false), json!(true))); + assert_eq!((value("separateArtboards"), value("range")), (o["options"]["separateArtboards"]["default"].clone(), o["options"]["range"]["default"].clone())); + let _ = std::fs::remove_dir_all(d); +} + #[test] fn another_format_without_a_path_hands_the_bytes_back() { let d = dir("otherfmt"); diff --git a/docs/mcp.md b/docs/mcp.md index fc9d738e7..536e48bf9 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -588,7 +588,8 @@ it replaced by the template's extension, and any other path that does not end in `card.png` is saved as `card.vctemplate` and `card` as `card.vctemplate`; the suggested name and the Templates folder come back without a path), `file.newFromTemplate {path}`, `file.revert` and `file.formatOptions {format?}` (a format's options with the values a save would use). Without a path the save commands return `{dataBase64, name, folder?, warnings}`. A document saved as SVG or PDF -remembers its options, so the next save reuses them. A PDF opened with `pages`, `cropTo` or `password` is only part +remembers its options, so the next save reuses them; a document not yet saved as a native file is offered the +VectorCraft Options the last native Save or Save As used (not its artboards, nor Use Compression). A PDF opened with `pages`, `cropTo` or `password` is only part of the file, so Save asks for a new name instead of writing it back. `file.documentColorMode {mode, convert?, intent?, grays?}` switches the document color mode, converting the colors of the art (opacity masks included), symbols, graph designs, pattern tiles (with the tile that pattern editing's Cancel