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