From 0fe70314b8ee86a6a2726127257d057f2e931781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabio=20William=20Concei=C3=A7=C3=A3o?= Date: Wed, 14 Oct 2026 12:42:07 +0100 Subject: [PATCH 1/3] =?UTF-8?q?G6:=20action.batch=20for=20File=20=E2=80=BA?= =?UTF-8?q?=20Automate=20=E2=80=BA=20Batch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Play an action on every file in a folder or every data set, with destination and Override Open/Save/Export. Actions panel and File › Automate › Batch… open the dialog; new strings in pt-br. Co-authored-by: Cursor --- crates/engine/src/cmd/fileio/action_batch.rs | 450 +++++++++++++++++++ crates/engine/src/cmd/fileio/mod.rs | 14 +- crates/ui-egui/src/dialogs/action_batch.rs | 125 ++++++ crates/ui-egui/src/dialogs/mod.rs | 2 + crates/ui-egui/src/i18n/pt-br.tsv | 13 + crates/ui-egui/src/menus.rs | 13 + crates/ui-egui/src/panels/actions.rs | 18 + crates/ui-egui/src/panels/mod.rs | 1 + docs/gaps.md | 4 + docs/mcp.md | 11 +- docs/target-app-parity.md | 2 + 11 files changed, 651 insertions(+), 2 deletions(-) create mode 100644 crates/engine/src/cmd/fileio/action_batch.rs create mode 100644 crates/ui-egui/src/dialogs/action_batch.rs diff --git a/crates/engine/src/cmd/fileio/action_batch.rs b/crates/engine/src/cmd/fileio/action_batch.rs new file mode 100644 index 000000000..7dfe44ed9 --- /dev/null +++ b/crates/engine/src/cmd/fileio/action_batch.rs @@ -0,0 +1,450 @@ +//! `action.batch`: play an action's commands on every file in a folder, or on every data set +//! of the open document (File › Automate › Batch / the Actions panel). +//! +//! Public options follow the Batch dialog: source Folder or Data Sets; destination None, +//! Save and Close, or Folder; Override Open / Save / Export. Cited: +//! + +use serde_json::{Value, json}; + +use super::super::*; + +#[cfg(target_arch = "wasm32")] +pub(super) fn action_batch(_: &mut Session, _: &Value) -> Result { + Err(bad("action.batch", "no file system here")) +} + +/// Caps on a batch (folders and MCP payloads are untrusted). +#[cfg(not(target_arch = "wasm32"))] +const MAX_BATCH_FILES: usize = 500; +#[cfg(not(target_arch = "wasm32"))] +const MAX_BATCH_DEPTH: usize = 8; + +/// Shared Batch options for one run. +#[cfg(not(target_arch = "wasm32"))] +struct Run<'a> { + label: &'a str, + steps: &'a [Value], + stop_on_error: bool, + override_save: bool, + override_export: bool, + destination: &'a str, + dest_folder: Option<&'a str>, +} + +/// Play `commands` on each source file or data set. +#[cfg(not(target_arch = "wasm32"))] +pub(super) fn action_batch(s: &mut Session, p: &Value) -> Result { + use super::{absolute_path, may_write}; + + const C: &str = "action.batch"; + let cmds = p.get("commands").and_then(Value::as_array).ok_or_else(|| bad(C, "missing `commands`"))?; + if cmds.is_empty() { + return Err(bad(C, "`commands` must not be empty")); + } + if cmds.len() > 256 { + return Err(bad(C, "more than 256 commands")); + } + for (i, c) in cmds.iter().enumerate() { + let id = c.get("command").and_then(Value::as_str).unwrap_or(""); + if id.is_empty() { + return Err(bad(C, format!("commands[{i}]: missing `command`"))); + } + if id == "command.batch" || id == "action.batch" { + return Err(bad(C, "batches cannot nest")); + } + if find_command(id).is_none() { + return Err(bad(C, format!("commands[{i}]: unknown command `{id}`"))); + } + } + let source = str_param(p, "source").unwrap_or("folder"); + let destination = str_param(p, "destination").unwrap_or("none"); + if !matches!(destination, "none" | "saveAndClose" | "folder") { + return Err(bad(C, "`destination` must be `none`, `saveAndClose` or `folder`")); + } + let dest_folder = str_param(p, "destinationFolder").map(absolute_path); + if destination == "folder" { + let Some(ref folder) = dest_folder else { + return Err(bad(C, "`destinationFolder` is required when destination is `folder`")); + }; + may_write(folder)?; + std::fs::create_dir_all(folder).map_err(|e| bad(C, format!("{folder}: {e}")))?; + } + let label = str_param(p, "label").unwrap_or("Batch").to_string(); + let override_open = bool_or(p, "overrideOpen", true); + let run = Run { + label: &label, + steps: cmds, + stop_on_error: bool_or(p, "stopOnError", true), + override_save: bool_or(p, "overrideSave", true), + override_export: bool_or(p, "overrideExport", true), + destination, + dest_folder: dest_folder.as_deref(), + }; + match source { + "folder" => batch_folder(s, C, p, override_open, &run), + "datasets" => batch_datasets(s, C, &run), + _ => Err(bad(C, "`source` must be `folder` or `datasets`")), + } +} + +#[cfg(not(target_arch = "wasm32"))] +fn batch_folder(s: &mut Session, cmd: &str, p: &Value, override_open: bool, run: &Run<'_>) -> Result { + use super::{absolute_path, may_read}; + + let folder = str_param(p, "folder").map(absolute_path).ok_or_else(|| bad(cmd, "missing `folder`"))?; + may_read(&folder)?; + let include_subdirs = bool_or(p, "includeSubdirs", false); + let files = collect_files(&folder, include_subdirs, 0)?; + if files.len() > MAX_BATCH_FILES { + return Err(bad(cmd, format!("more than {MAX_BATCH_FILES} files"))); + } + if files.is_empty() { + return Err(bad(cmd, "no readable files in the folder")); + } + let steps = filter_steps(run.steps, override_open, false); + let run = Run { steps: &steps, ..*run }; + let mut processed = Vec::new(); + let mut failed = 0usize; + for path in &files { + let stem = std::path::Path::new(path).file_stem().and_then(|s| s.to_str()).unwrap_or("file"); + let ext = super::extension(path); + match process_one_file(s, &run, path, stem, &ext) { + Ok(row) => processed.push(row), + Err(e) => { + failed += 1; + processed.push(json!({"path": path, "error": e.to_string()})); + if run.stop_on_error { + return Ok(json!({"processed": processed, "count": processed.len(), "failed": failed, "stopped": true})); + } + } + } + } + Ok(json!({"processed": processed, "count": processed.len(), "failed": failed, "stopped": false})) +} + +#[cfg(not(target_arch = "wasm32"))] +fn batch_datasets(s: &mut Session, cmd: &str, run: &Run<'_>) -> Result { + let st = s.doc()?; + let names: Vec = st.doc.variables.datasets.iter().map(|d| d.name.clone()).collect(); + if names.is_empty() { + return Err(bad(cmd, "no data sets")); + } + if names.len() > MAX_BATCH_FILES { + return Err(bad(cmd, format!("more than {MAX_BATCH_FILES} data sets"))); + } + let doc_stem = st.path.as_deref().and_then(|p| std::path::Path::new(p).file_stem().and_then(|s| s.to_str())).unwrap_or("Untitled").to_string(); + let steps = filter_steps(run.steps, true, false); + let run = Run { steps: &steps, ..*run }; + let mut processed = Vec::new(); + let mut failed = 0usize; + for name in &names { + match process_one_dataset(s, &run, name, &doc_stem) { + Ok(row) => processed.push(row), + Err(e) => { + failed += 1; + processed.push(json!({"dataset": name, "error": e.to_string()})); + if run.stop_on_error { + return Ok(json!({"processed": processed, "count": processed.len(), "failed": failed, "stopped": true})); + } + } + } + } + Ok(json!({"processed": processed, "count": processed.len(), "failed": failed, "stopped": false})) +} + +/// Drop Open (and optionally Close) steps when the batch supplies the file itself. +#[cfg(not(target_arch = "wasm32"))] +fn filter_steps(cmds: &[Value], drop_open: bool, drop_close: bool) -> Vec { + cmds.iter() + .filter(|c| { + let id = c.get("command").and_then(Value::as_str).unwrap_or(""); + if drop_open && matches!(id, "document.open" | "file.open") { + return false; + } + if drop_close && matches!(id, "file.close" | "document.close") { + return false; + } + true + }) + .cloned() + .collect() +} + +#[cfg(not(target_arch = "wasm32"))] +fn process_one_file(s: &mut Session, run: &Run<'_>, path: &str, stem: &str, ext: &str) -> Result { + s.execute("document.open", &json!({"path": path}))?; + let rewritten = rewrite_paths(run.steps, run.override_save, run.override_export, run.dest_folder, stem, ext); + let batch = s.execute("command.batch", &json!({"label": run.label, "commands": rewritten}))?; + // Folder sources open a file per pass: close after Save and Close / Folder. + finish_destination(s, run, stem, ext, path, true)?; + Ok(json!({"path": path, "result": batch})) +} + +#[cfg(not(target_arch = "wasm32"))] +fn process_one_dataset(s: &mut Session, run: &Run<'_>, name: &str, doc_stem: &str) -> Result { + s.execute("dataset.select", &json!({"name": name}))?; + // File + Data Set Name naming when overriding Save/Export (helpx). + let stem = format!("{doc_stem}_{name}"); + let rewritten = rewrite_paths(run.steps, run.override_save, run.override_export, run.dest_folder, &stem, "vectorcraft"); + let batch = s.execute("command.batch", &json!({"label": run.label, "commands": rewritten}))?; + // Data sets share one open document: write each row out but leave it open for the next. + finish_destination(s, run, &stem, "vectorcraft", "", false)?; + Ok(json!({"dataset": name, "result": batch})) +} + +#[cfg(not(target_arch = "wasm32"))] +fn finish_destination(s: &mut Session, run: &Run<'_>, stem: &str, ext: &str, source_path: &str, close: bool) -> Result<()> { + match run.destination { + "none" => Ok(()), + "saveAndClose" => { + if !source_path.is_empty() { + s.execute("document.save", &json!({"path": source_path}))?; + } else { + s.execute("document.save", &json!({}))?; + } + if close { + s.execute("file.close", &json!({}))?; + } + Ok(()) + } + "folder" => { + let folder = run.dest_folder.ok_or_else(|| bad("action.batch", "missing destination folder"))?; + let out_ext = if ext.is_empty() { "vectorcraft" } else { ext }; + let out = std::path::Path::new(folder).join(format!("{stem}.{out_ext}")); + let out = out.to_string_lossy().into_owned(); + // When `close` is false (data sets), write a copy so the template keeps its path. + write_result(s, &out, out_ext, !close)?; + if close { + s.execute("file.close", &json!({}))?; + } + Ok(()) + } + _ => Ok(()), + } +} + +/// Save when the extension is a Save format; otherwise export (PNG, JPEG…). +/// `as_copy`: `file.saveCopy` so the open document keeps its path (data-set batches). +#[cfg(not(target_arch = "wasm32"))] +fn write_result(s: &mut Session, path: &str, ext: &str, as_copy: bool) -> Result<()> { + use super::SAVE_FORMATS; + let id = if ext == "drawcraft" { "vectorcraft" } else { ext }; + if SAVE_FORMATS.contains(&id) { + let cmd = if as_copy { "file.saveCopy" } else { "document.save" }; + s.execute(cmd, &json!({"path": path}))?; + } else { + s.execute("document.export", &json!({"path": path, "format": id}))?; + } + Ok(()) +} + +/// Point Save / Export steps at `dest_folder/{stem}.{ext}` when overrides are on. +#[cfg(not(target_arch = "wasm32"))] +fn rewrite_paths( + steps: &[Value], + override_save: bool, + override_export: bool, + dest_folder: Option<&str>, + stem: &str, + default_ext: &str, +) -> Vec { + let Some(folder) = dest_folder else { + return steps.to_vec(); + }; + steps + .iter() + .map(|c| { + 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")); + 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 = params.get("format").and_then(Value::as_str).unwrap_or(default_ext); + let ext = if ext.is_empty() { default_ext } else { ext }; + 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)); + } + c + }) + .collect() +} + +/// Readable files under `folder` (and optionally its subfolders), capped. +#[cfg(not(target_arch = "wasm32"))] +fn collect_files(folder: &str, include_subdirs: bool, depth: usize) -> Result> { + use std::path::PathBuf; + + use super::list_dir; + + if depth > MAX_BATCH_DEPTH { + return Err(bad("action.batch", format!("folders deeper than {MAX_BATCH_DEPTH} levels"))); + } + let (entries, _) = list_dir(folder, MAX_BATCH_FILES.saturating_mul(2))?; + let mut files = Vec::new(); + let mut subdirs = Vec::new(); + for e in entries { + let name = e.get("name").and_then(Value::as_str).unwrap_or(""); + if name.is_empty() || name.starts_with('.') { + continue; + } + let kind = e.get("kind").and_then(Value::as_str).unwrap_or(""); + let path = PathBuf::from(folder).join(name); + let path = path.to_string_lossy().into_owned(); + match kind { + "file" if is_openable(&path) => files.push(path), + "folder" if include_subdirs => subdirs.push(path), + _ => {} + } + } + files.sort(); + for sub in subdirs { + let mut nested = collect_files(&sub, true, depth + 1)?; + files.append(&mut nested); + if files.len() > MAX_BATCH_FILES { + break; + } + } + Ok(files) +} + +#[cfg(not(target_arch = "wasm32"))] +fn is_openable(path: &str) -> bool { + use super::{FORMATS, OPEN_EXTS, extension}; + let ext = extension(path); + if ext.is_empty() { + return false; + } + OPEN_EXTS.iter().any(|e| *e == ext) || FORMATS.iter().any(|f| f.read && f.extensions.iter().any(|e| *e == ext)) +} + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use super::*; + use serde_json::json; + + fn tmp(tag: &str) -> std::path::PathBuf { + let d = std::env::temp_dir().join(format!("vc-action-batch-{tag}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&d); + std::fs::create_dir_all(&d).unwrap(); + d + } + + fn write_svg(dir: &std::path::Path, name: &str, fill: &str) -> std::path::PathBuf { + let p = dir.join(name); + std::fs::write( + &p, + format!( + r#" + "# + ), + ) + .unwrap(); + p + } + + #[test] + fn batch_folder_runs_action_and_saves_to_destination() { + let src = tmp("src"); + let dest = tmp("dest"); + write_svg(&src, "a.svg", "#f00"); + write_svg(&src, "b.svg", "#0f0"); + let mut s = Session::new(); + // Need a document so command.batch's has_doc passes after the first open leaves one. + s.execute("file.new", &json!({"width": 100, "height": 80})).unwrap(); + let r = s + .execute( + "action.batch", + &json!({ + "folder": src.to_string_lossy(), + "destination": "folder", + "destinationFolder": dest.to_string_lossy(), + "commands": [{"command": "object.unlockAll", "params": {}}], + "label": "Unlock" + }), + ) + .unwrap(); + assert_eq!(r["count"], 2); + assert_eq!(r["failed"], 0); + assert!(dest.join("a.svg").is_file(), "a.svg written"); + assert!(dest.join("b.svg").is_file(), "b.svg written"); + } + + #[test] + fn batch_skips_open_steps_when_override_open() { + let src = tmp("open"); + let dest = tmp("open-out"); + write_svg(&src, "only.svg", "#00f"); + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 100, "height": 80})).unwrap(); + let r = s + .execute( + "action.batch", + &json!({ + "folder": src.to_string_lossy(), + "destination": "folder", + "destinationFolder": dest.to_string_lossy(), + "overrideOpen": true, + "commands": [ + {"command": "document.open", "params": {"path": "/no/such/file.svg"}}, + {"command": "object.unlockAll", "params": {}} + ] + }), + ) + .unwrap(); + assert_eq!(r["failed"], 0, "{r}"); + assert!(dest.join("only.svg").is_file()); + } + + #[test] + fn batch_datasets_applies_each_row() { + let dest = tmp("sets"); + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 200, "height": 100})).unwrap(); + let id = s.execute("text.create", &json!({"x": 10, "y": 40, "text": "A"})).unwrap()["id"].as_u64().unwrap(); + s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap(); + s.execute("variable.bind", &json!({"variable": "Name", "ids": [id]})).unwrap(); + s.execute("dataset.capture", &json!({})).unwrap(); + s.execute("text.setText", &json!({"ids": [id], "text": "B"})).unwrap(); + s.execute("dataset.capture", &json!({})).unwrap(); + let r = s + .execute( + "action.batch", + &json!({ + "source": "datasets", + "destination": "folder", + "destinationFolder": dest.to_string_lossy(), + "commands": [{"command": "object.unlockAll", "params": {}}], + "label": "Per set" + }), + ) + .unwrap(); + assert_eq!(r["count"], 2, "{r}"); + assert_eq!(r["failed"], 0, "{r}"); + let written: Vec<_> = std::fs::read_dir(&dest).unwrap().filter_map(|e| e.ok()).map(|e| e.file_name()).collect(); + assert_eq!(written.len(), 2, "{written:?}"); + } + + #[test] + fn batch_refuses_nesting_and_empty_commands() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 100, "height": 80})).unwrap(); + assert!(s.execute("action.batch", &json!({"commands": []})).is_err()); + assert!( + s.execute( + "action.batch", + &json!({ + "folder": ".", + "commands": [{"command": "command.batch", "params": {"commands": []}}] + }) + ) + .is_err() + ); + } +} diff --git a/crates/engine/src/cmd/fileio/mod.rs b/crates/engine/src/cmd/fileio/mod.rs index ca6a0a5b9..205b11cfb 100644 --- a/crates/engine/src/cmd/fileio/mod.rs +++ b/crates/engine/src/cmd/fileio/mod.rs @@ -1,7 +1,9 @@ //! Document file I/O for every frontend (desktop, web, CLI, control channel, headless MCP): open, -//! save, export and serialize by path or bytes (base64), the format table, and `command.batch`. +//! save, export and serialize by path or bytes (base64), the format table, `command.batch` and +//! `action.batch` (File › Automate › Batch / the Actions panel). //! //! - `load`: `document.open` (native, legacy, SVG/SVGZ, PDF/.ai/.ait, DXF, raster images). +//! - `action_batch`: play an action on every file in a folder or every data set. //! - `encode`: one encoder per writable format, with typed options parsed from the params. //! - `svg`: the SVG Options (styling, fonts, images, object ids, artboards…). //! - `export`: `document.export` / `serialize` / `exportSelection` / `exportForOffice`. @@ -19,6 +21,7 @@ //! [`FORMATS`] is the single list of formats (append-only); open dialogs use [`open_filters`], //! agents query `document.formats`. +mod action_batch; mod affinity; mod batch; pub mod dxf; @@ -146,6 +149,15 @@ pub fn specs() -> Vec { has_doc, batch::batch ), + cmd!( + "action.batch", + "Batch…", + ["File", "Automate"], + 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 Override Save/Export). 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 + ), cmd!( query "document.formats", "File Formats", diff --git a/crates/ui-egui/src/dialogs/action_batch.rs b/crates/ui-egui/src/dialogs/action_batch.rs new file mode 100644 index 000000000..b3030b0ef --- /dev/null +++ b/crates/ui-egui/src/dialogs/action_batch.rs @@ -0,0 +1,125 @@ +//! Actions panel › Batch… / File › Automate › Batch: play the selected action on every file in a +//! folder, or on every data set of the open document (`action.batch`). + +use serde_json::{Value, json}; + +use super::{DialogSpec, form}; +use crate::state::Dialog; +use crate::theme::Tokens; +use crate::{VectorcraftApp, widgets}; + +pub(super) const KIND: &str = "actionBatch"; +const LABEL: f32 = 130.0; + +pub(super) const SPEC: DialogSpec = + DialogSpec { heading: |_| tl!("Batch").into(), body, confirm, ok: Some("OK"), min_width: 420.0, max_width: Some(460.0), ..DialogSpec::FORM }; + +/// Open the Batch dialog for the selected action (`set`, `action` indexes into `action_sets`). +pub fn open(app: &mut VectorcraftApp, set: usize, action: usize) -> Result { + let a = app.ui.action_sets.get(set).and_then(|s| s.actions.get(action)).ok_or("no such action")?; + if a.steps.is_empty() { + return Err("the action has no steps".into()); + } + let name = a.name.clone(); + app.ui.dialog = Some(Dialog::new( + KIND, + json!({ + "set": set, + "action": action, + "actionName": name, + "source": "folder", + "folder": "", + "includeSubdirs": false, + "destination": "folder", + "destinationFolder": "", + "overrideOpen": true, + "overrideSave": true, + "overrideExport": true, + "stopOnError": true, + }), + )); + Ok(Value::Null) +} + +fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { + let t = Tokens::get(ui.ctx()); + ui.label(egui::RichText::new(d.str("actionName")).color(t.text_strong)); + ui.add_space(8.0); + + form::choice(ui, d, "source", "Play:", (LABEL, 200.0), &[("folder", "Folder"), ("datasets", "Data Sets")]); + let source = d.str("source"); + if source == "folder" { + widgets::label_row(ui, tl!("Source Folder:"), LABEL, |ui| { + form::text(ui, d, "folder", 200.0); + if app.services.pick_folder.is_some() && ui.button(tl!("Choose…")).clicked() { + crate::picks::folder_field(app, d, "folder"); + } + }); + form::check(ui, d, "includeSubdirs", "Include All Subdirectories"); + } else { + ui.label(egui::RichText::new(tl!("Uses every data set of the open document.")).size(11.5).color(t.text_dim)); + } + ui.add_space(8.0); + + form::choice(ui, d, "destination", "Destination:", (LABEL, 200.0), &[("none", "None"), ("saveAndClose", "Save and Close"), ("folder", "Folder")]); + if d.str("destination") == "folder" { + widgets::label_row(ui, tl!("Destination Folder:"), LABEL, |ui| { + form::text(ui, d, "destinationFolder", 200.0); + if app.services.pick_folder.is_some() && ui.button(tl!("Choose…")).clicked() { + crate::picks::folder_field(app, d, "destinationFolder"); + } + }); + } + ui.add_space(6.0); + widgets::subheader(ui, tl!("Overrides")); + form::check(ui, d, "overrideOpen", "Override Action \"Open\" Commands"); + form::check(ui, d, "overrideSave", "Override Action \"Save\" Commands"); + form::check(ui, d, "overrideExport", "Override Action \"Export\" Commands"); + form::check(ui, d, "stopOnError", "Stop on Errors"); + false +} + +fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { + let set = d.fields.get("set").and_then(Value::as_u64).unwrap_or(0) as usize; + let action = d.fields.get("action").and_then(Value::as_u64).unwrap_or(0) as usize; + let a = app.ui.action_sets.get(set).and_then(|s| s.actions.get(action)).ok_or("no such action")?.clone(); + let commands: Vec = a.steps.iter().map(|(c, p)| json!({"command": c, "params": p})).collect(); + let source = d.str("source"); + if source == "folder" && d.str("folder").trim().is_empty() { + return Err("choose a source folder".into()); + } + let destination = d.str("destination"); + if destination == "folder" && d.str("destinationFolder").trim().is_empty() { + return Err("choose a destination folder".into()); + } + if source == "datasets" && app.session.active().is_none() { + return Err("open a document with data sets".into()); + } + let mut params = json!({ + "commands": commands, + "label": a.name, + "source": source, + "destination": destination, + "overrideOpen": d.bool("overrideOpen"), + "overrideSave": d.bool("overrideSave"), + "overrideExport": d.bool("overrideExport"), + "stopOnError": d.bool("stopOnError"), + "includeSubdirs": d.bool("includeSubdirs"), + }); + if source == "folder" { + params["folder"] = json!(d.str("folder")); + } + if destination == "folder" { + params["destinationFolder"] = json!(d.str("destinationFolder")); + } + let r = app.run("action.batch", params)?; + app.ui.dialog = None; + let count = r["count"].as_u64().unwrap_or(0); + let failed = r["failed"].as_u64().unwrap_or(0); + if failed > 0 { + app.status(format!("Batch: {count} processed, {failed} failed")); + } else { + app.status(format!("Batch: {count} processed")); + } + Ok(r) +} diff --git a/crates/ui-egui/src/dialogs/mod.rs b/crates/ui-egui/src/dialogs/mod.rs index c077a3423..27e16e45a 100644 --- a/crates/ui-egui/src/dialogs/mod.rs +++ b/crates/ui-egui/src/dialogs/mod.rs @@ -7,6 +7,7 @@ //! Shortcuts, Workspaces, Find Font) draw their own window. mod about; +pub mod action_batch; pub mod artboard_options; pub mod blend_options; mod brush_art_options; @@ -283,6 +284,7 @@ registry! { DxfOptions: [dxf_options::KIND] => dxf_options::SPEC, PlacementOptions: [placement_options::KIND] => placement_options::SPEC, Package: [package::KIND] => package::SPEC, + ActionBatch: [action_batch::KIND] => action_batch::SPEC, SliceOptions: [slices::OPTIONS] => slices::OPTIONS_SPEC, DivideSlices: [slices::DIVIDE] => slices::DIVIDE_SPEC, EpsOptions: [eps_options::KIND] => eps_options::SPEC, diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index f33beaf0c..4bc55b099 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -3802,6 +3802,19 @@ axis Both Ambos Softness Suavidade Ambient Light Luz ambiente Light behind the object Luz atrás do objeto + Automate Automatizar + Batch… Lote… + Source Folder: Pasta de origem: + Destination Folder: Pasta de destino: + Include All Subdirectories Incluir todos os subdiretórios + Uses every data set of the open document. Usa todos os conjuntos de dados do documento aberto. + Overrides Substituições + Override Action "Open" Commands Substituir comandos "Abrir" da ação + Override Action "Save" Commands Substituir comandos "Salvar" da ação + Override Action "Export" Commands Substituir comandos "Exportar" da ação + Stop on Errors Parar em erros + Save and Close Salvar e fechar + Play: Reproduzir: Make Graph Dynamic Tornar gráfico dinâmico Registration: Registro: # Dimension tool (linked dimension annotations) diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 51d4683c6..6e7037265 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -737,6 +737,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "Cmd+Alt+Shift+P", "{} open Package for the saved document (dialog `package`: folder, name, copyLinks, linksFolder, relink, copyFonts, report; a document never saved asks to Save As first); OK saves unsaved changes, runs file.package (the web downloads the zip) and offers file.showPackage", ), + ( + "ui.batchDialog", + "Batch…", + "", + "{set?: index, action?: index} open Batch (dialog `actionBatch`) for the action at set/action (default: the first action of the first set); OK runs action.batch with the dialog's source, destination and overrides", + ), ("file.showPackage", "Show Package", "", "{folder} show a package folder (file.package's folder) in the file manager → {folder}"), ( "docInfo.save", @@ -1495,6 +1501,11 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option crate::dialogs::package::open(app), + "ui.batchDialog" => { + let set = p.get("set").and_then(Value::as_u64).unwrap_or(0) as usize; + let action = p.get("action").and_then(Value::as_u64).unwrap_or(0) as usize; + crate::dialogs::action_batch::open(app, set, action) + } "ui.shortcutImportDialog" => crate::dialogs::shortcut_import::command(app, p), "file.showPackage" => crate::dialogs::package::show_package(app, p), "docInfo.save" => crate::panels::doc_info::save_report(app, p), @@ -2107,6 +2118,7 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool { "ui.placementOptionsDialog" => selected_image(app, |_| true) || selected_placed(app), "ui.findLinksInFolder" => crate::picks::can(app, &crate::picks::PickRequest::Folder) && app.session.active().is_some(), "ui.packageDialog" | "docInfo.save" => app.session.active().is_some(), + "ui.batchDialog" => !app.ui.action_sets.is_empty() && app.ui.action_sets.iter().any(|s| !s.actions.is_empty()), "ui.epsOptionsDialog" => app.session.active().is_some(), "file.saveForWeb" | "file.saveForWeb.browser" => app.session.active().is_some(), "file.exportSelection" => app.session.active().is_some_and(|d| !d.selection.is_empty()), @@ -2195,6 +2207,7 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec)> { c("Export Selection…", "file.exportSelection"), Sep, c("Package…", "ui.packageDialog"), + sub("Automate", vec![c("Batch…", "ui.batchDialog"), todos("Scripts…", "")]), sub("Scripts", vec![c("Check Script…", "ui.checkScript"), todos("Other Script…", "Cmd+F12")]), Sep, c("Document Setup…", "file.documentSetup"), diff --git a/crates/ui-egui/src/panels/actions.rs b/crates/ui-egui/src/panels/actions.rs index d8446570a..5eb641217 100644 --- a/crates/ui-egui/src/panels/actions.rs +++ b/crates/ui-egui/src/panels/actions.rs @@ -51,6 +51,24 @@ pub fn play(app: &mut VectorcraftApp, set: usize, idx: usize) -> Result = pstate(ui.ctx(), "actions-selected"); + let has = selected.and_then(|(s, a)| app.ui.action_sets.get(s).and_then(|set| set.actions.get(a))).is_some(); + if widgets::menu_item(ui, tl!("Batch…"), has, false) + && let Some((s, a)) = selected + && let Err(e) = crate::dialogs::action_batch::open(app, s, a) + { + app.status(e); + } + if widgets::menu_item(ui, tl!("Play Current Selection"), has, false) + && let Some((s, a)) = selected + && let Err(e) = play(app, s, a) + { + app.status(e); + } +} + pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); // The selected action as (set, action). Read as the type it is stored as: egui keeps a diff --git a/crates/ui-egui/src/panels/mod.rs b/crates/ui-egui/src/panels/mod.rs index 37b9e486c..8f36f231d 100644 --- a/crates/ui-egui/src/panels/mod.rs +++ b/crates/ui-egui/src/panels/mod.rs @@ -222,6 +222,7 @@ pub fn panel_menu_items(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) -> bool asset_export::ID => asset_export::menu(app, ui), css_properties::ID => css_properties::menu(app, ui), variables::ID => variables::menu(app, ui), + "actions" => actions::menu(app, ui), _ => return false, } true diff --git a/docs/gaps.md b/docs/gaps.md index d5dcb2e96..3c90413d0 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -265,6 +265,10 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall ## G6 Automation and scripting +- **Missing:** a scripting surface over the command registry (Illustrator: JavaScript), image and graph + variable kinds (dataset CSV import/export and Batch are open as PRs). Actions, Variables (text and + visibility) and full agent control over MCP are done. +- **Estimate:** 6–12 h. - **Missing:** a scripting surface over the command registry (Illustrator: JavaScript), Batch, image variable kind and dataset import/export (graph variables land separately). Actions, Variables (text, visibility, graph) and full agent control over MCP are done. - **Estimate:** 8–14 h. diff --git a/docs/mcp.md b/docs/mcp.md index 83f431f1d..e54dbb884 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -986,6 +986,12 @@ already walks Image Trace. Short agent entry points: `symbol.replace`, `symbol.edit`, `symbol.duplicate`, `symbol.selectInstances {name}`. - Brushes: `brush.list`, `brush.get {name}`, `brush.apply {name, ids?}`, `brush.new {type, name?, params?, ids?}`, `brush.setCurrent {name}`, `brush.options {name, params?}`, `brush.freehand`, `object.expandBrush {ids?}`. +- Variables: `variable.list`, `variable.define {name, kind: text|visibility}`, `variable.bind {variable, ids?}`, + `variable.makeTextDynamic`, `variable.makeVisibilityDynamic`, `variable.unbind`, `dataset.new` / `dataset.set` / + `dataset.select`. +- Batch: `action.batch {commands, source?: folder|datasets, folder?, destination?: none|saveAndClose|folder, + destinationFolder?, overrideOpen?, overrideSave?, overrideExport?, includeSubdirs?, stopOnError?}` plays an + action on every file in a folder or every data set (File › Automate › Batch…). - Variables: `variable.list`, `variable.define {name, kind: text|visibility|graph}`, `variable.bind {variable, ids?}`, `variable.makeTextDynamic`, `variable.makeVisibilityDynamic`, `variable.makeGraphDynamic`, `variable.unbind`, `dataset.new` / `dataset.set` / `dataset.select` (graph values are CSV as `graph.setData`). @@ -1795,7 +1801,10 @@ for `edit.pasteInPlace`, `edit.pasteInFront` and `edit.pasteInBack`, and to each none, when the step runs. When the document a step runs in has no artboard at that index (for example, after an earlier step deleted the active artboard when it was the last one), the step gets that document's last artboard. In remote mode, MCP's `run_command` calls and each step of the `command_batch` tool get the active artboard too. The -Actions panel plays an action as a `command.batch`. An action recorded in the Actions panel leaves the artboard out, +Actions panel plays an action as a `command.batch`. File › Automate › Batch… and the Actions panel's Batch… run +`action.batch`: the same steps on every readable file in a folder (or every data set of the open document), with +destination `none` | `saveAndClose` | `folder` and Override Open / Save / Export. Caps: 500 files or data sets, 8 +folder levels, 256 steps. An action recorded in the Actions panel leaves the artboard out, and its steps of these commands get the artboard that is active when the action plays. A press with the Selection, Direct Selection or Group Selection tool inside an artboard makes that artboard the active one; with the Selection tool, a Shift-click on an empty spot inside an artboard makes it active and keeps the selection. Choosing the Artboard diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index e7abe1783..10da19937 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -89,6 +89,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de | Guides, grids, smart guides, snapping, rulers | 3 | 80% | 30.0's tangent and perpendicular snapping and its snapping panel, "intersect" smart guides for a moved selection's anchors (drawn points and dragged anchors snap to intersections, #1658), global/artboard ruler mode switch and video rulers, distance readouts while drawing | 4–7 | estimated (lowered from 85%: target moved); new ruler-guide Alt orientation implemented | | Export for Screens, Asset Export, slices, Save for Web | 3 | 85% | anti-aliasing ghost borders (#983); TIFF choices and settings are implemented | 2–4 | estimated | | Print, colour management, separations, flattener | 3 | 80% | print preview fidelity | 2–4 | estimated | +| Automation (Actions, Variables, scripting, batch) | 3 | 62% | a scripting surface (Illustrator: JavaScript/ExtendScript), image and graph variables (Batch and dataset CSV open as PRs) | 6–12 | estimated | | Automation (Actions, Variables, scripting, batch) | 3 | 68% | a scripting surface (Illustrator: JavaScript/ExtendScript); Batch, image variables and dataset CSV open as PRs; graph variables done | 5–10 | estimated | | Automation (Actions, Variables, scripting, batch) | 3 | 65% | a scripting surface (Illustrator: JavaScript/ExtendScript), Batch, graph variables | 4–8 | estimated | | Automation (Actions, Variables, scripting, batch) | 3 | 62% | a scripting surface (Illustrator: JavaScript/ExtendScript), Batch, graph variables, dataset import/export | 6–12 | estimated | @@ -338,6 +339,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale) | File formats | 6 | 85% | Affinity export, and the rest of Affinity import (effects, adjustments, brushes, master pages, live symbol instances), DWG (no open spec: DXF instead), PSD placement as layers (Photoshop Import Options; the merged image is done), EPS checked against more real files users send (Inkscape files open as vectors; other generators' constructs are tested with programs written from the PostScript Language Reference only; CID-keyed CJK type and `CCITTFaxDecode` images not read), legacy `.ai` files saved without the prolog that defines their operators (their own operator set isn't read yet), the rest of Illustrator's editing data (appearances and live effects, symbols, pattern fills, brushes, placed files: #637), the remaining fidelity polish; EPS, DXF, EMF/WMF, TIFF, BMP, Targa, PSD export, SVGZ, PDF security, PDF/X, PDF layers, presets and Affinity import are done | 6–10 (existing scope; Affinity export unestimated) | | Export for Screens, Asset Export, slices, Save for Web | 3 | 85% | polish only (all four are done) | 2–4 | | Print, colour management, separations, flattener | 3 | 80% | print preview fidelity (flattener presets in PDF and print, overprint in PDF and composite print, Print dialog, presets, PostScript, marks, separations, Print Tiling, print overprint/flattener/bitmap options and printer profile are done) | 1–2 | +| Automation | 3 | 68% | scripting surface; image/graph variable kinds (Variables: text/visibility done; Batch `action.batch` and dataset CSV open as PRs) | 5–9 | | Automation | 3 | 70% | scripting surface, batch, image variables and dataset CSV (open as PRs); Variables: text, visibility and graph (CSV) bound to objects, dataset rows applied in one undo step | 4–8 | | Automation | 3 | 72% | scripting surface, batch (Variables: text/visibility/image variables bound to objects, an object may carry several, dataset rows applied in one undo step, CSV import/export, panel and Window › Variables with dialogs; graph kinds open) | 4–7 | | Automation | 3 | 68% | scripting surface, batch (Variables: text/visibility/image variables bound to objects, an object may carry several, dataset rows applied in one undo step, panel and Window › Variables with dialogs; graph kinds and dataset import/export open) | 5–9 | From 08dde8783ecce777451bd322d33164cce85f84fe Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 16 Oct 2026 04:36:11 +0700 Subject: [PATCH 2/3] =?UTF-8?q?Actions:=20Batch=E2=80=A6=20stays=20in=20th?= =?UTF-8?q?e=20Actions=20panel=20menu,=20as=20the=20reference=20app=20has?= =?UTF-8?q?=20it=20(#1938)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/engine/src/cmd/fileio/action_batch.rs | 5 +++-- crates/engine/src/cmd/fileio/mod.rs | 4 ++-- crates/ui-egui/src/dialogs/action_batch.rs | 2 +- crates/ui-egui/src/menus.rs | 1 - docs/mcp.md | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/engine/src/cmd/fileio/action_batch.rs b/crates/engine/src/cmd/fileio/action_batch.rs index 7dfe44ed9..ccc486896 100644 --- a/crates/engine/src/cmd/fileio/action_batch.rs +++ b/crates/engine/src/cmd/fileio/action_batch.rs @@ -1,5 +1,5 @@ //! `action.batch`: play an action's commands on every file in a folder, or on every data set -//! of the open document (File › Automate › Batch / the Actions panel). +//! of the open document (the Actions panel menu's Batch…). //! //! Public options follow the Batch dialog: source Folder or Data Sets; destination None, //! Save and Close, or Folder; Override Open / Save / Export. Cited: @@ -324,7 +324,8 @@ fn is_openable(path: &str) -> bool { OPEN_EXTS.iter().any(|e| *e == ext) || FORMATS.iter().any(|f| f.read && f.extensions.iter().any(|e| *e == ext)) } -#[cfg(all(test, not(target_arch = "wasm32")))] +#[cfg(test)] +#[cfg(not(target_arch = "wasm32"))] mod tests { use super::*; use serde_json::json; diff --git a/crates/engine/src/cmd/fileio/mod.rs b/crates/engine/src/cmd/fileio/mod.rs index 205b11cfb..f2e14c897 100644 --- a/crates/engine/src/cmd/fileio/mod.rs +++ b/crates/engine/src/cmd/fileio/mod.rs @@ -1,6 +1,6 @@ //! Document file I/O for every frontend (desktop, web, CLI, control channel, headless MCP): open, //! save, export and serialize by path or bytes (base64), the format table, `command.batch` and -//! `action.batch` (File › Automate › Batch / the Actions panel). +//! `action.batch` (the Actions panel menu's Batch…). //! //! - `load`: `document.open` (native, legacy, SVG/SVGZ, PDF/.ai/.ait, DXF, raster images). //! - `action_batch`: play an action on every file in a folder or every data set. @@ -152,7 +152,7 @@ pub fn specs() -> Vec { cmd!( "action.batch", "Batch…", - ["File", "Automate"], + [], 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 Override Save/Export). Caps: 500 files or data sets, 8 folder levels, 256 steps. Batches cannot nest. → {processed: [{path|dataset, result|error}…], count, failed, stopped}", always, diff --git a/crates/ui-egui/src/dialogs/action_batch.rs b/crates/ui-egui/src/dialogs/action_batch.rs index b3030b0ef..01c27b98a 100644 --- a/crates/ui-egui/src/dialogs/action_batch.rs +++ b/crates/ui-egui/src/dialogs/action_batch.rs @@ -1,4 +1,4 @@ -//! Actions panel › Batch… / File › Automate › Batch: play the selected action on every file in a +//! Actions panel menu › Batch…: play the selected action on every file in a //! folder, or on every data set of the open document (`action.batch`). use serde_json::{Value, json}; diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 6e7037265..ae1a6668c 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -2207,7 +2207,6 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec)> { c("Export Selection…", "file.exportSelection"), Sep, c("Package…", "ui.packageDialog"), - sub("Automate", vec![c("Batch…", "ui.batchDialog"), todos("Scripts…", "")]), sub("Scripts", vec![c("Check Script…", "ui.checkScript"), todos("Other Script…", "Cmd+F12")]), Sep, c("Document Setup…", "file.documentSetup"), diff --git a/docs/mcp.md b/docs/mcp.md index e54dbb884..3e164db9c 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -991,7 +991,7 @@ already walks Image Trace. Short agent entry points: `dataset.select`. - Batch: `action.batch {commands, source?: folder|datasets, folder?, destination?: none|saveAndClose|folder, destinationFolder?, overrideOpen?, overrideSave?, overrideExport?, includeSubdirs?, stopOnError?}` plays an - action on every file in a folder or every data set (File › Automate › Batch…). + action on every file in a folder or every data set (the Actions panel menu's Batch…). - Variables: `variable.list`, `variable.define {name, kind: text|visibility|graph}`, `variable.bind {variable, ids?}`, `variable.makeTextDynamic`, `variable.makeVisibilityDynamic`, `variable.makeGraphDynamic`, `variable.unbind`, `dataset.new` / `dataset.set` / `dataset.select` (graph values are CSV as `graph.setData`). @@ -1801,7 +1801,7 @@ for `edit.pasteInPlace`, `edit.pasteInFront` and `edit.pasteInBack`, and to each none, when the step runs. When the document a step runs in has no artboard at that index (for example, after an earlier step deleted the active artboard when it was the last one), the step gets that document's last artboard. In remote mode, MCP's `run_command` calls and each step of the `command_batch` tool get the active artboard too. The -Actions panel plays an action as a `command.batch`. File › Automate › Batch… and the Actions panel's Batch… run +Actions panel plays an action as a `command.batch`. The Actions panel menu's Batch… runs `action.batch`: the same steps on every readable file in a folder (or every data set of the open document), with destination `none` | `saveAndClose` | `folder` and Override Open / Save / Export. Caps: 500 files or data sets, 8 folder levels, 256 steps. An action recorded in the Actions panel leaves the artboard out, From 8412b4658f43956cb1db2f961f3004381adb13c8 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 16 Oct 2026 04:37:15 +0700 Subject: [PATCH 3/3] i18n: catalog rows for the new strings (#1938) --- crates/ui-egui/src/i18n/cs.tsv | 11 +++++++++++ crates/ui-egui/src/i18n/de.tsv | 11 +++++++++++ crates/ui-egui/src/i18n/es.tsv | 11 +++++++++++ crates/ui-egui/src/i18n/fr.tsv | 11 +++++++++++ crates/ui-egui/src/i18n/hu.tsv | 11 +++++++++++ crates/ui-egui/src/i18n/it.tsv | 11 +++++++++++ crates/ui-egui/src/i18n/ja.tsv | 11 +++++++++++ crates/ui-egui/src/i18n/ko.tsv | 11 +++++++++++ crates/ui-egui/src/i18n/pl.tsv | 11 +++++++++++ crates/ui-egui/src/i18n/pt-br.tsv | 6 ++++++ crates/ui-egui/src/i18n/ru.tsv | 11 +++++++++++ crates/ui-egui/src/i18n/uk.tsv | 11 +++++++++++ crates/ui-egui/src/i18n/vi.tsv | 11 +++++++++++ crates/ui-egui/src/i18n/zh-hans.tsv | 5 +++++ crates/ui-egui/src/i18n/zh-hant.tsv | 5 +++++ 15 files changed, 148 insertions(+) diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 6b804dc9c..5a2aea292 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -4915,3 +4915,14 @@ mark-style Japanese Japonské Import Data Sets… Importovat sady dat… Export Data Sets… Exportovat sady dat… Make Graph Dynamic Nastavit graf jako dynamický + Batch… Dávka… + Source Folder: Zdrojová složka: + Destination Folder: Cílová složka: + Overrides Přepsání + Uses every data set of the open document. Použije každou sadu dat otevřeného dokumentu. +@msg Batch: {count} processed Dávka: zpracováno {count} +@msg Batch: {count} processed, {failed} failed Dávka: zpracováno {count}, selhalo {failed} +@msg choose a destination folder vyberte cílovou složku +@msg choose a source folder vyberte zdrojovou složku +@msg open a document with data sets otevřete dokument se sadami dat +@msg the action has no steps akce nemá žádné kroky diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 131e64a43..862c70ecb 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4451,3 +4451,14 @@ theme Appearance Erscheinungsbild Import Data Sets… Datensätze importieren… Export Data Sets… Datensätze exportieren… Make Graph Dynamic Diagramm dynamisch machen + Batch… Stapelverarbeitung… + Source Folder: Quellordner: + Destination Folder: Zielordner: + Overrides Überschreibungen + Uses every data set of the open document. Verwendet jeden Datensatz des geöffneten Dokuments. +@msg Batch: {count} processed Stapelverarbeitung: {count} verarbeitet +@msg Batch: {count} processed, {failed} failed Stapelverarbeitung: {count} verarbeitet, {failed} fehlgeschlagen +@msg choose a destination folder wählen Sie einen Zielordner +@msg choose a source folder wählen Sie einen Quellordner +@msg open a document with data sets öffnen Sie ein Dokument mit Datensätzen +@msg the action has no steps die Aktion hat keine Schritte diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index f792ca030..8c71e88ad 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -4857,3 +4857,14 @@ mark-style Japanese Japonesas Import Data Sets… Importar conjuntos de datos… Export Data Sets… Exportar conjuntos de datos… Make Graph Dynamic Hacer gráfico dinámico + Batch… Lote… + Source Folder: Carpeta de origen: + Destination Folder: Carpeta de destino: + Overrides Anulaciones + Uses every data set of the open document. Usa todos los conjuntos de datos del documento abierto. +@msg Batch: {count} processed Lote: {count} procesados +@msg Batch: {count} processed, {failed} failed Lote: {count} procesados, {failed} con error +@msg choose a destination folder elija una carpeta de destino +@msg choose a source folder elija una carpeta de origen +@msg open a document with data sets abra un documento con conjuntos de datos +@msg the action has no steps la acción no tiene pasos diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 628bb75e9..a71873605 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -4914,3 +4914,14 @@ theme Appearance Apparence Import Data Sets… Importer des jeux de données… Export Data Sets… Exporter des jeux de données… Make Graph Dynamic Rendre le graphe dynamique + Batch… Traitement par lots… + Source Folder: Dossier source : + Destination Folder: Dossier de destination : + Overrides Remplacements + Uses every data set of the open document. Utilise chaque jeu de données du document ouvert. +@msg Batch: {count} processed Traitement par lots : {count} traités +@msg Batch: {count} processed, {failed} failed Traitement par lots : {count} traités, {failed} en échec +@msg choose a destination folder choisissez un dossier de destination +@msg choose a source folder choisissez un dossier source +@msg open a document with data sets ouvrez un document avec des jeux de données +@msg the action has no steps l'action n'a aucune étape diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 6313145f6..66ced89bf 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -4882,3 +4882,14 @@ imageTrace Outline Körvonal Import Data Sets… Adatkészletek importálása… Export Data Sets… Adatkészletek exportálása… Make Graph Dynamic Diagram dinamikussá tétele + Batch… Kötegelt feldolgozás… + Source Folder: Forrásmappa: + Destination Folder: Célmappa: + Overrides Felülbírálások + Uses every data set of the open document. A megnyitott dokumentum minden adatkészletét használja. +@msg Batch: {count} processed Kötegelt feldolgozás: {count} feldolgozva +@msg Batch: {count} processed, {failed} failed Kötegelt feldolgozás: {count} feldolgozva, {failed} sikertelen +@msg choose a destination folder válasszon célmappát +@msg choose a source folder válasszon forrásmappát +@msg open a document with data sets nyisson meg egy adatkészleteket tartalmazó dokumentumot +@msg the action has no steps a műveletnek nincsenek lépései diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 394e03fdc..093747349 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -4491,3 +4491,14 @@ lighting Light Luce Import Data Sets… Importa set di dati… Export Data Sets… Esporta set di dati… Make Graph Dynamic Rendi grafico dinamico + Batch… Elaborazione batch… + Source Folder: Cartella di origine: + Destination Folder: Cartella di destinazione: + Overrides Sostituzioni + Uses every data set of the open document. Usa ogni set di dati del documento aperto. +@msg Batch: {count} processed Elaborazione batch: {count} elaborati +@msg Batch: {count} processed, {failed} failed Elaborazione batch: {count} elaborati, {failed} non riusciti +@msg choose a destination folder scegli una cartella di destinazione +@msg choose a source folder scegli una cartella di origine +@msg open a document with data sets apri un documento con set di dati +@msg the action has no steps l'azione non ha passaggi diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 9c99e4836..0c5f7f462 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4486,3 +4486,14 @@ lighting Light ライト Import Data Sets… データセットを読み込み… Export Data Sets… データセットを書き出し… Make Graph Dynamic グラフを動的にする + Batch… バッチ… + Source Folder: ソースフォルダー: + Destination Folder: 出力先フォルダー: + Overrides 上書き + Uses every data set of the open document. 開いているドキュメントのすべてのデータセットを使用します。 +@msg Batch: {count} processed バッチ: {count} 件を処理しました +@msg Batch: {count} processed, {failed} failed バッチ: {count} 件を処理、{failed} 件が失敗しました +@msg choose a destination folder 出力先フォルダーを選択してください +@msg choose a source folder ソースフォルダーを選択してください +@msg open a document with data sets データセットのあるドキュメントを開いてください +@msg the action has no steps アクションにステップがありません diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 9cd201176..ea67dc650 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4455,3 +4455,14 @@ lighting Light 광원 Import Data Sets… 데이터 세트 가져오기… Export Data Sets… 데이터 세트 내보내기… Make Graph Dynamic 그래프를 동적으로 만들기 + Batch… 일괄 처리… + Source Folder: 소스 폴더: + Destination Folder: 대상 폴더: + Overrides 재정의 + Uses every data set of the open document. 열린 문서의 모든 데이터 세트를 사용합니다. +@msg Batch: {count} processed 일괄 처리: {count}개 처리됨 +@msg Batch: {count} processed, {failed} failed 일괄 처리: {count}개 처리됨, {failed}개 실패 +@msg choose a destination folder 대상 폴더를 선택하세요 +@msg choose a source folder 소스 폴더를 선택하세요 +@msg open a document with data sets 데이터 세트가 있는 문서를 여세요 +@msg the action has no steps 액션에 단계가 없습니다 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 73ceace12..8f68fc7cc 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -4759,3 +4759,14 @@ mark-style Japanese Japońskie Import Data Sets… Importuj zestawy danych… Export Data Sets… Eksportuj zestawy danych… Make Graph Dynamic Uczyń wykres dynamicznym + Batch… Przetwarzanie wsadowe… + Source Folder: Folder źródłowy: + Destination Folder: Folder docelowy: + Overrides Zastąpienia + Uses every data set of the open document. Używa każdego zestawu danych otwartego dokumentu. +@msg Batch: {count} processed Przetwarzanie wsadowe: przetworzono {count} +@msg Batch: {count} processed, {failed} failed Przetwarzanie wsadowe: przetworzono {count}, niepowodzenia: {failed} +@msg choose a destination folder wybierz folder docelowy +@msg choose a source folder wybierz folder źródłowy +@msg open a document with data sets otwórz dokument z zestawami danych +@msg the action has no steps operacja nie ma kroków diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 4bc55b099..23e036851 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -3829,3 +3829,9 @@ axis Both Ambos @msg span: "selection" places a linear or radial gradient, not a freeform one span: "selection" posiciona um degradê linear ou radial, não um de forma livre @msg span: "selection" places the gradient itself: leave out `start` and `end` (they already place it in document coordinates) span: "selection" posiciona o próprio degradê: omita `start` e `end` (eles já o posicionam em coordenadas do documento) Make Image Dynamic Tornar imagem dinâmica +@msg Batch: {count} processed Lote: {count} processados +@msg Batch: {count} processed, {failed} failed Lote: {count} processados, {failed} com falha +@msg choose a destination folder escolha uma pasta de destino +@msg choose a source folder escolha uma pasta de origem +@msg open a document with data sets abra um documento com conjuntos de dados +@msg the action has no steps a ação não tem etapas diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 8dac1596f..0962693d0 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -4487,3 +4487,14 @@ lighting Light Свет Import Data Sets… Импортировать наборы данных… Export Data Sets… Экспортировать наборы данных… Make Graph Dynamic Сделать диаграмму динамической + Batch… Пакетная обработка… + Source Folder: Исходная папка: + Destination Folder: Папка назначения: + Overrides Переопределения + Uses every data set of the open document. Использует каждый набор данных открытого документа. +@msg Batch: {count} processed Пакетная обработка: обработано {count} +@msg Batch: {count} processed, {failed} failed Пакетная обработка: обработано {count}, с ошибкой {failed} +@msg choose a destination folder выберите папку назначения +@msg choose a source folder выберите исходную папку +@msg open a document with data sets откройте документ с наборами данных +@msg the action has no steps в действии нет шагов diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 68695a5a3..a116db7a6 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -4471,3 +4471,14 @@ lighting Light Світло Import Data Sets… Імпортувати набори даних… Export Data Sets… Експортувати набори даних… Make Graph Dynamic Зробити діаграму динамічною + Batch… Пакетна обробка… + Source Folder: Вихідна папка: + Destination Folder: Папка призначення: + Overrides Перевизначення + Uses every data set of the open document. Використовує кожен набір даних відкритого документа. +@msg Batch: {count} processed Пакетна обробка: оброблено {count} +@msg Batch: {count} processed, {failed} failed Пакетна обробка: оброблено {count}, з помилкою {failed} +@msg choose a destination folder виберіть папку призначення +@msg choose a source folder виберіть вихідну папку +@msg open a document with data sets відкрийте документ із наборами даних +@msg the action has no steps у дії немає кроків diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 52df84888..d04b102cb 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4471,3 +4471,14 @@ theme Appearance Giao diện Import Data Sets… Nhập tập dữ liệu… Export Data Sets… Xuất tập dữ liệu… Make Graph Dynamic Tạo biểu đồ động + Batch… Hàng loạt… + Source Folder: Thư mục nguồn: + Destination Folder: Thư mục đích: + Overrides Ghi đè + Uses every data set of the open document. Dùng mọi tập dữ liệu của tài liệu đang mở. +@msg Batch: {count} processed Hàng loạt: đã xử lý {count} +@msg Batch: {count} processed, {failed} failed Hàng loạt: đã xử lý {count}, lỗi {failed} +@msg choose a destination folder hãy chọn thư mục đích +@msg choose a source folder hãy chọn thư mục nguồn +@msg open a document with data sets hãy mở một tài liệu có tập dữ liệu +@msg the action has no steps hành động không có bước nào diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 7e5a99ba4..42f79c1b1 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -4032,3 +4032,8 @@ lighting Light 光源 Import Data Sets… 导入数据集… Export Data Sets… 导出数据集… Make Graph Dynamic 使图表动态化 + Batch… 批处理… + Source Folder: 源文件夹: + Destination Folder: 目标文件夹: + Overrides 覆盖 + Uses every data set of the open document. 使用打开文档的每个数据集。 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 232fc4834..a5be81c89 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -3819,3 +3819,8 @@ lighting Light 光源 Import Data Sets… 匯入資料集… Export Data Sets… 匯出資料集… Make Graph Dynamic 使圖表動態化 + Batch… 批次處理… + Source Folder: 來源資料夾: + Destination Folder: 目的地資料夾: + Overrides 覆寫 + Uses every data set of the open document. 使用開啟文件的每個資料集。