From 74f5f6e58f8a1b482bbc648179dcac035d58b2b5 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 17 Oct 2026 15:16:24 -0700 Subject: [PATCH] Actions: Batch's Override Export sends Export for Screens files to the destination folder With Override Export on, action.batch gave each document.export, document.exportSelection and document.exportForScreens step a path in the destination folder. document.exportForScreens reads folder and has no path parameter, so its files still went to the step's own folder, or came back as data when the step named none. assets.export was not overridden at all. Override Export now gives a document.exportForScreens or assets.export step the folder destinationFolder/, in place of the step's own folder or an export preset's. Each source file gets its own folder, named after the file without its extension, and each data set gets one named _. The export names the files in that folder as it does outside Batch. Two data sets, or two source files whose names without the extension differ, no longer write over each other's files when their artboards or assets have the same names. Other export steps are unchanged. Docs: docs/mcp.md, docs/target-app-parity.md. --- crates/engine/src/cmd/fileio/action_batch.rs | 101 +++++++++++++++++-- crates/engine/src/cmd/fileio/mod.rs | 2 +- docs/mcp.md | 5 +- docs/target-app-parity.md | 1 + 4 files changed, 99 insertions(+), 10 deletions(-) diff --git a/crates/engine/src/cmd/fileio/action_batch.rs b/crates/engine/src/cmd/fileio/action_batch.rs index 7e757eb19..feebde275 100644 --- a/crates/engine/src/cmd/fileio/action_batch.rs +++ b/crates/engine/src/cmd/fileio/action_batch.rs @@ -248,7 +248,9 @@ fn write_result(s: &mut Session, path: &str, ext: &str, as_copy: bool) -> Result } /// Point Save / Export steps at `dest_folder/{stem}.{ext}` when overrides are on: `ext` is the -/// extension of the step's `format` ([`format_extension`]), else `default_ext`. +/// extension of the step's `format` ([`format_extension`]), else `default_ext`. Export for Screens +/// and Export Assets steps get `dest_folder/{stem}` as their `folder`, one folder for each source +/// file or data set, and name the files in it themselves. #[cfg(not(target_arch = "wasm32"))] fn rewrite_paths( steps: &[Value], @@ -267,18 +269,22 @@ fn rewrite_paths( let id = c.get("command").and_then(Value::as_str).unwrap_or(""); let mut c = c.clone(); let rewrite = (override_save && matches!(id, "document.save" | "file.save" | "file.saveAs")) - || (override_export && matches!(id, "document.export" | "document.exportForScreens" | "document.exportSelection")); + || (override_export && matches!(id, "document.export" | "document.exportForScreens" | "document.exportSelection" | "assets.export")); if !rewrite { return c; } let params = c.get("params").cloned().unwrap_or(json!({})); let mut params = params.as_object().cloned().unwrap_or_default(); - let ext = match params.get("format").and_then(Value::as_str).filter(|f| !f.is_empty()) { - Some(f) => format_extension(f), - None => default_ext.to_string(), - }; - let file = std::path::Path::new(folder).join(format!("{stem}.{ext}")); - params.insert("path".into(), json!(file.to_string_lossy())); + if matches!(id, "document.exportForScreens" | "assets.export") { + params.insert("folder".into(), json!(std::path::Path::new(folder).join(stem).to_string_lossy())); + } else { + let ext = match params.get("format").and_then(Value::as_str).filter(|f| !f.is_empty()) { + Some(f) => format_extension(f), + None => default_ext.to_string(), + }; + let file = std::path::Path::new(folder).join(format!("{stem}.{ext}")); + params.insert("path".into(), json!(file.to_string_lossy())); + } if let Some(o) = c.as_object_mut() { o.insert("params".into(), Value::Object(params)); } @@ -470,6 +476,85 @@ mod tests { assert!(file_names(&dest).is_empty(), "{r}"); } + /// Override Export writes an Export for Screens step's files into a folder for each source file + /// inside the destination folder, under the names the export gives them, whether the step names + /// a folder of its own, takes one from an export preset or names none. With Override Export off, + /// the files go to the step's folder. + #[test] + fn batch_override_export_sends_export_for_screens_files_to_the_destination_folder() { + let src = tmp("screens-src"); + write_svg(&src, "a.svg", "#f00"); + write_svg(&src, "b.svg", "#0f0"); + let own = tmp("screens-own"); + let svg = json!([{"format": "svg"}]); + let preset = json!({"name": "Own Folder", "settings": {"formats": svg, "folder": own.to_string_lossy()}}); + let steps = [json!({"folder": own.to_string_lossy(), "formats": svg}), json!({"exportPreset": "Own Folder"}), json!({"formats": svg})]; + for (i, params) in steps.into_iter().enumerate() { + let dest = tmp(&format!("screens-out-{i}")); + let mut s = Session::new(); + s.execute("document.exportPresets.save", &preset).unwrap(); + let r = s + .execute( + "action.batch", + &json!({ + "folder": src.to_string_lossy(), + "destinationFolder": dest.to_string_lossy(), + "commands": [{"command": "document.exportForScreens", "params": params}] + }), + ) + .unwrap(); + assert_eq!(r["failed"], 0, "{params}: {r}"); + assert_eq!(file_names(&dest), ["a", "b"], "{params}: {r}"); + for stem in ["a", "b"] { + assert_eq!(file_names(&dest.join(stem)), ["Artboard-1.svg"], "{params}: {r}"); + } + assert!(file_names(&own).is_empty(), "{params}: {r}"); + } + let dest = tmp("screens-out-off"); + let mut s = Session::new(); + let r = s + .execute( + "action.batch", + &json!({ + "folder": src.to_string_lossy(), + "destinationFolder": dest.to_string_lossy(), + "overrideExport": false, + "commands": [{"command": "document.exportForScreens", "params": {"folder": own.to_string_lossy(), "formats": svg}}] + }), + ) + .unwrap(); + assert_eq!(r["failed"], 0, "{r}"); + assert_eq!(file_names(&own), ["Artboard-1.svg"], "{r}"); + assert!(file_names(&dest).is_empty(), "{r}"); + } + + /// Override Export writes an Export Assets step's files into a folder for each data set inside + /// the destination folder, under the assets' names. + #[test] + fn batch_override_export_sends_asset_export_files_to_the_destination_folder() { + let own = tmp("assets-own"); + let dest = tmp("assets-out"); + let mut s = with_data_sets(&["a", "b"]); + let id = s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap()["id"].as_u64().unwrap(); + s.execute("assets.add", &json!({"ids": [id], "name": "Logo"})).unwrap(); + let r = s + .execute( + "action.batch", + &json!({ + "source": "datasets", + "destinationFolder": dest.to_string_lossy(), + "commands": [{"command": "assets.export", "params": {"folder": own.to_string_lossy(), "formats": [{"format": "svg"}]}}] + }), + ) + .unwrap(); + assert_eq!(r["failed"], 0, "{r}"); + assert_eq!(file_names(&dest), ["Untitled_a", "Untitled_b"], "{r}"); + for stem in ["Untitled_a", "Untitled_b"] { + assert_eq!(file_names(&dest.join(stem)), ["Logo.svg"], "{r}"); + } + assert!(file_names(&own).is_empty(), "{r}"); + } + /// Override Save names each file with the extension of the format the step names, also over /// data sets. #[test] diff --git a/crates/engine/src/cmd/fileio/mod.rs b/crates/engine/src/cmd/fileio/mod.rs index dd3f54e7e..224f8e754 100644 --- a/crates/engine/src/cmd/fileio/mod.rs +++ b/crates/engine/src/cmd/fileio/mod.rs @@ -187,7 +187,7 @@ pub fn specs() -> Vec { "Batch…", [], None, - "{commands: [{command, params}] (the action's steps), source?: folder (default) | datasets, folder? (required for folder: the source folder), includeSubdirs?: false, destination?: none (default: leave each file open) | saveAndClose (save over the source and close) | folder (save or export into destinationFolder and close), destinationFolder? (required when destination is folder; also the folder Override Save/Export write into), overrideOpen?: true (skip document.open / file.open steps: the batch opens each source file), overrideSave?: true (point document.save / file.save / file.saveAs at destinationFolder/{stem}.{ext}), overrideExport?: true (point document.export / document.exportForScreens / document.exportSelection the same way), stopOnError?: true, label?} play the commands on every readable file in folder (and its subfolders when includeSubdirs), or on every data set of the open document (source: datasets; File + Data Set Name for the folder destination and Override Save/Export, with `/`, `\\`, `:`, `<`, `>`, `\"`, `|`, `?`, `*` and control characters in the data set's name as `-`, and `-2`, `-3`… added to a name already used). Caps: 500 files or data sets, 8 folder levels, 256 steps. Batches cannot nest. → {processed: [{path|dataset, result|error}…], count, failed, stopped}", + "{commands: [{command, params}] (the action's steps), source?: folder (default) | datasets, folder? (required for folder: the source folder), includeSubdirs?: false, destination?: none (default: leave each file open) | saveAndClose (save over the source and close) | folder (save or export into destinationFolder and close), destinationFolder? (required when destination is folder; also the folder Override Save/Export write into), overrideOpen?: true (skip document.open / file.open steps: the batch opens each source file), overrideSave?: true (point document.save / file.save / file.saveAs at destinationFolder/{stem}.{ext}), overrideExport?: true (point document.export / document.exportSelection the same way, and give document.exportForScreens / assets.export the folder destinationFolder/{stem}, one for each source file or data set, in which the export names its files as it does outside Batch), stopOnError?: true, label?} play the commands on every readable file in folder (and its subfolders when includeSubdirs), or on every data set of the open document (source: datasets; File + Data Set Name for the folder destination and Override Save/Export, with `/`, `\\`, `:`, `<`, `>`, `\"`, `|`, `?`, `*` and control characters in the data set's name as `-`, and `-2`, `-3`… added to a name already used). Caps: 500 files or data sets, 8 folder levels, 256 steps. Batches cannot nest. → {processed: [{path|dataset, result|error}…], count, failed, stopped}", always, action_batch::action_batch ), diff --git a/docs/mcp.md b/docs/mcp.md index 265c55663..e782fd13f 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1154,7 +1154,10 @@ already walks Image Trace. Short agent entry points: Export end the file name of a step with `format` in that `format`, in lower case and without a leading dot, when it is one of the format's extensions (`.svg` and `SVG` give `.svg`, `.JPEG` gives `.jpeg`), and otherwise in the format's first extension (`png8` gives `.png`, `template` gives `.vctemplate`). A step without `format` keeps the - source file's extension (`.vectorcraft` over data sets). + source file's extension (`.vectorcraft` over data sets). Override Export gives a `document.exportForScreens` or + `assets.export` step the `folder` `destinationFolder/`, in place of the step's own folder or an export + preset's: one folder for each source file, named after the file without its extension, or for each data set, + named `_`. The export names the files in it as it does outside Batch. - Image Trace: `imageTrace.presets`, `imageTrace.make {id?, preset?, params?, view?}` (or `imageTrace.makeAndExpand`), `imageTrace.setView`, `imageTrace.expand`, `imageTrace.release`. - Live Paint: `livePaint.make`, `livePaint.merge`, `livePaint.fill {group?, point}`, `livePaint.strokeEdge`, diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 7b822f3f1..5b0689db7 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -430,6 +430,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Variables: `dataset.select`, `dataset.next` and `dataset.prev` name the area type and type on a path whose text they rewrote and that doesn't fit, in the reply's `overflow` key, as `text.setText` does. - Variables: Lock Variables (`variable.lock`, in the Variables panel's bottom bar and menu) stops variables from being defined, deleted or renamed until they are unlocked, while objects can still be bound and unbound and data sets still change and apply; the lock is saved with the document. - Batch…: Override Save and Override Export name each file with the extension of the format the step writes, however the step spells the format (`.svg` and `SVG` give `.svg`, `png8` gives `.png`), in `action.batch`. + - Batch…: Override Export writes the files of an Export for Screens or Export Assets step into a folder for each source file or data set inside the destination folder, under the names the export gives them (`action.batch`). - Actions panel (record/playback, persisted), generic parameter dialogs for every "…" command. - Batch… over data sets names each file after the document and the data set (`action.batch`). It writes `/`, `\`, `:`, `<`, `>`, `"`, `|`, `?`, `*` and control characters in the data set's name as `-`, and adds `-2`, `-3`… to a name an earlier data set used. - Export for Screens, Save for Web (slice images, and files named after the document's title) and the Libraries panel name files with the characters Windows does not allow written as `-` and with a `-` after a Windows device name such as `CON`, on every platform (`document.exportForScreens`, `document.exportForWeb`, `library.create`).