From 0dcb52c7cdf2c52d56940b9c8bfde112bd514f83 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 20 Oct 2026 17:55:07 -0700 Subject: [PATCH] Export for Screens: takes the saved document's file name In a file name pattern, is documented as the document's name without its extension, but it expanded to the File Info title whenever the document had one, so a saved my-logo.vectorcraft titled "Logo: final version" exported as Logo--final-version__Artboard 1.png. The Full Document file and the .zip took the same title. Export for Screens and the dialog's Example line now take from the document's file, as Save names it (save.rs), and fall back to the title only for a document that has not been saved. --- crates/engine/src/cmd/fileio/screens.rs | 11 ++++--- crates/engine/src/cmd/fileio/tests_screens.rs | 33 +++++++++++++++++++ docs/mcp.md | 5 +-- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/cmd/fileio/screens.rs b/crates/engine/src/cmd/fileio/screens.rs index 1d9c0303c..aed45c0cd 100644 --- a/crates/engine/src/cmd/fileio/screens.rs +++ b/crates/engine/src/cmd/fileio/screens.rs @@ -382,7 +382,7 @@ struct Naming { } impl Naming { - /// The naming of params `o` for a document titled `title`. + /// The naming of params `o` for a document named `title`: its file's path, or its title before it is saved. fn of(o: &ScreenParams, title: &str) -> Result { let folders = Folders::of(o)?; // A prefix names files, not folders. @@ -669,7 +669,9 @@ pub(crate) fn export(s: &mut Session, p: &Value) -> Result { let o = ScreenParams::of(p)?; let rows = rows_of(s, p, o.preset.as_deref())?; o.one_source()?; - let naming = Naming::of(&o, &s.doc()?.doc.title)?; + // ``: the document's file, as Save names it, or its title before it is saved. + let st = s.doc()?; + let naming = Naming::of(&o, st.path.as_deref().unwrap_or(&st.doc.title))?; let selection = o.selection_mode()?; let selected = if selection.is_some() { selected_artwork(s)? } else { vec![] }; let doc = s.doc()?.doc.clone(); @@ -846,13 +848,14 @@ pub fn first_file(s: &Session, p: &Value) -> Result { let rows = rows_of(s, &p, o.preset.as_deref())?; o.one_source()?; let selection = o.selection_mode()?; - let doc = &s.doc()?.doc; + let st = s.doc()?; + let doc = &st.doc; let mut selected = if selection.is_some() { selected_artwork(s)? } else { vec![] }; // A file per object: objects that draw nothing write no file. if selection == Some(true) { selected = selected.into_iter().find(|id| export_crop(doc, &[*id]).is_some()).into_iter().collect(); } - let naming = Naming::of(&o, &doc.title)?; + let naming = Naming::of(&o, st.path.as_deref().unwrap_or(&doc.title))?; let items = items(doc, &o, selection, &selected, &naming.filename, false, false)?; let (Some(item), Some(r)) = (items.first(), rows.first()) else { return Err(bad(C, "nothing to export")) }; let (sub, file) = naming.file(item, 0, 0, r); diff --git a/crates/engine/src/cmd/fileio/tests_screens.rs b/crates/engine/src/cmd/fileio/tests_screens.rs index 867911681..5634c0da7 100644 --- a/crates/engine/src/cmd/fileio/tests_screens.rs +++ b/crates/engine/src/cmd/fileio/tests_screens.rs @@ -205,6 +205,39 @@ fn the_first_file_is_the_file_the_export_writes_first() { } } +/// A saved document's ``, Full Document file and ZIP are named after its file without the +/// extension, as Save names it, whatever its File Info title says. +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn a_saved_document_exports_under_its_file_name() { + let mut s = session(1); + s.execute("file.info", &json!({"title": "Logo: final version"})).unwrap(); + let dir = std::env::temp_dir().join(format!("vc-screens-filename-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + s.execute("document.save", &json!({"path": dir.join("my-logo.vectorcraft").to_string_lossy()})).unwrap(); + for (p, name) in [ + (json!({"name": "__", "formats": [{"format": "png"}]}), "my-logo__Artboard 1.png"), + (json!({"fullDocument": true, "formats": [{"format": "png"}]}), "my-logo.png"), + ] { + assert_eq!(names(&export(&mut s, p.clone())), [name], "{p}"); + assert_eq!(super::screens::first_file(&s, &p).unwrap(), name, "{p}"); + } + assert_eq!(export(&mut s, json!({"zip": true, "formats": [{"format": "svg"}]}))["name"], "my-logo.zip"); + let _ = std::fs::remove_dir_all(dir); +} + +/// A document that has not been saved has no file: `` is its title. +#[test] +fn an_unsaved_document_exports_under_its_title() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 40, "height": 30, "name": "Poster"})).unwrap(); + s.execute("shape.rectangle", &json!({"x": 5, "y": 5, "width": 20, "height": 20})).unwrap(); + s.execute("file.info", &json!({"title": "Spring poster"})).unwrap(); + let p = json!({"name": "", "formats": [{"format": "png"}]}); + assert_eq!(names(&export(&mut s, p.clone())), ["Spring-poster.png"]); + assert_eq!(super::screens::first_file(&s, &p).unwrap(), "Spring-poster.png"); +} + #[cfg(not(target_arch = "wasm32"))] #[test] fn sub_folders_are_made_in_the_export_folder() { diff --git a/docs/mcp.md b/docs/mcp.md index d7d6f6890..1b0dea3cc 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3628,8 +3628,9 @@ fallback font…), each note once; the export still writes or returns every file Batch exports (#1935): - **What:** `layers: [id…]` (each layer's art alone) or `selection: "one" | "each"` instead of artboards (`assets`, `fullDocument` as before). -- **Names:** `name` is a pattern for every file: `` (the document), `` (the artboard, asset, layer - or object), ``, `####` (a counter zero-padded to the run of `#`, from `counterStart`; `counterPer: "item"` +- **Names:** `name` is a pattern for every file: `` (the document's file name without its extension, or + its title before it is saved; the Full Document file and the `.zip` use the same name), `` (the artboard, + asset, layer or object), ``, `####` (a counter zero-padded to the run of `#`, from `counterStart`; `counterPer: "item"` or `"file"`), `YYYYMMDD`, `YYYY-MM-DD`, ``, `