diff --git a/crates/engine/src/cmd.rs b/crates/engine/src/cmd.rs index e02382b6b1..80ed19d145 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 c4b21b1cb4..5958a84b53 100644 --- a/crates/engine/src/cmd/plugin_call/reviewed.rs +++ b/crates/engine/src/cmd/plugin_call/reviewed.rs @@ -528,6 +528,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 ea307c7575..ac9ca365f4 100644 --- a/crates/engine/src/cmd/select.rs +++ b/crates/engine/src/cmd/select.rs @@ -49,6 +49,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", @@ -449,6 +458,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 95c98f1ea5..2352776651 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -176,6 +176,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 d66ea18445..9d4066d524 100644 --- a/crates/mcp/src/ui_catalog.rs +++ b/crates/mcp/src/ui_catalog.rs @@ -1439,6 +1439,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.insertSelectObject", "Select Object", diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 8edde76eb3..a9ff74ad2e 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -5988,6 +5988,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 шестнадесетични цифри с малки букви Select Object… Избиране на обект… Select Object Избиране на обект Set Selection Задаване на селекция diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index c9c207eaa2..ab1069477e 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -6030,6 +6030,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 Select Object… Odaberi objekt… Select Object Odaberi objekt Set Selection Postavi odabir diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 0399e6c04a..1513be6519 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -4043,6 +4043,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 Select Object… Selecciona un objecte… Select Object Selecciona un objecte Set Selection Defineix la selecció diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index e62070329c..620f41c7af 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6434,6 +6434,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 Select Object… Vybrat objekt… Select Object Vybrat objekt Set Selection Nastavit výběr diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 47a78ef612..d4e7818eb1 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -6005,6 +6005,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 Select Object… Markér objekt… Select Object Markér objekt Set Selection Angiv markering diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index af4489a637..fec3116dd2 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -6025,6 +6025,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 Select Object… Objekt auswählen… Select Object Objekt auswählen Set Selection Auswahl festlegen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 6ad6ff11c0..e06a85d96b 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -6385,6 +6385,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 πεζά δεκαεξαδικά ψηφία Select Object… Επιλογή αντικειμένου… Select Object Επιλογή αντικειμένου Set Selection Ορισμός επιλογής diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 41f92e14f7..a2eec00b4d 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -6376,6 +6376,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 Select Object… Seleccionar objeto… Select Object Seleccionar objeto Set Selection Definir selección diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index fd38382f63..28d7bc3201 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6464,6 +6464,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 Select Object… Valitse objekti… Select Object Valitse objekti Set Selection Määritä valinta diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index dcbe436fc3..1cd422723e 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6429,6 +6429,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 Select Object… Sélectionner un objet… Select Object Sélectionner un objet Set Selection Définir la sélection diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 9b7a15bbad..d66890294c 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -3472,6 +3472,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 हेक्स अंक होना चाहिए Select Object… ऑब्जेक्ट सिलेक्ट करें… Select Object ऑब्जेक्ट सिलेक्ट करें Set Selection सिलेक्शन सेट करें diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 6832a86707..0424d09191 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6417,6 +6417,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 Select Object… Objektum kijelölése… Select Object Objektum kijelölése Set Selection Kijelölés beállítása diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 45e05b9622..e94af03d31 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -6005,6 +6005,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 Select Object… Pilih objek… Select Object Pilih objek Set Selection Atur seleksi diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 57f141f880..8644c5980a 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -6059,6 +6059,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 Select Object… Seleziona oggetto… Select Object Seleziona oggetto Set Selection Imposta selezione diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 69b55c9121..4f6ee44b60 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -6124,6 +6124,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 桁である必要があります Select Object… オブジェクトを選択… Select Object オブジェクトを選択 Set Selection 選択範囲を設定 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 1cbb2d1bf1..ba86395631 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -6385,6 +6385,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 პატარა თექვსმეტობითი ციფრი Select Object… ობიექტის მონიშვნა… Select Object ობიექტის მონიშვნა Set Selection მონიშვნის დაყენება diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 6135f8d5ec..535e9479ce 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -6020,6 +6020,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자리여야 합니다 Select Object… 개체 선택… Select Object 개체 선택 Set Selection 선택 영역 설정 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 327aee902c..8db8b8c4ed 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -6056,6 +6056,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 Select Object… Object selecteren… Select Object Object selecteren Set Selection Selectie instellen diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index f277d51d65..43f5161871 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -6288,6 +6288,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 Select Object… Zaznacz obiekt… Select Object Zaznacz obiekt Set Selection Ustaw zaznaczenie diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index afd10a5fbc..6d6f06c024 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -6349,6 +6349,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 Select Object… Selecionar Objeto… Select Object Selecionar Objeto Set Selection Definir Seleção diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 2dca232079..c142abeb3c 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -6045,6 +6045,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 шестнадцатеричных цифр в нижнем регистре Select Object… Выделить объект… Select Object Выделить объект Set Selection Задать выделение diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index e47c4e8662..3e4c3d38a6 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -6415,6 +6415,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 Select Object… Markera objekt… Select Object Markera objekt Set Selection Ange markering diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 5983ec4bfe..faef823a55 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -6390,6 +6390,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 Select Object… Nesne Seç… Select Object Nesne Seç Set Selection Seçimi Ayarla diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 4c9326af72..f1a5a53e21 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -6039,6 +6039,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 шістнадцяткових цифр у нижньому регістрі Select Object… Виділити об’єкт… Select Object Виділити об’єкт Set Selection Задати виділення diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index fef0b8ff90..b912fc8d8c 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -6040,6 +6040,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 Select Object… Chọn đối tượng… Select Object Chọn đối tượng Set Selection Đặt vùng chọn diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index a0d941ada8..65ac93bbee 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -6272,6 +6272,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 位小写十六进制数字 Select Object… 选择对象… Select Object 选择对象 Set Selection 设置选区 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index b724e57351..c7478bde0e 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -6077,6 +6077,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 位小寫十六進位數字 Select Object… 選取物件… Select Object 選取物件 Set Selection 設定選取範圍 diff --git a/crates/ui-egui/src/menus/run.rs b/crates/ui-egui/src/menus/run.rs index 964be00d61..c54d45cf4f 100644 --- a/crates/ui-egui/src/menus/run.rs +++ b/crates/ui-egui/src/menus/run.rs @@ -983,6 +983,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::INSERT_SELECT_OBJECT => crate::panels::actions::insert_select_object(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), diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index 08c1bbb4c6..623a6967b1 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -1440,6 +1440,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::INSERT_SELECT_OBJECT, "Select Object", diff --git a/crates/ui-egui/src/panels/actions.rs b/crates/ui-egui/src/panels/actions.rs index fc70693ce8..8196ad27c2 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 command a Select Object step runs, with {note, caseSensitive, wholeWord} as its params: it /// selects the objects whose Attributes note matches. pub const SELECT_OBJECT_STEP: &str = "select.byNote"; @@ -1757,6 +1782,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!("Select Object…"), recording || has, false) { let step = selected_step(app, ui.ctx(), selected); crate::dialogs::action_select_object::open(app, if recording { None } else { selected }, step); @@ -1947,11 +1983,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, and a Select Object step's its note; other + // A stop's row shows its message, a Select Path step's its name and a Select Object step's its note; 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 if command == SELECT_OBJECT_STEP { let note = params.get("note").and_then(Value::as_str).and_then(|m| m.lines().next()).unwrap_or_default(); crate::i18n::fmt(tl!("Select Object: {note}"), &[("note", note)]) @@ -3146,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", ), ] { @@ -4533,6 +4571,178 @@ 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!(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!(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": 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}"); + } + } + } + + #[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. @@ -5579,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 ee6304690b..6aa8b95df4 100644 --- a/crates/ui-egui/src/panels/actions/files.rs +++ b/crates/ui-egui/src/panels/actions/files.rs @@ -1,16 +1,18 @@ //! 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 -//! `{"note", "caseSensitive", "wholeWord"}`. `key` is the action's function key +//! `{"note", "caseSensitive", "wholeWord"}`, and 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, sets with stops use version 3 so older builds refuse them rather than fail at a -//! stop, and sets with Select Object 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`). @@ -145,6 +147,11 @@ fn steps_in<'a>(kind: &str, command: &str, params: &'a Value) -> Vec<&'a Value> steps.filter(|step| step.get("command").and_then(Value::as_str) == Some(kind)).map(|step| step.get("params").unwrap_or(&Value::Null)).collect() } +/// The params of the Select Path steps in a step, as [`stops_in`] finds stops. +fn select_paths_in<'a>(command: &str, params: &'a Value) -> Vec<&'a Value> { + steps_in(super::SELECT_PATH_STEP, command, params) +} + /// `set` as the text of an action set file. pub fn write_set(set: &ActionSet) -> String { let actions = set @@ -164,12 +171,14 @@ pub fn write_set(set: &ActionSet) -> String { .collect(); let steps = || set.actions.iter().flat_map(|a| &a.steps); 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_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() @@ -184,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 { @@ -197,6 +206,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 mut select_objects = f.actions.iter().flat_map(|a| &a.steps).flat_map(|s| select_objects_in(&s.command, &s.params)); if select_objects.any(|p| super::select_object_params(p).is_err()) { return Err("not an action set file".into()); diff --git a/docs/mcp.md b/docs/mcp.md index f95db364e0..be3a5fb047 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -2991,14 +2991,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 @@ -3091,6 +3092,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 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 + 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.insertSelectObject {set?, action?, step?, note, caseSensitive?, wholeWord?}` inserts a Select Object step without running it. The step is `select.byNote {note, caseSensitive, wholeWord}`: during playback it selects the objects whose Attributes note matches `note`, and the steps after it act on them. `note` loses its diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index daf1f0a8c1..71dcd92aeb 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -745,6 +745,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 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.