From 2b975d620ebb882f28b1142d4f13a249e685001c Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 16:02:00 -0800 Subject: [PATCH 1/2] Actions: Insert Select Path stores the selected path as a step The Actions panel menu has Insert Select Path, after Insert Menu Item. With one whole path or compound path selected, it inserts a Select Path step into the action being recorded, or after the selected action or step. Playing the step selects that path again. - New engine command `select.storedPath {id, fingerprint}`. The step stores the path's object id and a fingerprint of its geometry and fill rule ("v1:" and 16 lowercase hex digits, FNV-1a 64). Playback selects the path only while it exists unchanged and can be selected; otherwise the step fails and the selection stays as it was. A path on a locked artboard is refused in isolation mode too. - Capture refuses an empty selection, several objects, selected anchors or segments alone, groups, type, guides, a path selected together with ruler guides, a member of a compound path, and hidden, locked or template art. - New UI command `ui.action.insertSelectPath {set?, action?, step?}`. Step rows show "Select Path". - `.vcactions` files that hold a Select Path step are written as version 4. Reading accepts versions 1 to 4 and refuses a Select Path step with invalid params. - Rows for the new labels and messages in every catalog. Docs: docs/mcp.md, docs/target-app-parity.md. --- crates/engine/src/cmd.rs | 2 +- crates/engine/src/cmd/plugin_call/reviewed.rs | 1 + crates/engine/src/cmd/select.rs | 153 +++++++++++++ crates/engine/src/lib.rs | 1 + crates/engine/src/tests.rs | 1 + crates/engine/src/tests/stored_path.rs | 202 +++++++++++++++++ crates/mcp/src/ui_catalog.rs | 6 + crates/ui-egui/src/i18n/bg.tsv | 7 + crates/ui-egui/src/i18n/bs.tsv | 7 + crates/ui-egui/src/i18n/ca.tsv | 7 + crates/ui-egui/src/i18n/cs.tsv | 7 + crates/ui-egui/src/i18n/da.tsv | 7 + crates/ui-egui/src/i18n/de.tsv | 7 + crates/ui-egui/src/i18n/el.tsv | 7 + crates/ui-egui/src/i18n/es.tsv | 7 + crates/ui-egui/src/i18n/fi.tsv | 7 + crates/ui-egui/src/i18n/fr.tsv | 7 + crates/ui-egui/src/i18n/hi.tsv | 7 + crates/ui-egui/src/i18n/hu.tsv | 7 + crates/ui-egui/src/i18n/id.tsv | 7 + crates/ui-egui/src/i18n/it.tsv | 7 + crates/ui-egui/src/i18n/ja.tsv | 7 + crates/ui-egui/src/i18n/ka.tsv | 7 + crates/ui-egui/src/i18n/ko.tsv | 7 + crates/ui-egui/src/i18n/nl.tsv | 7 + crates/ui-egui/src/i18n/pl.tsv | 7 + crates/ui-egui/src/i18n/pt-br.tsv | 7 + crates/ui-egui/src/i18n/ru.tsv | 7 + crates/ui-egui/src/i18n/sv.tsv | 7 + crates/ui-egui/src/i18n/tr.tsv | 7 + crates/ui-egui/src/i18n/uk.tsv | 7 + crates/ui-egui/src/i18n/vi.tsv | 7 + crates/ui-egui/src/i18n/zh-hans.tsv | 7 + crates/ui-egui/src/i18n/zh-hant.tsv | 7 + crates/ui-egui/src/menus/run.rs | 1 + crates/ui-egui/src/menus/ui_commands.rs | 6 + crates/ui-egui/src/panels/actions.rs | 214 +++++++++++++++++- crates/ui-egui/src/panels/actions/files.rs | 40 +++- docs/mcp.md | 20 ++ docs/target-app-parity.md | 1 + 40 files changed, 824 insertions(+), 13 deletions(-) create mode 100644 crates/engine/src/tests/stored_path.rs diff --git a/crates/engine/src/cmd.rs b/crates/engine/src/cmd.rs index 66eb2104d1..1a0cf10846 100644 --- a/crates/engine/src/cmd.rs +++ b/crates/engine/src/cmd.rs @@ -82,7 +82,7 @@ pub mod rasterfx; mod recolor; pub mod recovery; pub mod scripts; -mod select; +pub(crate) mod select; pub(crate) mod shaper; pub mod shortcutset; pub(crate) mod slices; diff --git a/crates/engine/src/cmd/plugin_call/reviewed.rs b/crates/engine/src/cmd/plugin_call/reviewed.rs index fff7289352..9254022a4a 100644 --- a/crates/engine/src/cmd/plugin_call/reviewed.rs +++ b/crates/engine/src/cmd/plugin_call/reviewed.rs @@ -523,6 +523,7 @@ pub(super) const ALLOWED: &[&str] = &[ "select.same.textStrokeColor", "select.segments", "select.set", + "select.storedPath", "select.toggle", "select.update", "shape.arc", diff --git a/crates/engine/src/cmd/select.rs b/crates/engine/src/cmd/select.rs index a2c7b3a8d4..e1e39d9b48 100644 --- a/crates/engine/src/cmd/select.rs +++ b/crates/engine/src/cmd/select.rs @@ -48,6 +48,15 @@ pub fn specs() -> Vec { has_doc, set ), + cmd!( + "select.storedPath", + "Select Path", + [], + None, + "{id: object id, fingerprint: text (\"v1:\" and 16 lowercase hex digits)} the Actions panel's Select Path step: select only the path or compound path `id` when its fingerprint (a hash of its kind, fill rule and anchors, its members' for a compound path) is still `fingerprint` and it can be selected: not hidden or locked, nor in a hidden or locked layer or group, a template layer or on a locked artboard, and inside the isolated object in isolation mode. A missing object, another kind of object, a path whose kind, fill rule or anchors changed, or one that cannot be selected is an error, and the selection stays as it was → {count, ids} (the resulting selection)", + has_doc, + stored_path + ), cmd!( "select.add", "Add to Selection", @@ -439,6 +448,150 @@ fn selection_result(s: &Session) -> Result { Ok(json!({ "count": selection.len(), "ids": selection.objects })) } +/// The prefix of every fingerprint [`path_fingerprint`] gives: the version of its hash. +const FINGERPRINT_PREFIX: &str = "v1:"; + +/// FNV-1a (64 bits) over the bytes written to it, starting from the offset basis. +pub(crate) struct Fnv(pub(crate) u64); + +impl Default for Fnv { + fn default() -> Self { + Self(0xcbf2_9ce4_8422_2325) + } +} + +impl std::io::Write for Fnv { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.0 = buf.iter().fold(self.0, |h, b| (h ^ u64::from(*b)).wrapping_mul(0x0100_0000_01b3)); + Ok(buf.len()) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +/// The fingerprint of the path or compound path `n` that `select.storedPath` compares: FNV-1a (64 +/// bits) over a kind byte, the fill rule and the anchors of the path, or of each member of the +/// compound path in order, as JSON, written as [`FINGERPRINT_PREFIX`] and 16 lowercase hex digits. +/// None for any object [`is_storable_kind`] does not take. +pub fn path_fingerprint(n: &Node) -> Option { + fn feed(h: &mut Fnv, n: &Node) -> Option<()> { + use std::io::Write; + match &n.kind { + NodeKind::Path { path, rule, .. } => { + h.write_all(b"P").ok()?; + serde_json::to_writer(&mut *h, rule).ok()?; + serde_json::to_writer(&mut *h, path).ok() + } + NodeKind::Compound { children, rule } => { + h.write_all(b"C").ok()?; + serde_json::to_writer(&mut *h, rule).ok()?; + h.write_all(&(children.len() as u64).to_le_bytes()).ok()?; + children.iter().try_for_each(|c| feed(h, c)) + } + _ => None, + } + } + if !is_storable_kind(n) { + return None; + } + let mut h = Fnv::default(); + feed(&mut h, n)?; + Some(format!("{FINGERPRINT_PREFIX}{:016x}", h.0)) +} + +/// Whether `n` is an object a Select Path step can store: a path that is not a guide, or a compound +/// path whose members all are. +fn is_storable_kind(n: &Node) -> bool { + let path = |n: &Node| matches!(n.kind, NodeKind::Path { guide: false, .. }); + match &n.kind { + NodeKind::Compound { children, .. } => children.iter().all(|c| path(c)), + _ => path(n), + } +} + +/// The `id` and `fingerprint` of `select.storedPath`'s params `p`, or why they are not ones: an +/// object with only a non-negative whole `id` and a `fingerprint` that [`path_fingerprint`] could give. +pub fn stored_path_params(p: &Value) -> std::result::Result<(NodeId, &str), String> { + let o = p.as_object().ok_or("`params` must be an object")?; + if let Some(key) = o.keys().find(|k| !matches!(k.as_str(), "id" | "fingerprint")) { + let keys = "id, fingerprint"; + return Err(format!("unknown parameter `{key}`; the command takes: {keys}")); + } + let value = o.get("id").unwrap_or(&Value::Null); + let id = value.as_u64().map(NodeId).ok_or_else(|| format!("invalid object id {value}: expected a non-negative integer"))?; + let fingerprint = o.get("fingerprint").and_then(Value::as_str).unwrap_or_default(); + let hex = fingerprint.strip_prefix(FINGERPRINT_PREFIX).unwrap_or_default(); + if hex.len() != 16 || !hex.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) { + return Err("`fingerprint` must be \"v1:\" and 16 lowercase hex digits".into()); + } + Ok((id, fingerprint)) +} + +/// Why the object `id` of `st` cannot be the path of a Select Path step: it is not there, is not a +/// path (not a guide) or compound path ([`is_storable_kind`]), is a member of a compound path, cannot +/// be selected ([`is_selectable`]), or is on a locked artboard, in isolation mode too. +fn storable_path(st: &DocState, id: NodeId) -> std::result::Result<(), String> { + let value = id.0; + let n = st.doc.node(id).ok_or_else(|| format!("no such object id {value}"))?; + let in_compound = st.doc.parent_of(id).and_then(|p| st.doc.node(p)).is_some_and(|p| matches!(p.kind, NodeKind::Compound { .. })); + if !is_storable_kind(n) || in_compound { + return Err(WHOLE_PATH.into()); + } + // In isolation mode `is_selectable` does not look at the locked artboards, so they are checked here too. + let locks = st.doc.locked_artboards(); + if !is_selectable(&st.doc, st.isolation, &locks, id) || locks.holds_id(id) { + return Err("the path is hidden or locked, or cannot be selected here".into()); + } + Ok(()) +} + +/// What [`storable_selection`] asks for when the selection is not a whole path or compound path. +const WHOLE_PATH: &str = "select one whole path or compound path"; + +/// The one object selected in `st` when it can be the path of a Select Path step: a whole path or +/// compound path that can be selected ([`storable_path`]). An error for no selection, several +/// objects, a path with ruler guides, anchors or segments of a path alone, a group, type or any other object, a guide, or a +/// member of a compound path. It does not hash the path, so menus can ask it every frame. +pub fn storable_selection(st: &DocState) -> std::result::Result { + let sel = &st.selection; + let id = match sel.objects.as_slice() { + [] => return Err("select a path".into()), + [id] if sel.guides.is_empty() => *id, + _ => return Err("select only one path".into()), + }; + if sel.anchors.get(&id).is_some_and(|a| !a.is_empty()) || sel.segments.get(&id).is_some_and(|s| !s.is_empty()) { + return Err(WHOLE_PATH.into()); + } + storable_path(st, id)?; + Ok(id) +} + +/// The params of a `select.storedPath` step for the path selected in `st` ([`storable_selection`]): +/// `{id, fingerprint}`. +pub fn stored_path_step(st: &DocState) -> std::result::Result { + let id = storable_selection(st)?; + let fingerprint = st.doc.node(id).and_then(path_fingerprint).ok_or(WHOLE_PATH)?; + Ok(json!({ "id": id.0, "fingerprint": fingerprint })) +} + +/// `select.storedPath {id, fingerprint}`: selects only the path `id` when it is still the path the +/// step stored ([`path_fingerprint`]) and can be selected ([`storable_path`]); else an error, and +/// the selection stays as it was. +fn stored_path(s: &mut Session, p: &Value) -> Result { + const C: &str = "select.storedPath"; + let (id, fingerprint) = stored_path_params(p).map_err(|e| bad(C, e))?; + let st = s.doc()?; + let value = id.0; + let n = st.doc.node(id).ok_or_else(|| bad(C, format!("no such object id {value}")))?; + if path_fingerprint(n).as_deref() != Some(fingerprint) { + return Err(bad(C, format!("object {value} is not the stored path: it changed, or the document is another one"))); + } + storable_path(st, id).map_err(|e| bad(C, e))?; + s.select(|_, sel| sel.set([id]))?; + selection_result(s) +} + fn key(s: &mut Session, p: &Value) -> Result { let id = p.get("id").map(|value| checked_id(s, value, "select.key")).transpose()?; let guide = match p.get("guide").filter(|v| !v.is_null()) { diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index a891fb9599..0a5eeaa65c 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -32,6 +32,7 @@ pub use cmd::distortcmds::perspective_click; pub use cmd::params::{check_keys, check_params}; pub use cmd::pathops::auto_simplify_level; pub use cmd::rasterfx::{export_pdf, flatten_raster_effects}; +pub use cmd::select::{path_fingerprint, storable_selection, stored_path_params, stored_path_step}; pub use cmd::{CommandInfo, CommandSpec, command_specs, find_command}; pub use cmd::{EyedropperOptions, PathfinderOptions}; pub use journal::Journal; diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 18eadcdf80..36c61b411b 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -175,6 +175,7 @@ mod smartguides; mod smooth_path; mod spelling; mod store; +mod stored_path; mod strokegeom; mod strokegradient; mod strokereach; diff --git a/crates/engine/src/tests/stored_path.rs b/crates/engine/src/tests/stored_path.rs new file mode 100644 index 0000000000..3e3bd4ef95 --- /dev/null +++ b/crates/engine/src/tests/stored_path.rs @@ -0,0 +1,202 @@ +//! `select.storedPath`, the Actions panel's Select Path step: the step stores a path's id and a +//! fingerprint of it, and playback selects that path only while it is still there unchanged and +//! can be selected. + +use std::io::Write; + +use serde_json::{Value, json}; + +use crate::*; + +/// A document with two rectangles and an ellipse, nothing selected → the session and their ids. +fn doc() -> (Session, [u64; 3]) { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + let mut add = |shape: &str, x: u32| s.execute(shape, &json!({"x": x, "y": 10, "width": 40, "height": 30})).unwrap()["id"].as_u64().unwrap(); + let ids = [add("shape.rectangle", 10), add("shape.rectangle", 80), add("shape.ellipse", 150)]; + s.execute("select.none", &json!({})).unwrap(); + (s, ids) +} + +fn select(s: &mut Session, ids: &[u64]) { + s.execute("select.set", &json!({"ids": ids})).unwrap(); +} + +fn selected(s: &Session) -> Vec { + s.doc().unwrap().selection.objects.iter().map(|id| id.0).collect() +} + +/// The Select Path step's params for `id`, selected alone. +fn step_for(s: &mut Session, id: u64) -> Value { + select(s, &[id]); + stored_path_step(s.doc().unwrap()).unwrap() +} + +#[test] +fn the_hash_is_fnv_1a_64() { + // The published FNV-1a 64-bit test values for "", "a" and "foobar". + for (bytes, want) in [(&b""[..], 0xcbf2_9ce4_8422_2325_u64), (b"a", 0xaf63_dc4c_8601_ec8c), (b"foobar", 0x8594_4171_f739_67e8)] { + let mut h = crate::cmd::select::Fnv::default(); + h.write_all(bytes).unwrap(); + assert_eq!(h.0, want, "{bytes:?}"); + } +} + +#[test] +fn the_step_stores_the_id_and_a_fingerprint_and_plays_by_selecting_the_path() { + let (mut s, [a, b, _]) = doc(); + let p = step_for(&mut s, a); + assert_eq!(p["id"], json!(a)); + let fingerprint = p["fingerprint"].as_str().unwrap(); + assert!(fingerprint.starts_with("v1:") && fingerprint.len() == 19, "{fingerprint}"); + assert_eq!(step_for(&mut s, a), p, "the same path gives the same step"); + assert_ne!(step_for(&mut s, b)["fingerprint"], p["fingerprint"], "another path gives another fingerprint"); + select(&mut s, &[b]); + let r = s.execute("select.storedPath", &p).unwrap(); + assert_eq!(r, json!({"count": 1, "ids": [a]})); + assert_eq!(selected(&s), [a], "only the stored path is selected"); +} + +#[test] +fn a_compound_path_is_one_step_and_a_path_in_a_group_is_one_too() { + let (mut s, [a, b, c]) = doc(); + select(&mut s, &[a, b]); + let compound = s.execute("object.compoundPath.make", &json!({})).unwrap()["id"].as_u64().unwrap(); + let p = step_for(&mut s, compound); + let member = s.doc().unwrap().doc.children(Some(vectorcraft_doc::NodeId(compound))).unwrap()[0].id.0; + select(&mut s, &[member]); + assert_eq!(stored_path_step(s.doc().unwrap()).unwrap_err(), "select one whole path or compound path", "a member alone is not stored"); + s.execute("select.none", &json!({})).unwrap(); + s.execute("select.storedPath", &p).unwrap(); + assert_eq!(selected(&s), [compound]); + + select(&mut s, &[c, compound]); + let group = s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap(); + assert_eq!(stored_path_step(s.doc().unwrap()).unwrap_err(), "select one whole path or compound path", "a group is not stored"); + let inner = step_for(&mut s, c); + select(&mut s, &[group]); + s.execute("select.storedPath", &inner).unwrap(); + assert_eq!(selected(&s), [c], "the path inside the group is selected alone"); +} + +#[test] +fn only_one_whole_path_that_can_be_selected_is_stored() { + let (mut s, [a, b, _]) = doc(); + let whole = "select one whole path or compound path"; + assert_eq!(stored_path_step(s.doc().unwrap()).unwrap_err(), "select a path"); + select(&mut s, &[a, b]); + assert_eq!(stored_path_step(s.doc().unwrap()).unwrap_err(), "select only one path"); + s.execute("guide.add", &json!({"vertical": true, "pos": 200})).unwrap(); + select(&mut s, &[a]); + s.execute("guide.select", &json!({"indexes": [0], "toggle": true})).unwrap(); + assert_eq!(s.doc().unwrap().selection.guides.len(), 1, "the guide is selected with the path"); + assert_eq!(stored_path_step(s.doc().unwrap()).unwrap_err(), "select only one path", "a path with a guide"); + s.execute("guide.select", &json!({"indexes": [0], "toggle": true})).unwrap(); + assert_eq!(stored_path_step(s.doc().unwrap()).unwrap(), step_for(&mut s, a), "the path alone again"); + s.execute("select.anchors", &json!({"id": a, "anchors": [[0, 0]], "mode": "set"})).unwrap(); + assert_eq!(stored_path_step(s.doc().unwrap()).unwrap_err(), whole, "an anchor alone"); + let t = s.execute("text.create", &json!({"x": 10, "y": 200, "text": "Type"})).unwrap()["id"].as_u64().unwrap(); + select(&mut s, &[t]); + assert_eq!(stored_path_step(s.doc().unwrap()).unwrap_err(), whole, "type"); + select(&mut s, &[a]); + s.execute("object.lock", &json!({})).unwrap(); + let mut st = s.doc().unwrap().clone(); + st.selection.objects = vec![vectorcraft_doc::NodeId(a)]; + assert_eq!(stored_path_step(&st).unwrap_err(), "the path is hidden or locked, or cannot be selected here"); + let guide = { + let mut d = (*st.doc).clone(); + if let Some(vectorcraft_doc::NodeKind::Path { guide, .. }) = d.node_mut(vectorcraft_doc::NodeId(a)).map(|n| &mut n.kind) { + *guide = true; + } + d + }; + assert!(path_fingerprint(guide.node(vectorcraft_doc::NodeId(a)).unwrap()).is_none(), "a guide is not stored"); +} + +#[test] +fn a_changed_missing_hidden_or_other_path_is_an_error_and_keeps_the_selection() { + let (mut s, [a, b, c]) = doc(); + let p = step_for(&mut s, a); + let fails = |s: &mut Session, p: &Value, want: &str| { + select(s, &[c]); + let e = s.execute("select.storedPath", p).unwrap_err().to_string(); + assert!(e.contains(want), "{e}"); + assert_eq!(selected(s), [c], "the selection stays as it was"); + }; + select(&mut s, &[a]); + s.execute("object.move", &json!({"dx": 5, "dy": 0})).unwrap(); + fails(&mut s, &p, "is not the stored path"); + let moved = step_for(&mut s, a); + s.execute("object.hide", &json!({})).unwrap(); + fails(&mut s, &moved, "hidden or locked"); + s.execute("object.showAll", &json!({})).unwrap(); + let shown = step_for(&mut s, b); + s.execute("edit.clear", &json!({})).unwrap(); + fails(&mut s, &shown, "no such object id"); + // Another document: the id names another path there, or nothing. + s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); + s.execute("shape.rectangle", &json!({"x": 1, "y": 1, "width": 7, "height": 9})).unwrap(); + let c2 = s.execute("shape.ellipse", &json!({"x": 20, "y": 20, "width": 9, "height": 7})).unwrap()["id"].as_u64().unwrap(); + select(&mut s, &[c2]); + assert!(s.execute("select.storedPath", &moved).is_err()); + assert_eq!(selected(&s), [c2]); +} + +#[test] +fn a_path_on_a_locked_artboard_is_refused_in_isolation_mode_too() { + let (mut s, [a, b, _]) = doc(); + select(&mut s, &[a, b]); + let group = s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap(); + let p = step_for(&mut s, a); + s.execute("select.none", &json!({})).unwrap(); + s.execute("object.isolate", &json!({"id": group})).unwrap(); + s.execute("artboard.new", &json!({"x": 0, "y": 0, "width": 300, "height": 200})).unwrap(); + s.execute("artboard.setProps", &json!({"index": 1, "locked": true})).unwrap(); + assert!(s.doc().unwrap().isolation.is_some(), "the group stays isolated"); + assert!(s.doc().unwrap().doc.on_locked_artboard(vectorcraft_doc::NodeId(a)), "the path is on the locked artboard"); + let mut st = s.doc().unwrap().clone(); + st.selection.objects = vec![vectorcraft_doc::NodeId(a)]; + assert_eq!(stored_path_step(&st).unwrap_err(), "the path is hidden or locked, or cannot be selected here", "capture"); + let before = selected(&s); + let e = s.execute("select.storedPath", &p).unwrap_err().to_string(); + assert!(e.contains("hidden or locked"), "{e}"); + assert_eq!(selected(&s), before, "the selection stays as it was"); +} + +#[test] +fn a_failing_step_in_a_batch_undoes_the_steps_before_it() { + let (mut s, [a, _, c]) = doc(); + let p = step_for(&mut s, a); + select(&mut s, &[a]); + s.execute("object.move", &json!({"dx": 5, "dy": 0})).unwrap(); + select(&mut s, &[c]); + let before = s.doc().unwrap().doc.clone(); + let steps = [json!({"command": "object.move", "params": {"dx": 1, "dy": 0}}), json!({"command": "select.storedPath", "params": p})]; + assert!(s.execute("command.batch", &json!({"commands": steps})).is_err()); + assert!(s.doc().unwrap().doc == before, "the move before the step is undone"); +} + +#[test] +fn params_are_an_id_and_a_v1_fingerprint() { + let (mut s, [a, ..]) = doc(); + let p = step_for(&mut s, a); + let f = p["fingerprint"].clone(); + for bad in [ + json!({"fingerprint": f}), + json!({"id": -1, "fingerprint": f}), + json!({"id": "1", "fingerprint": f}), + json!({"id": a}), + json!({"id": a, "fingerprint": 5}), + json!({"id": a, "fingerprint": "v2:0123456789abcdef"}), + json!({"id": a, "fingerprint": "v1:0123456789ABCDEF"}), + json!({"id": a, "fingerprint": "v1:0123456789abcde"}), + json!({"id": a, "fingerprint": f, "extra": 1}), + json!([a]), + ] { + assert!(stored_path_params(&bad).is_err(), "{bad}"); + s.execute("select.none", &json!({})).unwrap(); + assert!(s.execute("select.storedPath", &bad).is_err(), "{bad}"); + assert!(selected(&s).is_empty(), "{bad}"); + } + assert!(stored_path_params(&p).is_ok()); +} diff --git a/crates/mcp/src/ui_catalog.rs b/crates/mcp/src/ui_catalog.rs index e449bd52cc..35913573be 100644 --- a/crates/mcp/src/ui_catalog.rs +++ b/crates/mcp/src/ui_catalog.rs @@ -1433,6 +1433,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "", "{set?: index, action?: index, step?: index, command, params?: object} insert the command of a menu item as a step, without running it; the step plays as choosing the item runs it. command and params (default {}) must be those of a menu item that can be a step: one that runs an engine command, whose label does not end with \"…\", outside the VectorCraft, File, Window and Help menus, and that is not Undo, Redo, a Paste, a batch (command.batch), a preference (prefs.*) or action (action.*) command, an Object › Align item, Image Trace's Make or Make and Expand, Pattern's Make or Edit Pattern, a saved selection, saved view or recent font item, or a plug-in's item. The place is as ui.action.insertStop takes it: with set and action, after step, else after the action's last step; without them, the recording in progress. The new step becomes the panel's selected step, and its action and set become the selected ones. A command and params that no such menu item has, a param of another type, params that are not an object, no action and no recording, or a step that would give the action more than 10,000 steps or its set's file more than 8 MiB, counting the steps that the recording in progress adds to the set, is an error and changes nothing. The panel menu's Insert Menu Item… (dialog `actionInsertMenuItem` with find, command, params, set, action and step) runs this on OK → {set, action, step}, or {recording: true}", ), + ( + "ui.action.insertSelectPath", + "Insert Select Path", + "", + "{set?: index, action?: index, step?: index} insert a Select Path step for the path selected in the active document, without changing the selection: the step `select.storedPath {id, fingerprint}` stores the path's id and a fingerprint of its kind, fill rule and anchors, and playback selects only that path while it is still there unchanged and can be selected, else the step is an error. The selection must be one whole path or compound path that can be selected: not anchors or segments alone, several objects, a group, type or another kind of object, a guide, a member of a compound path, or a hidden or locked object. The place is as ui.action.insertStop takes it: with set and action, after step, else after the action's last step; without them, the recording in progress. The new step becomes the panel's selected step, and its action and set become the selected ones. No document, a selection that is not such a path, a param of another type, params that are not an object, no action and no recording, or a step that would give the action more than 10,000 steps or its set's file more than 8 MiB, counting the steps that the recording in progress adds to the set, is an error and changes nothing. The panel menu's Insert Select Path runs this, for the selected step or action or the recording in progress → {set, action, step}, or {recording: true}", + ), ( "ui.action.continue", "Continue Action", diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 582a927f09..f69cd07e32 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -5976,6 +5976,13 @@ axis Y Axis Y ос @msg Batch cannot play an action with a stop Пакет не може да изпълни действие със спиране Insert Menu Item… Вмъкване на елемент от меню… Insert Menu Item Вмъкване на елемент от меню + Insert Select Path Вмъкване на избор на път + Select Path Избор на път +@msg select one whole path or compound path изберете един цял път или съставен път +@msg select only one path изберете само един път +@msg the path is hidden or locked, or cannot be selected here пътят е скрит или заключен, или не може да бъде избран тук +@msg object {value} is not the stored path: it changed, or the document is another one обект {value} не е запазеният път: той е променен или документът е друг +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` трябва да е "v1:" и 16 шестнадесетични цифри с малки букви Menu Item: Елемент от меню: No menu item matches. Няма съвпадащ елемент от меню. Several menu items match. Choose one from the list. Съвпадат няколко елемента от меню. Изберете един от списъка. diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index d92da7ae40..ea38155135 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -6018,6 +6018,13 @@ axis Y Axis Y osa @msg Batch cannot play an action with a stop Paketna obrada ne može reprodukovati akciju sa zaustavljanjem Insert Menu Item… Umetni stavku menija… Insert Menu Item Umetni stavku menija + Insert Select Path Umetni odabir putanje + Select Path Odabir putanje +@msg select one whole path or compound path odaberite jednu cijelu putanju ili složenu putanju +@msg select only one path odaberite samo jednu putanju +@msg the path is hidden or locked, or cannot be selected here putanja je skrivena ili zaključana, ili se ovdje ne može odabrati +@msg object {value} is not the stored path: it changed, or the document is another one objekt {value} nije sačuvana putanja: promijenjena je ili je dokument drugi +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` mora biti "v1:" i 16 malih heksadecimalnih cifara Menu Item: Stavka menija: No menu item matches. Nijedna stavka menija ne odgovara. Several menu items match. Choose one from the list. Odgovara više stavki menija. Izaberite jednu sa liste. diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 283dae1800..f3334536b3 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -4032,6 +4032,13 @@ digits Malayalam Malaiàlam @msg Batch cannot play an action with a stop Lot no pot reproduir una acció amb una aturada Insert Menu Item… Insereix un element de menú… Insert Menu Item Insereix un element de menú + Insert Select Path Insereix una selecció de traçat + Select Path Selecciona el traçat +@msg select one whole path or compound path selecciona un traçat sencer o un traçat compost +@msg select only one path selecciona només un traçat +@msg the path is hidden or locked, or cannot be selected here el traçat està amagat o bloquejat, o no es pot seleccionar aquí +@msg object {value} is not the stored path: it changed, or the document is another one l’objecte {value} no és el traçat desat: ha canviat o el document és un altre +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` ha de ser "v1:" i 16 dígits hexadecimals en minúscula Menu Item: Element de menú: No menu item matches. Cap element de menú no coincideix. Several menu items match. Choose one from the list. Coincideixen diversos elements de menú. Trieu-ne un de la llista. diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 519963c4eb..ea730781d9 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6338,6 +6338,13 @@ axis Y Axis osa Y @msg Batch cannot play an action with a stop Dávka nemůže přehrát akci se zastavením Insert Menu Item… Vložit položku nabídky… Insert Menu Item Vložit položku nabídky + Insert Select Path Vložit výběr cesty + Select Path Vybrat cestu +@msg select one whole path or compound path vyberte jednu celou cestu nebo složenou cestu +@msg select only one path vyberte jen jednu cestu +@msg the path is hidden or locked, or cannot be selected here cesta je skrytá nebo zamčená, nebo ji zde nelze vybrat +@msg object {value} is not the stored path: it changed, or the document is another one objekt {value} není uložená cesta: změnila se, nebo jde o jiný dokument +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` musí být "v1:" a 16 šestnáctkových číslic malými písmeny Menu Item: Položka nabídky: No menu item matches. Žádná položka nabídky neodpovídá. Several menu items match. Choose one from the list. Odpovídá několik položek nabídky. Vyberte jednu ze seznamu. diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 3269755003..e9519ab9db 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -5993,6 +5993,13 @@ axis Y Axis Y-akse @msg Batch cannot play an action with a stop Batch kan ikke afspille en handling med et stop Insert Menu Item… Indsæt menupunkt… Insert Menu Item Indsæt menupunkt + Insert Select Path Indsæt kurvemarkering + Select Path Markér kurve +@msg select one whole path or compound path markér én hel kurve eller sammensat kurve +@msg select only one path markér kun én kurve +@msg the path is hidden or locked, or cannot be selected here kurven er skjult eller låst, eller kan ikke markeres her +@msg object {value} is not the stored path: it changed, or the document is another one objekt {value} er ikke den gemte kurve: den er ændret, eller dokumentet er et andet +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` skal være "v1:" og 16 hexadecimale cifre med små bogstaver Menu Item: Menupunkt: No menu item matches. Intet menupunkt passer. Several menu items match. Choose one from the list. Flere menupunkter passer. Vælg et fra listen. diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index f3858abc1f..e57dda183c 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5929,6 +5929,13 @@ axis Y Axis Y-Achse @msg Batch cannot play an action with a stop Die Stapelverarbeitung kann keine Aktion mit einem Stopp abspielen Insert Menu Item… Menübefehl einfügen… Insert Menu Item Menübefehl einfügen + Insert Select Path Pfadauswahl einfügen + Select Path Pfad auswählen +@msg select one whole path or compound path wählen Sie einen ganzen Pfad oder zusammengesetzten Pfad aus +@msg select only one path wählen Sie nur einen Pfad aus +@msg the path is hidden or locked, or cannot be selected here der Pfad ist ausgeblendet oder gesperrt oder kann hier nicht ausgewählt werden +@msg object {value} is not the stored path: it changed, or the document is another one Objekt {value} ist nicht der gespeicherte Pfad: Er wurde geändert, oder es ist ein anderes Dokument +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` muss aus "v1:" und 16 hexadezimalen Ziffern in Kleinbuchstaben bestehen Menu Item: Menübefehl: No menu item matches. Kein Menübefehl passt. Several menu items match. Choose one from the list. Mehrere Menübefehle passen. Wählen Sie einen aus der Liste. diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 52a12276f7..b885996171 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -6289,6 +6289,13 @@ axis Y Axis Άξονας Y @msg Batch cannot play an action with a stop Η μαζική επεξεργασία δεν μπορεί να αναπαράγει ενέργεια με διακοπή Insert Menu Item… Εισαγωγή στοιχείου μενού… Insert Menu Item Εισαγωγή στοιχείου μενού + Insert Select Path Εισαγωγή επιλογής διαδρομής + Select Path Επιλογή διαδρομής +@msg select one whole path or compound path επιλέξτε μία ολόκληρη διαδρομή ή σύνθετη διαδρομή +@msg select only one path επιλέξτε μόνο μία διαδρομή +@msg the path is hidden or locked, or cannot be selected here η διαδρομή είναι κρυφή ή κλειδωμένη, ή δεν μπορεί να επιλεγεί εδώ +@msg object {value} is not the stored path: it changed, or the document is another one το αντικείμενο {value} δεν είναι η αποθηκευμένη διαδρομή: άλλαξε ή το έγγραφο είναι άλλο +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits το `fingerprint` πρέπει να είναι "v1:" και 16 πεζά δεκαεξαδικά ψηφία Menu Item: Στοιχείο μενού: No menu item matches. Κανένα στοιχείο μενού δεν ταιριάζει. Several menu items match. Choose one from the list. Ταιριάζουν πολλά στοιχεία μενού. Επιλέξτε ένα από τη λίστα. diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index e6f77cd908..bc6f6f5749 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -6280,6 +6280,13 @@ axis Y Axis Eje Y @msg Batch cannot play an action with a stop Lote no puede reproducir una acción con una parada Insert Menu Item… Insertar elemento de menú… Insert Menu Item Insertar elemento de menú + Insert Select Path Insertar selección de trazado + Select Path Seleccionar trazado +@msg select one whole path or compound path selecciona un trazado completo o un trazado compuesto +@msg select only one path selecciona solo un trazado +@msg the path is hidden or locked, or cannot be selected here el trazado está oculto o bloqueado, o no se puede seleccionar aquí +@msg object {value} is not the stored path: it changed, or the document is another one el objeto {value} no es el trazado guardado: ha cambiado o el documento es otro +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` debe ser "v1:" y 16 dígitos hexadecimales en minúscula Menu Item: Elemento de menú: No menu item matches. Ningún elemento de menú coincide. Several menu items match. Choose one from the list. Coinciden varios elementos de menú. Elija uno de la lista. diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index dc0e432264..bf4aaa3cde 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6368,6 +6368,13 @@ axis Y Axis Y-akseli @msg Batch cannot play an action with a stop Erä ei voi toistaa toimintoa, jossa on pysäytys Insert Menu Item… Lisää valikkokohta… Insert Menu Item Lisää valikkokohta + Insert Select Path Lisää polun valinta + Select Path Valitse polku +@msg select one whole path or compound path valitse yksi kokonainen polku tai yhdistelmäpolku +@msg select only one path valitse vain yksi polku +@msg the path is hidden or locked, or cannot be selected here polku on piilotettu tai lukittu, tai sitä ei voi valita tässä +@msg object {value} is not the stored path: it changed, or the document is another one objekti {value} ei ole tallennettu polku: se on muuttunut, tai asiakirja on toinen +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` on oltava "v1:" ja 16 pientä heksadesimaalinumeroa Menu Item: Valikkokohta: No menu item matches. Mikään valikkokohta ei vastaa hakua. Several menu items match. Choose one from the list. Useampi valikkokohta vastaa hakua. Valitse yksi luettelosta. diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 3c74586ef0..d262b1ea5e 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6333,6 +6333,13 @@ axis Y Axis Axe Y @msg Batch cannot play an action with a stop Le traitement par lots ne peut pas exécuter un script comportant un arrêt Insert Menu Item… Insérer une commande de menu… Insert Menu Item Insérer une commande de menu + Insert Select Path Insérer une sélection de tracé + Select Path Sélectionner le tracé +@msg select one whole path or compound path sélectionnez un tracé entier ou un tracé transparent +@msg select only one path sélectionnez un seul tracé +@msg the path is hidden or locked, or cannot be selected here le tracé est masqué ou verrouillé, ou ne peut pas être sélectionné ici +@msg object {value} is not the stored path: it changed, or the document is another one l’objet {value} n’est pas le tracé enregistré : il a été modifié, ou il s’agit d’un autre document +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` doit être "v1:" suivi de 16 chiffres hexadécimaux en minuscules Menu Item: Commande de menu : No menu item matches. Aucune commande de menu ne correspond. Several menu items match. Choose one from the list. Plusieurs commandes de menu correspondent. Choisissez-en une dans la liste. diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 8e1ce7cc2a..a036037049 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -3465,6 +3465,13 @@ antiAlias Sharp शार्प @msg Batch cannot play an action with a stop बैच स्टॉप वाला एक्शन नहीं चला सकता Insert Menu Item… मेनू आइटम जोड़ें… Insert Menu Item मेनू आइटम जोड़ें + Insert Select Path सिलेक्ट पाथ जोड़ें + Select Path पाथ सिलेक्ट करें +@msg select one whole path or compound path एक पूरा पाथ या कंपाउंड पाथ चुनें +@msg select only one path केवल एक पाथ चुनें +@msg the path is hidden or locked, or cannot be selected here पाथ छिपा हुआ है या लॉक है, या यहाँ चुना नहीं जा सकता +@msg object {value} is not the stored path: it changed, or the document is another one ऑब्जेक्ट {value} सहेजा गया पाथ नहीं है: वह बदल गया है, या डॉक्यूमेंट दूसरा है +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` "v1:" और छोटे अक्षरों वाले 16 हेक्स अंक होना चाहिए Menu Item: मेनू आइटम: No menu item matches. कोई मेनू आइटम मेल नहीं खाता। Several menu items match. Choose one from the list. कई मेनू आइटम मेल खाते हैं। सूची से एक चुनें। diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 15ce573c11..2399e21ef1 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6321,6 +6321,13 @@ axis Y Axis Y tengely @msg Batch cannot play an action with a stop A kötegelt feldolgozás nem játszhat le megállást tartalmazó műveletet Insert Menu Item… Menüelem beszúrása… Insert Menu Item Menüelem beszúrása + Insert Select Path Útvonal kijelölésének beszúrása + Select Path Útvonal kijelölése +@msg select one whole path or compound path jelölj ki egy teljes útvonalat vagy összetett útvonalat +@msg select only one path csak egy útvonalat jelölj ki +@msg the path is hidden or locked, or cannot be selected here az útvonal rejtett vagy zárolt, vagy itt nem jelölhető ki +@msg object {value} is not the stored path: it changed, or the document is another one a(z) {value} objektum nem a tárolt útvonal: megváltozott, vagy ez egy másik dokumentum +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits a `fingerprint` értéke "v1:" és 16 kisbetűs hexadecimális számjegy kell legyen Menu Item: Menüelem: No menu item matches. Egy menüelem sem egyezik. Several menu items match. Choose one from the list. Több menüelem is egyezik. Válasszon egyet a listából. diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index f68fad0c1c..c93f40aa19 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -5993,6 +5993,13 @@ axis Y Axis Sumbu Y @msg Batch cannot play an action with a stop Batch tidak dapat memutar tindakan dengan titik henti Insert Menu Item… Sisipkan item menu… Insert Menu Item Sisipkan item menu + Insert Select Path Sisipkan pemilihan jalur + Select Path Pilih jalur +@msg select one whole path or compound path pilih satu jalur utuh atau jalur majemuk +@msg select only one path pilih hanya satu jalur +@msg the path is hidden or locked, or cannot be selected here jalur tersembunyi atau terkunci, atau tidak dapat dipilih di sini +@msg object {value} is not the stored path: it changed, or the document is another one objek {value} bukan jalur yang disimpan: jalur itu berubah, atau dokumennya berbeda +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` harus berupa "v1:" dan 16 digit heksadesimal huruf kecil Menu Item: Item menu: No menu item matches. Tidak ada item menu yang cocok. Several menu items match. Choose one from the list. Beberapa item menu cocok. Pilih salah satu dari daftar. diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 480115859b..6959c1b749 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5963,6 +5963,13 @@ axis Y Axis Asse Y @msg Batch cannot play an action with a stop L'elaborazione batch non può eseguire un'azione con un'interruzione Insert Menu Item… Inserisci voce di menu… Insert Menu Item Inserisci voce di menu + Insert Select Path Inserisci selezione tracciato + Select Path Seleziona tracciato +@msg select one whole path or compound path seleziona un tracciato intero o un tracciato composto +@msg select only one path seleziona un solo tracciato +@msg the path is hidden or locked, or cannot be selected here il tracciato è nascosto o bloccato, oppure non può essere selezionato qui +@msg object {value} is not the stored path: it changed, or the document is another one l’oggetto {value} non è il tracciato memorizzato: è cambiato, oppure il documento è un altro +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` deve essere "v1:" seguito da 16 cifre esadecimali minuscole Menu Item: Voce di menu: No menu item matches. Nessuna voce di menu corrisponde. Several menu items match. Choose one from the list. Corrispondono più voci di menu. Sceglierne una dall’elenco. diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index ba3105aec0..8185f116ac 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -6028,6 +6028,13 @@ axis Y Axis Y 軸 @msg Batch cannot play an action with a stop バッチでは停止を含むアクションを再生できません Insert Menu Item… メニュー項目を挿入… Insert Menu Item メニュー項目を挿入 + Insert Select Path パスの選択を挿入 + Select Path パスを選択 +@msg select one whole path or compound path パス全体または複合パスを 1 つ選択してください +@msg select only one path パスを 1 つだけ選択してください +@msg the path is hidden or locked, or cannot be selected here パスが非表示またはロックされているか、ここでは選択できません +@msg object {value} is not the stored path: it changed, or the document is another one オブジェクト {value} は保存されたパスではありません: 変更されたか、別のドキュメントです +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` は "v1:" と小文字の 16 進数字 16 桁である必要があります Menu Item: メニュー項目: No menu item matches. 一致するメニュー項目がありません。 Several menu items match. Choose one from the list. 複数のメニュー項目が一致します。リストから 1 つ選択してください。 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 83fadf726a..06891d0a79 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -6289,6 +6289,13 @@ axis Y Axis Y ღერძი @msg Batch cannot play an action with a stop პაკეტურ დამუშავებას არ შეუძლია გაჩერების მქონე მოქმედების შესრულება Insert Menu Item… მენიუს ელემენტის ჩასმა… Insert Menu Item მენიუს ელემენტის ჩასმა + Insert Select Path ტრაექტორიის მონიშვნის ჩასმა + Select Path ტრაექტორიის მონიშვნა +@msg select one whole path or compound path მონიშნეთ ერთი მთლიანი ტრაექტორია ან შედგენილი ტრაექტორია +@msg select only one path მონიშნეთ მხოლოდ ერთი ტრაექტორია +@msg the path is hidden or locked, or cannot be selected here ტრაექტორია დამალული ან ჩაკეტილია, ან აქ ვერ მოინიშნება +@msg object {value} is not the stored path: it changed, or the document is another one ობიექტი {value} არ არის შენახული ტრაექტორია: ის შეიცვალა, ან დოკუმენტი სხვაა +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` უნდა იყოს "v1:" და 16 პატარა თექვსმეტობითი ციფრი Menu Item: მენიუს ელემენტი: No menu item matches. მენიუს არცერთი ელემენტი არ ემთხვევა. Several menu items match. Choose one from the list. ემთხვევა მენიუს რამდენიმე ელემენტი. აირჩიეთ ერთი სიიდან. diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index e6f7478bf3..cce862e60f 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5924,6 +5924,13 @@ axis Y Axis Y축 @msg Batch cannot play an action with a stop 일괄 처리에서는 중지가 있는 액션을 재생할 수 없습니다 Insert Menu Item… 메뉴 항목 삽입… Insert Menu Item 메뉴 항목 삽입 + Insert Select Path 패스 선택 삽입 + Select Path 패스 선택 +@msg select one whole path or compound path 전체 패스 또는 컴파운드 패스를 하나 선택하세요 +@msg select only one path 패스를 하나만 선택하세요 +@msg the path is hidden or locked, or cannot be selected here 패스가 숨겨져 있거나 잠겨 있거나 여기에서 선택할 수 없습니다 +@msg object {value} is not the stored path: it changed, or the document is another one 개체 {value}은(는) 저장된 패스가 아닙니다: 변경되었거나 다른 문서입니다 +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint`는 "v1:"과 소문자 16진수 16자리여야 합니다 Menu Item: 메뉴 항목: No menu item matches. 일치하는 메뉴 항목이 없습니다. Several menu items match. Choose one from the list. 여러 메뉴 항목이 일치합니다. 목록에서 하나를 선택하세요. diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 26baaa8dc5..fdbfb2cb88 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -6044,6 +6044,13 @@ axis Y Axis Y-as @msg Batch cannot play an action with a stop Batch kan geen actie met een stop afspelen Insert Menu Item… Menu-item invoegen… Insert Menu Item Menu-item invoegen + Insert Select Path Pad selecteren invoegen + Select Path Pad selecteren +@msg select one whole path or compound path selecteer één volledig pad of samengesteld pad +@msg select only one path selecteer slechts één pad +@msg the path is hidden or locked, or cannot be selected here het pad is verborgen of vergrendeld, of kan hier niet worden geselecteerd +@msg object {value} is not the stored path: it changed, or the document is another one object {value} is niet het opgeslagen pad: het is gewijzigd, of het document is een ander +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` moet "v1:" zijn, gevolgd door 16 hexadecimale cijfers in kleine letters Menu Item: Menu-item: No menu item matches. Geen menu-item komt overeen. Several menu items match. Choose one from the list. Meerdere menu-items komen overeen. Kies er een uit de lijst. diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 3637bb6f94..f9e5844fb9 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -6192,6 +6192,13 @@ axis Y Axis Oś Y @msg Batch cannot play an action with a stop Przetwarzanie wsadowe nie może odtworzyć operacji z zatrzymaniem Insert Menu Item… Wstaw polecenie menu… Insert Menu Item Wstaw polecenie menu + Insert Select Path Wstaw zaznaczenie ścieżki + Select Path Zaznacz ścieżkę +@msg select one whole path or compound path zaznacz jedną całą ścieżkę lub ścieżkę złożoną +@msg select only one path zaznacz tylko jedną ścieżkę +@msg the path is hidden or locked, or cannot be selected here ścieżka jest ukryta lub zablokowana albo nie można jej tu zaznaczyć +@msg object {value} is not the stored path: it changed, or the document is another one obiekt {value} nie jest zapisaną ścieżką: zmienił się albo dokument jest inny +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` musi mieć postać "v1:" i 16 cyfr szesnastkowych małymi literami Menu Item: Polecenie menu: No menu item matches. Żadne polecenie menu nie pasuje. Several menu items match. Choose one from the list. Pasuje kilka poleceń menu. Wybierz jedno z listy. diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 15aa70e244..168907a0c8 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -6253,6 +6253,13 @@ axis Y Axis Eixo Y @msg Batch cannot play an action with a stop Lote não pode reproduzir uma ação com uma parada Insert Menu Item… Inserir Item de Menu… Insert Menu Item Inserir Item de Menu + Insert Select Path Inserir Seleção de Caminho + Select Path Selecionar Caminho +@msg select one whole path or compound path selecione um caminho inteiro ou um caminho composto +@msg select only one path selecione apenas um caminho +@msg the path is hidden or locked, or cannot be selected here o caminho está oculto ou bloqueado, ou não pode ser selecionado aqui +@msg object {value} is not the stored path: it changed, or the document is another one o objeto {value} não é o caminho armazenado: ele mudou, ou o documento é outro +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` deve ser "v1:" e 16 dígitos hexadecimais minúsculos Menu Item: Item de menu: No menu item matches. Nenhum item de menu corresponde. Several menu items match. Choose one from the list. Vários itens de menu correspondem. Escolha um da lista. diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 21f270a0fb..379598b34c 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -5949,6 +5949,13 @@ axis Y Axis Ось Y @msg Batch cannot play an action with a stop Пакет не может воспроизвести действие с остановкой Insert Menu Item… Вставить пункт меню… Insert Menu Item Вставить пункт меню + Insert Select Path Вставить выделение контура + Select Path Выделить контур +@msg select one whole path or compound path выделите один целый контур или составной контур +@msg select only one path выделите только один контур +@msg the path is hidden or locked, or cannot be selected here контур скрыт или заблокирован либо не может быть выделен здесь +@msg object {value} is not the stored path: it changed, or the document is another one объект {value} не является сохранённым контуром: он изменился или это другой документ +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` должен состоять из "v1:" и 16 шестнадцатеричных цифр в нижнем регистре Menu Item: Пункт меню: No menu item matches. Нет подходящих пунктов меню. Several menu items match. Choose one from the list. Подходит несколько пунктов меню. Выберите один из списка. diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 7969960880..47e8ed6640 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -6319,6 +6319,13 @@ axis Y Axis Y-axel @msg Batch cannot play an action with a stop Batch kan inte spela upp en åtgärd med ett stopp Insert Menu Item… Infoga menyalternativ… Insert Menu Item Infoga menyalternativ + Insert Select Path Infoga markering av bana + Select Path Markera bana +@msg select one whole path or compound path markera en hel bana eller sammansatt bana +@msg select only one path markera bara en bana +@msg the path is hidden or locked, or cannot be selected here banan är dold eller låst, eller kan inte markeras här +@msg object {value} is not the stored path: it changed, or the document is another one objekt {value} är inte den sparade banan: den har ändrats, eller så är dokumentet ett annat +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` måste vara "v1:" och 16 hexadecimala siffror med gemener Menu Item: Menyalternativ: No menu item matches. Inget menyalternativ matchar. Several menu items match. Choose one from the list. Flera menyalternativ matchar. Välj ett i listan. diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 639bf692a8..9e67b91ff4 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -6294,6 +6294,13 @@ axis Y Axis Y ekseni @msg Batch cannot play an action with a stop Toplu işlem durak içeren bir eylemi oynatamaz Insert Menu Item… Menü Öğesi Ekle… Insert Menu Item Menü Öğesi Ekle + Insert Select Path Yol Seçimi Ekle + Select Path Yolu Seç +@msg select one whole path or compound path bir yolun tamamını veya bir bileşik yolu seçin +@msg select only one path yalnızca bir yol seçin +@msg the path is hidden or locked, or cannot be selected here yol gizli veya kilitli ya da burada seçilemiyor +@msg object {value} is not the stored path: it changed, or the document is another one {value} nesnesi kaydedilen yol değil: değişti ya da belge başka +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` "v1:" ve 16 küçük harfli onaltılık basamak olmalıdır Menu Item: Menü öğesi: No menu item matches. Eşleşen menü öğesi yok. Several menu items match. Choose one from the list. Birden çok menü öğesi eşleşiyor. Listeden birini seçin. diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 2889757941..4eec319785 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5943,6 +5943,13 @@ axis Y Axis Вісь Y @msg Batch cannot play an action with a stop Пакет не може відтворити дію із зупинкою Insert Menu Item… Вставити пункт меню… Insert Menu Item Вставити пункт меню + Insert Select Path Вставити виділення контуру + Select Path Виділити контур +@msg select one whole path or compound path виділіть один цілий контур або складений контур +@msg select only one path виділіть лише один контур +@msg the path is hidden or locked, or cannot be selected here контур прихований або заблокований, або його не можна виділити тут +@msg object {value} is not the stored path: it changed, or the document is another one об'єкт {value} не є збереженим контуром: його змінено або це інший документ +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` має складатися з "v1:" і 16 шістнадцяткових цифр у нижньому регістрі Menu Item: Пункт меню: No menu item matches. Немає відповідних пунктів меню. Several menu items match. Choose one from the list. Відповідає кілька пунктів меню. Виберіть один зі списку. diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index ca39a2cd16..6b72631716 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5944,6 +5944,13 @@ axis Y Axis Trục Y @msg Batch cannot play an action with a stop Hàng loạt không thể phát hành động có điểm dừng Insert Menu Item… Chèn mục menu… Insert Menu Item Chèn mục menu + Insert Select Path Chèn chọn đường dẫn + Select Path Chọn đường dẫn +@msg select one whole path or compound path hãy chọn một đường dẫn trọn vẹn hoặc đường dẫn phức +@msg select only one path chỉ chọn một đường dẫn +@msg the path is hidden or locked, or cannot be selected here đường dẫn bị ẩn hoặc bị khóa, hoặc không thể chọn ở đây +@msg object {value} is not the stored path: it changed, or the document is another one đối tượng {value} không phải là đường dẫn đã lưu: nó đã thay đổi, hoặc đây là tài liệu khác +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` phải là "v1:" và 16 chữ số thập lục phân viết thường Menu Item: Mục menu: No menu item matches. Không có mục menu nào khớp. Several menu items match. Choose one from the list. Có nhiều mục menu khớp. Hãy chọn một mục trong danh sách. diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index d4fe4ce4f3..6ab859d646 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -6176,6 +6176,13 @@ axis Y Axis Y 轴 @msg Batch cannot play an action with a stop 批处理无法播放含有停止的动作 Insert Menu Item… 插入菜单项… Insert Menu Item 插入菜单项 + Insert Select Path 插入选择路径 + Select Path 选择路径 +@msg select one whole path or compound path 请选择一条完整路径或复合路径 +@msg select only one path 请只选择一条路径 +@msg the path is hidden or locked, or cannot be selected here 该路径已隐藏或锁定,或无法在此处选择 +@msg object {value} is not the stored path: it changed, or the document is another one 对象 {value} 不是存储的路径:它已更改,或这是另一个文档 +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` 必须为 "v1:" 加 16 位小写十六进制数字 Menu Item: 菜单项: No menu item matches. 没有匹配的菜单项。 Several menu items match. Choose one from the list. 有多个菜单项匹配。请从列表中选择一个。 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 1a81d6d01c..6f86b74438 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5981,6 +5981,13 @@ axis Y Axis Y 軸 @msg Batch cannot play an action with a stop 批次無法播放含有停止的動作 Insert Menu Item… 插入選單項目… Insert Menu Item 插入選單項目 + Insert Select Path 插入選取路徑 + Select Path 選取路徑 +@msg select one whole path or compound path 請選取一條完整路徑或複合路徑 +@msg select only one path 請只選取一條路徑 +@msg the path is hidden or locked, or cannot be selected here 該路徑已隱藏或鎖定,或無法在此處選取 +@msg object {value} is not the stored path: it changed, or the document is another one 物件 {value} 不是儲存的路徑:它已變更,或這是另一份文件 +@msg `fingerprint` must be "v1:" and 16 lowercase hex digits `fingerprint` 必須為 "v1:" 加上 16 位小寫十六進位數字 Menu Item: 選單項目: No menu item matches. 沒有相符的選單項目。 Several menu items match. Choose one from the list. 有多個選單項目相符。請從清單中選擇一個。 diff --git a/crates/ui-egui/src/menus/run.rs b/crates/ui-egui/src/menus/run.rs index 72e6c25acb..dfb88c880c 100644 --- a/crates/ui-egui/src/menus/run.rs +++ b/crates/ui-egui/src/menus/run.rs @@ -978,6 +978,7 @@ fn run_ui_part11(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option crate::panels::actions::stop(app, p), crate::panels::actions::INSERT_STOP => crate::panels::actions::insert_stop(app, p), crate::panels::actions::INSERT_MENU_ITEM => crate::panels::actions::insert_menu_item(app, p), + crate::panels::actions::INSERT_SELECT_PATH => crate::panels::actions::insert_select_path(app, p), crate::panels::actions::CONTINUE_PLAYBACK => crate::panels::actions::continue_playback(app, p), crate::panels::actions::DELETE_STEP => crate::panels::actions::delete_step(app, p), "ui.batchDialog" => { diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index b0b2db020d..e2a07ee680 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -1434,6 +1434,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "", "{set?: index, action?: index, step?: index, command, params?: object} insert the command of a menu item as a step, without running it; the step plays as choosing the item runs it. command and params (default {}) must be those of a menu item that can be a step: one that runs an engine command, whose label does not end with \"…\", outside the VectorCraft, File, Window and Help menus, and that is not Undo, Redo, a Paste, a batch (command.batch), a preference (prefs.*) or action (action.*) command, an Object › Align item, Image Trace's Make or Make and Expand, Pattern's Make or Edit Pattern, a saved selection, saved view or recent font item, or a plug-in's item. The place is as ui.action.insertStop takes it: with set and action, after step, else after the action's last step; without them, the recording in progress. The new step becomes the panel's selected step, and its action and set become the selected ones. A command and params that no such menu item has, a param of another type, params that are not an object, no action and no recording, or a step that would give the action more than 10,000 steps or its set's file more than 8 MiB, counting the steps that the recording in progress adds to the set, is an error and changes nothing. The panel menu's Insert Menu Item… (dialog `actionInsertMenuItem` with find, command, params, set, action and step) runs this on OK → {set, action, step}, or {recording: true}", ), + ( + crate::panels::actions::INSERT_SELECT_PATH, + "Insert Select Path", + "", + "{set?: index, action?: index, step?: index} insert a Select Path step for the path selected in the active document, without changing the selection: the step `select.storedPath {id, fingerprint}` stores the path's id and a fingerprint of its kind, fill rule and anchors, and playback selects only that path while it is still there unchanged and can be selected, else the step is an error. The selection must be one whole path or compound path that can be selected: not anchors or segments alone, several objects, a group, type or another kind of object, a guide, a member of a compound path, or a hidden or locked object. The place is as ui.action.insertStop takes it: with set and action, after step, else after the action's last step; without them, the recording in progress. The new step becomes the panel's selected step, and its action and set become the selected ones. No document, a selection that is not such a path, a param of another type, params that are not an object, no action and no recording, or a step that would give the action more than 10,000 steps or its set's file more than 8 MiB, counting the steps that the recording in progress adds to the set, is an error and changes nothing. The panel menu's Insert Select Path runs this, for the selected step or action or the recording in progress → {set, action, step}, or {recording: true}", + ), ( crate::panels::actions::CONTINUE_PLAYBACK, "Continue Action", diff --git a/crates/ui-egui/src/panels/actions.rs b/crates/ui-egui/src/panels/actions.rs index f2426f33ab..1ac205c2c7 100644 --- a/crates/ui-egui/src/panels/actions.rs +++ b/crates/ui-egui/src/panels/actions.rs @@ -948,6 +948,31 @@ pub fn insert_menu_item(app: &mut VectorcraftApp, p: &Value) -> Result Result { + if !p.is_object() { + return Err("`params` must be an object".into()); + } + let st = app.session.active().ok_or("no document open")?; + let params = vectorcraft_engine::stored_path_step(st)?; + insert(app, p, (SELECT_PATH_STEP.to_string(), params)) +} + /// The journal entry the recording in progress records from. fn recording_start(app: &VectorcraftApp) -> Option { let (_, _, start) = app.ui.recording.as_ref()?; @@ -1706,6 +1731,17 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut egui::Ui) { let step = selected_step(app, ui.ctx(), selected); crate::dialogs::action_insert_menu_item::open(app, if recording { None } else { selected }, step); } + let path = app.session.active().is_some_and(|st| vectorcraft_engine::storable_selection(st).is_ok()); + if widgets::menu_item(ui, tl!("Insert Select Path"), (recording || has) && path, false) { + let place = match (recording, selected, selected_step(app, ui.ctx(), selected)) { + (true, ..) | (false, None, _) => json!({}), + (false, Some((s, a)), None) => json!({"set": s, "action": a}), + (false, Some((s, a)), Some(step)) => json!({"set": s, "action": a, "step": step}), + }; + if let Err(e) = app.run(INSERT_SELECT_PATH, place) { + app.status(e); + } + } if widgets::menu_item(ui, tl!("Action Options…"), has, false) && let Some((s, a)) = selected { @@ -1892,10 +1928,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) { current = Some(si); sets_selected = vec![si]; } - // A stop's row shows its message; other steps show their command. + // A stop's row shows its message, and a Select Path step's its name; other + // steps show their command. let shown = if command == STOP_STEP { let message = params.get("message").and_then(Value::as_str).and_then(|m| m.lines().next()).unwrap_or_default(); crate::i18n::fmt(tl!("Stop: {message}"), &[("message", message)]) + } else if command == SELECT_PATH_STEP { + tl!("Select Path").to_string() } else { command.clone() }; @@ -3087,7 +3126,7 @@ mod tests { (json!(long.to_string()), "an action in the action set file has more than 10,000 steps"), (json!(" ".repeat(files::MAX_SET_FILE + 1)), "the action set file is larger than 8 MiB"), ( - json!("{\"format\": \"vcactions\", \"version\": 4, \"name\": \"S\", \"actions\": []}"), + json!("{\"format\": \"vcactions\", \"version\": 5, \"name\": \"S\", \"actions\": []}"), "the action set file needs a later version of VectorCraft", ), ] { @@ -4474,6 +4513,175 @@ mod tests { assert_eq!(recorded.steps, [("object.group".to_string(), json!({}))]); } + /// [`app_with_three_moves`] with a second rectangle, 100 pt to the right, and the first one + /// selected again → (the app, the left edge of the first one as it moves, the ids of both). + fn app_with_two_rectangles() -> (VectorcraftApp, impl Fn(&VectorcraftApp) -> f64, [u64; 2]) { + let (mut app, x) = app_with_three_moves(); + let first = app.session.active().unwrap().selection.objects[0].0; + let second = app.run("shape.rectangle", json!({"x": 110, "y": 10, "width": 20, "height": 20})).unwrap()["id"].as_u64().unwrap(); + app.run("select.set", json!({"ids": [first]})).unwrap(); + (app, x, [first, second]) + } + + fn left_of(app: &VectorcraftApp, id: u64) -> f64 { + app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().geometric_bounds().unwrap().x0 + } + + #[test] + fn insert_select_path_stores_the_selected_path_changes_nothing_and_plays_by_selecting_it() { + let (mut app, x, [first, second]) = app_with_two_rectangles(); + app.ui.action_sets[0].actions[0].excluded_steps = BTreeSet::from([0, 2]); + let doc = app.session.active().unwrap().doc.clone(); + let journal = app.session.journal.len(); + let undo = undo_steps(&app); + let r = app.run(INSERT_SELECT_PATH, json!({"set": 0, "action": 0, "step": 0})).unwrap(); + assert_eq!(r, json!({"set": 0, "action": 0, "step": 1})); + assert!(std::sync::Arc::ptr_eq(&app.session.active().unwrap().doc, &doc), "inserting edits nothing"); + assert_eq!(app.session.journal.len(), journal, "and records nothing"); + assert_eq!(undo_steps(&app), undo, "and adds no undo step"); + let action = &app.ui.action_sets[0].actions[0]; + assert_eq!(action.steps[1].0, SELECT_PATH_STEP); + assert_eq!(action.steps[1].1["id"], json!(first)); + assert!(action.steps[1].1["fingerprint"].as_str().is_some_and(|f| f.starts_with("v1:")), "{:?}", action.steps[1]); + assert_eq!(action.excluded_steps, BTreeSet::from([0, 3]), "the step excluded after the insertion point moved with it"); + assert!(!action.excluded_steps.contains(&1), "the new step is included"); + + app.run("select.set", json!({"ids": [second]})).unwrap(); + let before = undo_steps(&app); + play(&mut app, 0, 0).unwrap(); + assert_eq!(app.session.active().unwrap().selection.objects, [vectorcraft_doc::NodeId(first)], "the step selected the stored path"); + assert_eq!(x(&app), 2.0, "the move after it moved the stored path"); + assert_eq!(left_of(&app, second), 110.0, "and not the path selected before"); + assert_eq!(undo_steps(&app), before + 1, "the action plays as one undo step"); + } + + #[test] + fn a_select_path_step_plays_step_by_step_and_a_changed_path_ends_the_playback() { + let (mut app, x, [_, second]) = app_with_two_rectangles(); + app.ui.action_sets[0].actions[0].steps.truncate(1); + app.run(INSERT_SELECT_PATH, json!({"set": 0, "action": 0})).unwrap(); + app.ui.action_sets[0].actions[0].steps.push(("object.move".into(), json!({"dx": 4, "dy": 0}))); + app.run(PLAYBACK_OPTIONS, json!({"speed": "step"})).unwrap(); + let before = undo_steps(&app); + app.run("select.set", json!({"ids": [second]})).unwrap(); + assert_eq!(undo_steps(&app), before, "a selection change alone is no undo step"); + play(&mut app, 0, 0).unwrap(); + play_out(&mut app); + assert_eq!(left_of(&app, second), 111.0, "the first move moved the path selected when the action started"); + assert_eq!(x(&app), 4.0, "the step selected the stored path, and the last move moved it"); + assert_eq!(undo_steps(&app), before + 2, "each move its own undo step, and the selection change none, as select.set adds none"); + + // The stored path has moved since: the step ends the playback, and the move after it does not run. + app.run("select.set", json!({"ids": [second]})).unwrap(); + play(&mut app, 0, 0).unwrap(); + play_out(&mut app); + assert!(app.ui.status.contains("is not the stored path"), "{}", app.ui.status); + assert_eq!(app.session.active().unwrap().selection.objects, [vectorcraft_doc::NodeId(second)], "the selection stays as it was"); + assert_eq!((left_of(&app, second), x(&app)), (112.0, 4.0), "the step before it ran, the one after it did not"); + } + + #[test] + fn insert_select_path_refuses_what_is_not_one_whole_path_and_changes_nothing() { + let (mut app, _, [first, second]) = app_with_two_rectangles(); + let place = json!({"set": 0, "action": 0}); + let refused = |app: &mut VectorcraftApp, params: Value, want: &str| { + let selection = app.session.active().map(|st| st.selection.objects.clone()); + assert_eq!(app.run(INSERT_SELECT_PATH, params.clone()).unwrap_err(), want, "{params}"); + assert_eq!(app.ui.action_sets[0].actions[0].steps.len(), 3, "{params}"); + assert_eq!(app.session.active().map(|st| st.selection.objects.clone()), selection, "{params}"); + }; + app.run("select.set", json!({"ids": [first, second]})).unwrap(); + refused(&mut app, place.clone(), "select only one path"); + app.run("select.none", json!({})).unwrap(); + refused(&mut app, place.clone(), "select a path"); + app.run("select.anchors", json!({"id": first, "anchors": [[0, 0]], "mode": "set"})).unwrap(); + refused(&mut app, place.clone(), "select one whole path or compound path"); + app.run("select.set", json!({"ids": [first, second]})).unwrap(); + let group = app.run("object.group", json!({})).unwrap()["id"].as_u64().unwrap(); + app.run("select.set", json!({"ids": [group]})).unwrap(); + refused(&mut app, place.clone(), "select one whole path or compound path"); + app.run("select.set", json!({"ids": [first]})).unwrap(); + for (params, want) in [ + (json!({}), "no such action"), + (json!({"set": 0}), "no such action"), + (json!({"set": 1, "action": 0}), "no such action"), + (json!({"set": 0, "action": 1}), "no such action"), + (json!({"set": "0", "action": 0}), "no such action"), + (json!({"set": 0, "action": 0, "step": 3}), "no such action step"), + (json!({"set": 0, "action": 0, "step": null}), "no such action step"), + (json!(5), "`params` must be an object"), + (json!(null), "`params` must be an object"), + ] { + refused(&mut app, params, want); + } + let mut empty = app_with_sets(); + assert_eq!(empty.run(INSERT_SELECT_PATH, json!({"set": 0, "action": 0})).unwrap_err(), "no document open"); + } + + #[test] + fn insert_select_path_goes_into_the_recording() { + let (mut app, _, [first, _]) = app_with_two_rectangles(); + app.run(START_RECORDING, json!({})).unwrap(); + assert_eq!(app.run(INSERT_SELECT_PATH, json!({})).unwrap(), json!({"recording": true})); + assert_eq!(app.session.active().unwrap().selection.objects, [vectorcraft_doc::NodeId(first)]); + app.run(STOP_RECORDING, json!({})).unwrap(); + let recorded = app.ui.action_sets[0].actions.last().unwrap(); + assert_eq!(recorded.steps.len(), 1); + assert_eq!((recorded.steps[0].0.as_str(), &recorded.steps[0].1["id"]), (SELECT_PATH_STEP, &json!(first))); + } + + #[test] + fn a_saved_select_path_step_survives_and_a_damaged_one_is_refused() { + let (mut app, _, _) = app_with_two_rectangles(); + app.run(INSERT_SELECT_PATH, json!({"set": 0, "action": 0, "step": 0})).unwrap(); + let saved = files::write_set(&app.ui.action_sets[0]); + assert_eq!(serde_json::from_str::(&saved).unwrap()["version"], json!(4), "older builds must refuse a Select Path step"); + let loaded = files::read_set(&saved, "Test").unwrap(); + assert_eq!(loaded.actions[0].steps, app.ui.action_sets[0].actions[0].steps); + let step = app.ui.action_sets[0].actions[0].steps[1].clone(); + let nested = json!({"commands": [{"command": SELECT_PATH_STEP, "params": step.1}]}); + let set = ActionSet::new("Nested", vec![Action::new("A", vec![("command.batch".into(), nested)])]); + assert_eq!(serde_json::from_str::(&files::write_set(&set)).unwrap()["version"], json!(4), "a step in a recorded batch counts"); + for params in + [json!({"id": 3}), json!({"id": -1, "fingerprint": step.1["fingerprint"]}), json!({"id": 3, "fingerprint": "v1:xyz"}), json!(42)] + { + let bad = json!({"command": SELECT_PATH_STEP, "params": params}); + for s in [bad.clone(), json!({"command": "command.batch", "params": {"commands": [bad]}})] { + let file = json!({"format": "vcactions", "version": 4, "name": "Bad", "actions": [{"name": "A", "steps": [s]}]}); + assert_eq!(files::read_set(&file.to_string(), "Test").unwrap_err(), "not an action set file", "{s}"); + } + } + } + + #[test] + fn insert_select_path_in_the_menu_needs_a_path_and_shows_its_row() { + use super::super::tests_appearance::{click, frame_events, text_rect}; + let (mut app, _, [first, _]) = app_with_two_rectangles(); + app.ui.action_set = Some(0); + let ctx = egui::Context::default(); + app.ui_ctx = Some(ctx.clone()); + let menu_click = |ctx: &egui::Context, app: &mut VectorcraftApp| { + let texts = frame_events(ctx, app, vec![], menu); + let item = texts.iter().find(|(s, _)| s.trim() == "Insert Select Path").map(|(_, r)| *r).expect("Insert Select Path in the menu"); + click(ctx, app, item.center(), menu); + }; + menu_click(&ctx, &mut app); + assert_eq!(app.ui.action_sets[0].actions[0].steps.len(), 3, "no action selected and nothing recording: the item is off"); + + let texts = frame_events(&ctx, &mut app, vec![], show); + click(&ctx, &mut app, text_rect(&texts, "Three Moves").center(), show); + app.run("select.none", json!({})).unwrap(); + menu_click(&ctx, &mut app); + assert_eq!(app.ui.action_sets[0].actions[0].steps.len(), 3, "no path selected: the item is off"); + + app.run("select.set", json!({"ids": [first]})).unwrap(); + menu_click(&ctx, &mut app); + let steps = &app.ui.action_sets[0].actions[0].steps; + assert_eq!((steps.len(), steps[3].0.as_str()), (4, SELECT_PATH_STEP), "the action selected: the step goes at its end"); + let texts = frame_events(&ctx, &mut app, vec![], show); + assert!(texts.iter().any(|(s, _)| s == "Select Path"), "the step's row names it: {texts:?}"); + } + /// An action stays one that Load Actions… reads back: a stop or a menu item that would give it /// more than 10,000 steps, or its set's file more than 8 MiB, is refused, and the action keeps its /// steps and exclusions as they were. @@ -5520,7 +5728,7 @@ mod tests { json!({"format": "vcactions", "version": version, "name": "Old", "actions": [{"name": "A", "steps": [{"command": "object.move"}]}]}); assert_eq!(files::read_set(&old.to_string(), "Test").unwrap().actions[0].steps.len(), 1, "version {version}"); } - let later = json!({"format": "vcactions", "version": 4, "name": "New", "actions": []}); + let later = json!({"format": "vcactions", "version": 5, "name": "New", "actions": []}); assert_eq!(files::read_set(&later.to_string(), "Test").unwrap_err(), "the action set file needs a later version of VectorCraft"); for params in [json!({"message": 5}), json!({"allowContinue": "yes"}), json!(42)] { let stop = json!({"command": "action.stop", "params": params}); diff --git a/crates/ui-egui/src/panels/actions/files.rs b/crates/ui-egui/src/panels/actions/files.rs index fd8b40fbd7..b48c5e114c 100644 --- a/crates/ui-egui/src/panels/actions/files.rs +++ b/crates/ui-egui/src/panels/actions/files.rs @@ -1,14 +1,16 @@ //! Action set files: the Actions panel menu's Save Actions… and Load Actions…. //! -//! An action set file (`.vcactions`) is JSON: `{"format": "vcactions", "version": 1, 2 or 3, "name", +//! An action set file (`.vcactions`) is JSON: `{"format": "vcactions", "version": 1, 2, 3 or 4, "name", //! "actions": [{"name", "steps": [{"command", "params", "excluded"?}], "key"?, "color"?}]}`. Each step is a //! command id and its params, as `command.batch` takes them, or a stop: the command `action.stop` -//! with `{"message", "allowContinue"}`. `key` is the action's function key +//! with `{"message", "allowContinue"}`. A Select Path step is the command `select.storedPath` with +//! `{"id", "fingerprint"}`. `key` is the action's function key //! (`{"key": 1 to 15, "cmd", "shift"}`) and `color` its Button Mode color; an action without them, //! or with a value that is not one, has none. `excluded: true` keeps a step but skips it during //! playback. Sets with exclusions use version 2 so older builds refuse them rather than replay -//! excluded steps, and sets with stops use version 3 so older builds refuse them rather than fail -//! at a stop. Other sets use version 1, and earlier version 1 and 2 files still work. +//! excluded steps, sets with stops use version 3 so older builds refuse them rather than fail at a +//! stop, and sets with Select Path steps use version 4 so older builds refuse them rather than fail +//! at the step. Other sets use version 1, and earlier version 1, 2 and 3 files still work. //! Load Actions… also reads the action sets another app saves as text (`.aia`, through //! `actionSet.read`). @@ -132,6 +134,20 @@ fn stops_in<'a>(command: &str, params: &'a Value) -> Vec<&'a Value> { steps.filter(|step| super::is_stop(step)).map(|step| step.get("params").unwrap_or(&Value::Null)).collect() } +/// The params of the Select Path steps in a step: a Select Path step's own, or those of the Select +/// Path steps a `command.batch` step holds. +fn select_paths_in<'a>(command: &str, params: &'a Value) -> Vec<&'a Value> { + if command == super::SELECT_PATH_STEP { + return vec![params]; + } + if command != "command.batch" { + return vec![]; + } + let steps = params.get("commands").and_then(Value::as_array).into_iter().flatten(); + let select_path = |step: &&Value| step.get("command").and_then(Value::as_str) == Some(super::SELECT_PATH_STEP); + steps.filter(select_path).map(|step| step.get("params").unwrap_or(&Value::Null)).collect() +} + /// `set` as the text of an action set file. pub fn write_set(set: &ActionSet) -> String { let actions = set @@ -150,10 +166,12 @@ pub fn write_set(set: &ActionSet) -> String { }) .collect(); let has_stop = set.actions.iter().flat_map(|a| &a.steps).any(|(command, params)| !stops_in(command, params).is_empty()); - let version = match (has_stop, set.actions.iter().any(|a| !a.excluded_steps.is_empty())) { - (true, _) => 3, - (false, true) => 2, - (false, false) => 1, + let has_select_path = set.actions.iter().flat_map(|a| &a.steps).any(|(command, params)| !select_paths_in(command, params).is_empty()); + let version = match (has_select_path, has_stop, set.actions.iter().any(|a| !a.excluded_steps.is_empty())) { + (true, ..) => 4, + (false, true, _) => 3, + (false, false, true) => 2, + (false, false, false) => 1, }; let file = SetFile { format: ACTIONS_EXT.into(), version, name: set.name.clone(), actions }; serde_json::to_string_pretty(&file).unwrap_or_default() @@ -168,7 +186,7 @@ pub fn read_set(text: &str, name: &str) -> Result { if f.format != ACTIONS_EXT { return Err("not an action set file".into()); } - if !matches!(f.version, 1..=3) { + if !matches!(f.version, 1..=4) { return Err("the action set file needs a later version of VectorCraft".into()); } if f.actions.len() > MAX_SET_ACTIONS { @@ -181,6 +199,10 @@ pub fn read_set(text: &str, name: &str) -> Result { if stops.any(|p| super::stop_params(p).is_err()) { return Err("not an action set file".into()); } + let mut select_paths = f.actions.iter().flat_map(|a| &a.steps).flat_map(|s| select_paths_in(&s.command, &s.params)); + if select_paths.any(|p| vectorcraft_engine::stored_path_params(p).is_err()) { + return Err("not an action set file".into()); + } let actions = f .actions .into_iter() diff --git a/docs/mcp.md b/docs/mcp.md index 0a6d0cf4b9..81197ca955 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3070,6 +3070,26 @@ already walks Image Trace. Short agent entry points: `actionInsertMenuItem` dialog (`find`, `command`, `params`, `set`, `action`, `step`): typing in Find and pressing Find picks the one item whose menu path, shown name or command id holds every word typed, or lists the matches, and OK runs this. +- Actions: `ui.action.insertSelectPath {set?, action?, step?}` inserts a Select Path step for the path selected in the + active document, without changing the selection or the art. The step is `select.storedPath {id, fingerprint}`: the + path's id and a fingerprint of its kind, fill rule and anchors (`v1:` and 16 lowercase hex digits, FNV-1a over them). + The selection must be one whole path or compound path that can be selected. Anchors or segments of a path alone, + several objects, a path with selected ruler guides, a group, type or any other object, a guide, a member of a compound + path, and a hidden or locked object are refused. A path inside a group can be stored on its own. The place, the selected step and the result are + as for `ui.action.insertStop`. No document, a selection that is not such a path, params that are not an object, an + index past the last set, action or step, no action and no recording, or a step past the 10,000-step and 8 MiB limits + is an error, and then nothing changes. The panel menu's Insert Select Path runs this for the selected step or action, + or the recording in progress, and is off while the selection is not such a path. The step's row in the panel reads + Select Path. An action set file holding the step is version 4, and Load Actions… refuses a step whose params are not + an id and such a fingerprint. +- Actions: `select.storedPath {id, fingerprint}` selects only the path `id` while its fingerprint is still + `fingerprint` and it can be selected: not hidden or locked, nor in a hidden or locked layer or group, a template + layer or on a locked artboard, and inside the isolated object in isolation mode. A missing object, another kind of + object, a path whose kind, fill rule or anchors changed (moving it changes its anchors), or one that cannot be selected + is an error, and the selection stays as it was. So a Select Path step finds its path in the document it was stored + from, and in another document only an object with the same id and the same fingerprint. A selection change alone adds + no undo step. In an Accelerated playback the error undoes the steps before it, and at Step by Step and Pause For it + ends the playback after them. Returns `{count, ids}`. These rules are VectorCraft's own. - Actions: `ui.action.continue {}` plays on from the stop the playing action waits at, on the document active now, and closes the stop's message box (`actionStopMessage`). The next step runs at once, or at Accelerated the steps up to the next stop as one undo step. It returns `{results, pending, atStop?}`. With no action waiting at a stop it is an diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 68b93ac4d8..6850ad0ba9 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -741,6 +741,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Actions panel: the panel menu's Playback Options… sets how actions play: Accelerated (the default) plays every step at once, Step by Step plays one step at a time with the canvas drawn after each, and Pause For waits the given number of seconds between steps ([Specify playback speed](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/specify-playback-speed.html); `ui.action.playbackOptions`). VectorCraft's own rules, which the page does not cover: the pause is a whole number of seconds from 1 to 60, the setting is kept with the UI preferences, a paced playback ends at Stop Playing/Recording or when a step comes due while another document is active, each step of a paced playback is its own undo step, and Batch… plays at full speed. - Actions panel: the panel menu's Insert Stop… opens Record Stop, with a Message box and Allow Continue, and adds a stop to the recording in progress, after the selected command, or at the end of the selected action ([Insert stops in actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/insert-stops-in-actions.html); `ui.action.insertStop`). A playing action waits at the stop and shows the message so the user can do a manual task, and Play Current Selection plays the rest ([Illustrator CS5 help](https://help.adobe.com/archive/en_US/illustrator/cs5/illustrator_cs5_help.pdf#page=459); `ui.action.continue`). With Allow Continue, the message box also has Continue. VectorCraft's own rules, which the pages do not cover: an empty message still waits, Stop Playing/Recording ends the playback and keeps the steps already played, the rest plays on the document active then, a stop waits at every playback speed, at Accelerated the steps between two stops are one undo step, Batch… refuses an action with an included stop, and Delete Selection on a selected step deletes that step, a stop or a command, without asking (`ui.action.deleteStep`). - Actions panel: the panel menu's Insert Menu Item… adds a menu command to the recording in progress, after the selected step, or at the end of the selected action, without running it, and playback runs it as the menu does ([Insert non-recordable tasks into actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/insert-non-recordable-tasks-into-actions.html); `ui.action.insertMenuItem`). The dialog's Find box picks the one item that matches the words typed, and a click on a listed item picks it. VectorCraft's own rules, which the page does not cover: items that open a dialog, the Object › Align items, Image Trace's Make and Make and Expand, Pattern's Make and Edit Pattern, and the File, Window and Help menu items are not listed, and an action holds at most 10,000 steps and its set file at most 8 MiB, for stops too. + - Actions panel: with a path selected, the panel menu's Insert Select Path adds a step that selects that path to the recording in progress, after the selected step, or at the end of the selected action, without changing the selection ([Insert non-recordable tasks into actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/insert-non-recordable-tasks-into-actions.html); `ui.action.insertSelectPath`, `select.storedPath`). VectorCraft's own rules, which the page does not cover: the selection must be one whole path or compound path that can be selected, the step stores the path's id and a fingerprint of its anchors, and playback selects the path only while it is still there unchanged and can be selected, else the step is an error and the selection stays; the row reads Select Path, and a set file with the step is version 4. - Actions panel: Delete Selection deletes the selected action set with its actions and keeps the other sets ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html); `ui.actionSet.delete`). VectorCraft's own rules, which the overview does not cover: deleting a set asks first, Shift-click on a set's name selects a range of sets to delete together, the panel menu's Delete does the same, and Default Actions and the last set can be deleted. Undo does not bring a deleted set back. - Actions panel: the panel menu's Duplicate copies the selected action, else the current action set with all its actions ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html); `ui.actionSet.duplicate`). VectorCraft's own rules, which the overview does not cover: the copy goes right after the original, is named after it with " copy" added and is selected. - 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. From 3bf00207a642b1fe342ab1b40ed40c8814146fdc Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 19:41:57 -0800 Subject: [PATCH 2/2] Actions: action set files with Select Path steps are version 5 Version 4 marks sets with Select Object steps, so a build that reads version 4 but has no select.storedPath accepted a Select Path set and failed at the step during playback. Sets with Select Path steps now save as version 5, Load Actions reads versions 1 to 5, and docs/mcp.md and the file header say so. --- crates/ui-egui/src/panels/actions.rs | 13 ++++++++----- crates/ui-egui/src/panels/actions/files.rs | 21 +++++++++++---------- docs/mcp.md | 15 ++++++++------- docs/target-app-parity.md | 2 +- 4 files changed, 28 insertions(+), 23 deletions(-) diff --git a/crates/ui-egui/src/panels/actions.rs b/crates/ui-egui/src/panels/actions.rs index 6cf62385c9..8196ad27c2 100644 --- a/crates/ui-egui/src/panels/actions.rs +++ b/crates/ui-egui/src/panels/actions.rs @@ -3184,7 +3184,7 @@ mod tests { (json!(long.to_string()), "an action in the action set file has more than 10,000 steps"), (json!(" ".repeat(files::MAX_SET_FILE + 1)), "the action set file is larger than 8 MiB"), ( - json!("{\"format\": \"vcactions\", \"version\": 5, \"name\": \"S\", \"actions\": []}"), + json!("{\"format\": \"vcactions\", \"version\": 6, \"name\": \"S\", \"actions\": []}"), "the action set file needs a later version of VectorCraft", ), ] { @@ -4693,19 +4693,22 @@ mod tests { let (mut app, _, _) = app_with_two_rectangles(); app.run(INSERT_SELECT_PATH, json!({"set": 0, "action": 0, "step": 0})).unwrap(); let saved = files::write_set(&app.ui.action_sets[0]); - assert_eq!(serde_json::from_str::(&saved).unwrap()["version"], json!(4), "older builds must refuse a Select Path step"); + assert_eq!(serde_json::from_str::(&saved).unwrap()["version"], json!(5), "older builds must refuse a Select Path step"); let loaded = files::read_set(&saved, "Test").unwrap(); assert_eq!(loaded.actions[0].steps, app.ui.action_sets[0].actions[0].steps); let step = app.ui.action_sets[0].actions[0].steps[1].clone(); let nested = json!({"commands": [{"command": SELECT_PATH_STEP, "params": step.1}]}); let set = ActionSet::new("Nested", vec![Action::new("A", vec![("command.batch".into(), nested)])]); - assert_eq!(serde_json::from_str::(&files::write_set(&set)).unwrap()["version"], json!(4), "a step in a recorded batch counts"); + assert_eq!(serde_json::from_str::(&files::write_set(&set)).unwrap()["version"], json!(5), "a step in a recorded batch counts"); + let mut both = app.ui.action_sets[0].clone(); + both.actions[0].steps.push((SELECT_OBJECT_STEP.into(), json!({"note": "a", "caseSensitive": false, "wholeWord": false}))); + assert_eq!(serde_json::from_str::(&files::write_set(&both)).unwrap()["version"], json!(5), "with a Select Object step too"); for params in [json!({"id": 3}), json!({"id": -1, "fingerprint": step.1["fingerprint"]}), json!({"id": 3, "fingerprint": "v1:xyz"}), json!(42)] { let bad = json!({"command": SELECT_PATH_STEP, "params": params}); for s in [bad.clone(), json!({"command": "command.batch", "params": {"commands": [bad]}})] { - let file = json!({"format": "vcactions", "version": 4, "name": "Bad", "actions": [{"name": "A", "steps": [s]}]}); + let file = json!({"format": "vcactions", "version": 5, "name": "Bad", "actions": [{"name": "A", "steps": [s]}]}); assert_eq!(files::read_set(&file.to_string(), "Test").unwrap_err(), "not an action set file", "{s}"); } } @@ -5786,7 +5789,7 @@ mod tests { json!({"format": "vcactions", "version": version, "name": "Old", "actions": [{"name": "A", "steps": [{"command": "object.move"}]}]}); assert_eq!(files::read_set(&old.to_string(), "Test").unwrap().actions[0].steps.len(), 1, "version {version}"); } - let later = json!({"format": "vcactions", "version": 5, "name": "New", "actions": []}); + let later = json!({"format": "vcactions", "version": 6, "name": "New", "actions": []}); assert_eq!(files::read_set(&later.to_string(), "Test").unwrap_err(), "the action set file needs a later version of VectorCraft"); for params in [json!({"message": 5}), json!({"allowContinue": "yes"}), json!(42)] { let stop = json!({"command": "action.stop", "params": params}); diff --git a/crates/ui-egui/src/panels/actions/files.rs b/crates/ui-egui/src/panels/actions/files.rs index 14f7717e64..6aa8b95df4 100644 --- a/crates/ui-egui/src/panels/actions/files.rs +++ b/crates/ui-egui/src/panels/actions/files.rs @@ -1,6 +1,6 @@ //! Action set files: the Actions panel menu's Save Actions… and Load Actions…. //! -//! An action set file (`.vcactions`) is JSON: `{"format": "vcactions", "version": 1 to 4, "name", +//! An action set file (`.vcactions`) is JSON: `{"format": "vcactions", "version": 1 to 5, "name", //! "actions": [{"name", "steps": [{"command", "params", "excluded"?}], "key"?, "color"?}]}`. Each step is a //! command id and its params, as `command.batch` takes them, or a stop: the command `action.stop` //! with `{"message", "allowContinue"}`. A Select Object step is the command `select.byNote` with @@ -10,9 +10,9 @@ //! or with a value that is not one, has none. `excluded: true` keeps a step but skips it during //! playback. Sets with exclusions use version 2 so older builds refuse them rather than replay //! excluded steps, sets with stops use version 3 so older builds refuse them rather than fail at a -//! stop, and sets with Select Object or Select Path steps use version 4 so older builds refuse them -//! rather than fail at that step. Other sets use version 1, and earlier version 1 to 3 files still -//! work. +//! stop, sets with Select Object steps use version 4 and sets with Select Path steps use version 5 so +//! older builds refuse them rather than fail at that step. Other sets use version 1, and earlier +//! version 1 to 4 files still work. //! Load Actions… also reads the action sets another app saves as text (`.aia`, through //! `actionSet.read`). @@ -173,11 +173,12 @@ pub fn write_set(set: &ActionSet) -> String { let has_select_object = steps().any(|(command, params)| !select_objects_in(command, params).is_empty()); let has_select_path = steps().any(|(command, params)| !select_paths_in(command, params).is_empty()); let has_stop = steps().any(|(command, params)| !stops_in(command, params).is_empty()); - let version = match (has_select_object || has_select_path, has_stop, set.actions.iter().any(|a| !a.excluded_steps.is_empty())) { - (true, ..) => 4, - (false, true, _) => 3, - (false, false, true) => 2, - (false, false, false) => 1, + let version = match (has_select_path, has_select_object, has_stop, set.actions.iter().any(|a| !a.excluded_steps.is_empty())) { + (true, ..) => 5, + (false, true, ..) => 4, + (false, false, true, _) => 3, + (false, false, false, true) => 2, + (false, false, false, false) => 1, }; let file = SetFile { format: ACTIONS_EXT.into(), version, name: set.name.clone(), actions }; serde_json::to_string_pretty(&file).unwrap_or_default() @@ -192,7 +193,7 @@ pub fn read_set(text: &str, name: &str) -> Result { if f.format != ACTIONS_EXT { return Err("not an action set file".into()); } - if !matches!(f.version, 1..=4) { + if !matches!(f.version, 1..=5) { return Err("the action set file needs a later version of VectorCraft".into()); } if f.actions.len() > MAX_SET_ACTIONS { diff --git a/docs/mcp.md b/docs/mcp.md index 15fa2b061b..1e0571c3c2 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -2987,14 +2987,15 @@ already walks Image Trace. Short agent entry points: the file at `path`, else a picked one, or the file's text `data`, adds its set after the other sets and returns `{set, name, actions}`. A set named like one already in the panel gets the next free number (`Print 2`), and the other sets and the current set stay as they are. An action set file (`.vcactions`) is JSON: - `{"format": "vcactions", "version": 1 to 4, "name", "actions": [{"name", "steps": [{"command", "params", "excluded"?}]}]}`, each step + `{"format": "vcactions", "version": 1 to 5, "name", "actions": [{"name", "steps": [{"command", "params", "excluded"?}]}]}`, each step a command id and its params as `command.batch` takes them, or a stop: the command `action.stop` with `{message, allowContinue}`. A Select Object step is the command `select.byNote` with `{note, caseSensitive, - wholeWord}`. `excluded: true` preserves a step but omits it from playback; - an absent or false flag includes the step. Files with Select Object steps use version 4, files with stops and no - Select Object step use version 3, and files with excluded steps and neither use version 2, so older builds refuse them - instead of playing the excluded steps or failing at a stop or a Select Object step; other files use version 1. A file with another `format` or a `version` other than - 1 to 4, a stop whose message is not text or whose `allowContinue` is not true or false, a Select Object step whose params `ui.action.insertSelectObject` would refuse, a file larger than 8 MiB, one with more than 1,000 actions and one with an action of more than 10,000 steps are + wholeWord}`, and a Select Path step is the command `select.storedPath` with `{id, fingerprint}`. `excluded: true` preserves a step but omits it from playback; + an absent or false flag includes the step. Files with Select Path steps use version 5, files with Select Object steps + and no Select Path step use version 4, files with stops and neither use version 3, and files with excluded steps and + none of these use version 2, so older builds refuse them instead of playing the excluded steps or failing at a stop, a + Select Object step or a Select Path step; other files use version 1, and version 1 to 4 files still load. A file with another `format` or a `version` other than + 1 to 5, a stop whose message is not text or whose `allowContinue` is not true or false, a Select Object step whose params `ui.action.insertSelectObject` would refuse, a file larger than 8 MiB, one with more than 1,000 actions and one with an action of more than 10,000 steps are refused, and no set is added. So is a file whose set `ui.actionSet.save` would write larger than 8 MiB (a file with less white space, a set name that gets a number, or an `.aia` set), with the error `the action set would be larger than 8 MiB`. The Actions panel menu's Save Actions… and Load Actions… run these; Load Actions… is @@ -3097,7 +3098,7 @@ already walks Image Trace. Short agent entry points: index past the last set, action or step, no action and no recording, or a step past the 10,000-step and 8 MiB limits is an error, and then nothing changes. The panel menu's Insert Select Path runs this for the selected step or action, or the recording in progress, and is off while the selection is not such a path. The step's row in the panel reads - Select Path. An action set file holding the step is version 4, and Load Actions… refuses a step whose params are not + Select Path. An action set file holding the step is version 5, and Load Actions… refuses a step whose params are not an id and such a fingerprint. - Actions: `select.storedPath {id, fingerprint}` selects only the path `id` while its fingerprint is still `fingerprint` and it can be selected: not hidden or locked, nor in a hidden or locked layer or group, a template diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 9d3107a2ae..57df2225e6 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -743,7 +743,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Actions panel: the panel menu's Playback Options… sets how actions play: Accelerated (the default) plays every step at once, Step by Step plays one step at a time with the canvas drawn after each, and Pause For waits the given number of seconds between steps ([Specify playback speed](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/specify-playback-speed.html); `ui.action.playbackOptions`). VectorCraft's own rules, which the page does not cover: the pause is a whole number of seconds from 1 to 60, the setting is kept with the UI preferences, a paced playback ends at Stop Playing/Recording or when a step comes due while another document is active, each step of a paced playback is its own undo step, and Batch… plays at full speed. - Actions panel: the panel menu's Insert Stop… opens Record Stop, with a Message box and Allow Continue, and adds a stop to the recording in progress, after the selected command, or at the end of the selected action ([Insert stops in actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/insert-stops-in-actions.html); `ui.action.insertStop`). A playing action waits at the stop and shows the message so the user can do a manual task, and Play Current Selection plays the rest ([Illustrator CS5 help](https://help.adobe.com/archive/en_US/illustrator/cs5/illustrator_cs5_help.pdf#page=459); `ui.action.continue`). With Allow Continue, the message box also has Continue. VectorCraft's own rules, which the pages do not cover: an empty message still waits, Stop Playing/Recording ends the playback and keeps the steps already played, the rest plays on the document active then, a stop waits at every playback speed, at Accelerated the steps between two stops are one undo step, Batch… refuses an action with an included stop, and Delete Selection on a selected step deletes that step, a stop or a command, without asking (`ui.action.deleteStep`). - Actions panel: the panel menu's Insert Menu Item… adds a menu command to the recording in progress, after the selected step, or at the end of the selected action, without running it, and playback runs it as the menu does ([Insert non-recordable tasks into actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/insert-non-recordable-tasks-into-actions.html); `ui.action.insertMenuItem`). The dialog's Find box picks the one item that matches the words typed, and a click on a listed item picks it. VectorCraft's own rules, which the page does not cover: items that open a dialog, the Object › Align items, Image Trace's Make and Make and Expand, Pattern's Make and Edit Pattern, and the File, Window and Help menu items are not listed, and an action holds at most 10,000 steps and its set file at most 8 MiB, for stops too. - - Actions panel: with a path selected, the panel menu's Insert Select Path adds a step that selects that path to the recording in progress, after the selected step, or at the end of the selected action, without changing the selection ([Insert non-recordable tasks into actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/insert-non-recordable-tasks-into-actions.html); `ui.action.insertSelectPath`, `select.storedPath`). VectorCraft's own rules, which the page does not cover: the selection must be one whole path or compound path that can be selected, the step stores the path's id and a fingerprint of its anchors, and playback selects the path only while it is still there unchanged and can be selected, else the step is an error and the selection stays; the row reads Select Path, and a set file with the step is version 4. + - Actions panel: with a path selected, the panel menu's Insert Select Path adds a step that selects that path to the recording in progress, after the selected step, or at the end of the selected action, without changing the selection ([Insert non-recordable tasks into actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/insert-non-recordable-tasks-into-actions.html); `ui.action.insertSelectPath`, `select.storedPath`). VectorCraft's own rules, which the page does not cover: the selection must be one whole path or compound path that can be selected, the step stores the path's id and a fingerprint of its anchors, and playback selects the path only while it is still there unchanged and can be selected, else the step is an error and the selection stays; the row reads Select Path, and a set file with the step is version 5. - Actions panel: the panel menu's Select Object… opens Set Selection, where the note typed names the objects to select, and adds a step to the recording in progress, after the selected step, or at the end of the selected action; playback selects the objects whose Attributes note matches it, and the steps after it act on them ([Insert non-recordable tasks into actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/insert-non-recordable-tasks-into-actions.html); `ui.action.insertSelectObject`, `select.byNote`). VectorCraft's own rules, which the page does not cover: the note matches anywhere and in any letter case unless Case Sensitive or Whole Word is checked, hidden and locked objects are left out, the objects found replace the selection, a step that finds none stops the playback with an error, and the note is at most 1,024 bytes. Action sets with the step save as version 4. - Actions panel: Delete Selection deletes the selected action set with its actions and keeps the other sets ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html); `ui.actionSet.delete`). VectorCraft's own rules, which the overview does not cover: deleting a set asks first, Shift-click on a set's name selects a range of sets to delete together, the panel menu's Delete does the same, and Default Actions and the last set can be deleted. Undo does not bring a deleted set back. - Actions panel: the panel menu's Duplicate copies the selected action, else the current action set with all its actions ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html); `ui.actionSet.duplicate`). VectorCraft's own rules, which the overview does not cover: the copy goes right after the original, is named after it with " copy" added and is selected.