diff --git a/crates/engine/src/cmd/docmenu.rs b/crates/engine/src/cmd/docmenu.rs index cd521a58b..fc56b5c6e 100644 --- a/crates/engine/src/cmd/docmenu.rs +++ b/crates/engine/src/cmd/docmenu.rs @@ -269,11 +269,11 @@ fn candidates(s: &Session, f: impl Fn(&Node) -> bool) -> Result> { } fn finish_same(s: &mut Session, cmd: &str, ids: Vec) -> Result { - let n = ids.len(); let from = s.doc()?.selection.objects.clone(); s.select(|_, sel| sel.set(ids))?; - s.doc_mut()?.last_selection_cmd = Some((cmd.to_string(), json!({}), from)); - Ok(json!({ "count": n })) + let st = s.doc_mut()?; + st.last_selection_cmd = Some((cmd.to_string(), json!({}), from)); + Ok(json!({ "count": st.selection.objects.len() })) } fn same(s: &mut Session, cmd: &str, eq: fn(&Node, &Node) -> bool) -> Result { @@ -312,9 +312,9 @@ fn same_text(s: &mut Session, cmd: &str, eq: fn(&CharStyle, &CharStyle) -> bool) fn by_kind(s: &mut Session, f: impl Fn(&Node) -> bool) -> Result { let ids = candidates(s, f)?; - let n = ids.len(); s.select(|_, sel| sel.set(ids))?; - Ok(json!({ "count": n })) + // The objects selected, after Lock Guides has left out guide paths. + Ok(json!({ "count": s.doc()?.selection.objects.len() })) } fn direction_handles(s: &mut Session, _: &Value) -> Result { diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 1ac71598e..5ec67c4ea 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -27,7 +27,7 @@ pub fn specs() -> Vec { cmd!("document.activate", "Activate Document", [], None, "{index}", always, doc_activate), cmd!(query "document.inspect", "Inspect Document", [], None, "{depth?, childLimit?, ids?: [id…] (list just these nodes, of any level, in this order, under `nodes` in place of `layers`; an unknown id is an error; at most 10000), kind?: panel label such as Layer; Group; Path; Type or Image (exact, ignoring case; list every such node under `nodes`, each with `path`, its ancestor ids with the layer first, searching the subtrees of `ids` when given), limit?: at most this many entries of the list, offset?: entries skipped first (default 0; with `ids`, `kind`, `limit` or `offset` the reply adds `total`, the entries before paging)} → layer tree (each node: id, name, kind, visible, locked, bounds, opacity when below 1, blend when not Normal, fill, stroke and strokeWidth of objects and compound paths; a compound path's members have compoundMember: true instead of paint, which is the compound's), artboards, selection, history (a sliced tree reports childCount); without the scope options the reply is the whole layer tree", has_doc, doc_inspect), cmd!(query "document.node", "Inspect Object", [], None, "{id, summary?: compact summary, depth?: child levels in the summary (default all), childLimit?: children shown per node (default all; a level that shows fewer reports childCount)} → one object", has_doc, doc_node), - cmd!(query "document.find", "Find Objects", [], None, "{name?: substring of the Layers panel name, kind?: panel label such as Group; Path; Type or Image (exact), text?: substring of type content, limit?: max matches (default 100, 0 counts only)} → {matches: [{id, name, kind, path}], total}", has_doc, doc_find), + cmd!(query "document.find", "Find Objects", [], None, "{name?: substring of the Layers panel name, kind?: panel label such as Group; Path; Type or Image (exact), text?: substring of type content, within?: [x, y, width, height] (an object's geometric bounds inside this rectangle, edges included; document points as document.inspect's bounds; layers never match this or the size filters), intersects?: [x, y, width, height] (bounds touching or overlapping it), minWidth?, maxWidth?, minHeight?, maxHeight?: pt (the bounds' size), minThickness?, maxThickness?: pt (the smaller of the bounds' width and height), minArea?, maxArea?: pt² (the bounds' width × height), minAnchors?, maxAnchors?: anchor points of a path (a compound path's members together; other objects never match), select?: bool (also select the matches, leaving out hidden and locked ones, those inside another match, and a compound path's members unless the compound path is isolated; Select > Reselect repeats it), limit?: max matches listed (default 100, 0 counts only)} → {matches: [{id, name, kind, path}], total, selected (with select: the count selected)}; at least one filter, all of them matching", has_doc, doc_find), cmd!(query "document.json", "Document JSON", [], None, "{} → complete document model", has_doc, |s, _| Ok(serde_json::to_value(&*s.doc()?.doc).unwrap_or(Value::Null))), cmd!( "document.setUnits", @@ -243,19 +243,82 @@ fn opt_u64(p: &Value, cmd: &str, key: &str) -> Result> { } fn doc_find(s: &mut Session, p: &Value) -> Result { + const C: &str = "document.find"; // A filter of the wrong type is an error, not dropped: dropping it would widen the search. let needle = |key: &str| match p.get(key) { None | Some(Value::Null) => Ok(None), Some(Value::String(v)) => Ok(Some(v.to_lowercase()).filter(|v| !v.is_empty())), - Some(_) => Err(bad("document.find", format!("`{key}` must be a string"))), + Some(_) => Err(bad(C, format!("`{key}` must be a string"))), }; - let filter = inspect::FindFilter { name: needle("name")?, kind: needle("kind")?, text: needle("text")? }; - if filter.is_empty() { - return Err(bad("document.find", "give at least one of `name`, `kind`, `text`")); + let rect = |key: &str| match p.get(key) { + None | Some(Value::Null) => Ok(None), + Some(v) => match finite_numbers::<4>(v) { + Some([x, y, w, h]) if w >= 0.0 && h >= 0.0 => Ok(Some(vectorcraft_geom::Rect::new(x, y, x + w, y + h))), + _ => Err(bad(C, format!("`{key}` must be [x, y, width, height]: four finite numbers, width and height not negative"))), + }, + }; + let bound = |key: &str| match p.get(key) { + None | Some(Value::Null) => Ok(None), + Some(v) => v.as_f64().filter(|n| n.is_finite() && *n >= 0.0).map(Some).ok_or_else(|| bad(C, format!("`{key}` must be a number not below 0"))), + }; + let range = |name: &str| -> Result { + let (min_key, max_key) = (format!("min{name}"), format!("max{name}")); + let r = inspect::FindRange { min: bound(&min_key)?, max: bound(&max_key)? }; + if let (Some(min), Some(max)) = (r.min, r.max) + && min > max + { + return Err(bad(C, format!("`{min_key}` is greater than `{max_key}`"))); + } + Ok(r) + }; + let anchors = range("Anchors")?; + if [anchors.min, anchors.max].iter().flatten().any(|n| n.fract() != 0.0) { + return Err(bad(C, "`minAnchors` and `maxAnchors` must be whole numbers")); } - let limit = opt_u64(p, "document.find", "limit")?.unwrap_or(100); - let (matches, total) = inspect::find_nodes(&s.doc()?.doc, &filter, limit); - Ok(json!({"matches": matches, "total": total})) + let filter = inspect::FindFilter { + name: needle("name")?, + kind: needle("kind")?, + text: needle("text")?, + within: rect("within")?, + intersects: rect("intersects")?, + width: range("Width")?, + height: range("Height")?, + thickness: range("Thickness")?, + area: range("Area")?, + anchors, + }; + if filter.is_empty() { + return Err(bad( + C, + "give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors`", + )); + } + let select = match p.get("select") { + None | Some(Value::Null) => false, + Some(v) => v.as_bool().ok_or_else(|| bad(C, "`select` must be true or false"))?, + }; + let limit = opt_u64(p, C, "limit")?.unwrap_or(100); + let (matches, ids) = inspect::find_nodes(&s.doc()?.doc, &filter, limit); + let mut reply = json!({"matches": matches, "total": ids.len()}); + if select { + reply["selected"] = select_found(s, p, &ids)?["count"].clone(); + s.journal_query(); + } + Ok(reply) +} + +/// Select the objects among `ids` that Select All and the objects inside them take (none hidden or +/// locked, nor inside a hidden or locked one), leaving out those inside another of them and the +/// members of compound paths, unless a compound path is isolated. Select > Reselect repeats +/// `document.find` with `p`. +fn select_found(s: &mut Session, p: &Value, ids: &[NodeId]) -> Result { + let st = s.doc()?; + let compound = |n: &Node| matches!(n.kind, vectorcraft_doc::NodeKind::Compound { .. }); + let members = st.isolation.and_then(|i| st.doc.ancestry(i)).is_some_and(|a| a.iter().any(|i| st.doc.node(*i).is_some_and(compound))); + let found: BTreeSet = ids.iter().copied().collect(); + super::select::select_where(s, "document.find", p, |d, n| { + found.contains(&n.id) && (members || !d.parent_of(n.id).and_then(|p| d.node(p)).is_some_and(compound)) + }) } /// Document Setup's units (also `document.setup {units}`). diff --git a/crates/engine/src/cmd/select.rs b/crates/engine/src/cmd/select.rs index e2b7c2a92..26f90aa1f 100644 --- a/crates/engine/src/cmd/select.rs +++ b/crates/engine/src/cmd/select.rs @@ -689,7 +689,7 @@ fn same(s: &mut Session, cmd: &str, eq: impl Fn(&Node, &Node) -> bool) -> Result /// Select the objects [`walk_selectable`] visits that `f` accepts (not looking inside accepted /// ones); Select > Reselect repeats `cmd` with `p`. -fn select_where(s: &mut Session, cmd: &str, p: &Value, f: impl Fn(&Document, &Node) -> bool) -> Result { +pub(crate) fn select_where(s: &mut Session, cmd: &str, p: &Value, f: impl Fn(&Document, &Node) -> bool) -> Result { let st = s.doc()?; let mut ids = vec![]; walk_selectable(&st.doc, st.isolation, |n| { @@ -701,8 +701,10 @@ fn select_where(s: &mut Session, cmd: &str, p: &Value, f: impl Fn(&Document, &No }); let from = st.selection.objects.clone(); s.select(|_, sel| sel.set(ids.iter().copied()))?; - s.doc_mut()?.last_selection_cmd = Some((cmd.to_string(), p.clone(), from)); - Ok(json!({ "count": ids.len() })) + let st = s.doc_mut()?; + st.last_selection_cmd = Some((cmd.to_string(), p.clone(), from)); + // The objects selected, after Lock Guides has left out guide paths. + Ok(json!({ "count": st.selection.objects.len() })) } fn same_graphic_style(s: &mut Session, _: &Value) -> Result { @@ -763,6 +765,7 @@ fn by_kind(s: &mut Session, f: fn(&Node) -> bool) -> Result { } true }); - s.select(|_, sel| sel.set(ids.iter().copied()))?; - Ok(json!({ "count": ids.len() })) + s.select(|_, sel| sel.set(ids))?; + // The objects selected, after Lock Guides has left out guide paths. + Ok(json!({ "count": s.doc()?.selection.objects.len() })) } diff --git a/crates/engine/src/inspect.rs b/crates/engine/src/inspect.rs index 023d801c2..53db233aa 100644 --- a/crates/engine/src/inspect.rs +++ b/crates/engine/src/inspect.rs @@ -2,7 +2,8 @@ use serde_json::{Value, json}; use vectorcraft_color::BlendMode; -use vectorcraft_doc::{ArrowAlign, Document, LineCap, LineJoin, Node, NodeKind, StrokeAlign, StrokeLayer, SymbolBoxes, WidthProfile}; +use vectorcraft_doc::{ArrowAlign, Document, LineCap, LineJoin, Node, NodeId, NodeKind, StrokeAlign, StrokeLayer, SymbolBoxes, WidthProfile}; +use vectorcraft_geom::Rect; use crate::Session; @@ -298,20 +299,87 @@ fn document_base(s: &Session) -> Value { /// What `document.find` matches: every given filter must hit (AND). All matching is /// case-insensitive; `name` and `text` are substrings, `kind` is the exact panel label -/// (`Layer`, `Group`, `Path`, `Type`, `Image`, ...). +/// (`Layer`, `Group`, `Path`, `Type`, `Image`, ...). The geometric filters read an object's +/// geometric bounds as `document.inspect` reports them; layers and sublayers fail them, and so +/// does a node without bounds (an empty group); `anchors` counts a path's anchor points, summed +/// over a compound path's members, and a node that is neither fails it. +#[derive(Default)] pub struct FindFilter { pub name: Option, pub kind: Option, pub text: Option, + /// The bounds lie inside this rectangle, edges included. + pub within: Option, + /// The bounds touch or overlap this rectangle. + pub intersects: Option, + /// The bounds' width. + pub width: FindRange, + /// The bounds' height. + pub height: FindRange, + /// The smaller of the bounds' width and height. + pub thickness: FindRange, + /// The bounds' width times their height. + pub area: FindRange, + /// The anchor points of a path, or of a compound path's members together. + pub anchors: FindRange, +} + +/// Inclusive limits on one measure of a [`FindFilter`]; neither given matches everything. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct FindRange { + pub min: Option, + pub max: Option, +} + +impl FindRange { + pub fn is_empty(&self) -> bool { + self.min.is_none() && self.max.is_none() + } + + fn hits(&self, v: f64) -> bool { + self.min.is_none_or(|m| v >= m) && self.max.is_none_or(|m| v <= m) + } } impl FindFilter { /// True when no effective filter was given (empty strings count as absent). pub fn is_empty(&self) -> bool { [self.name.as_ref(), self.kind.as_ref(), self.text.as_ref()].iter().all(|f| f.is_none()) + && self.within.is_none() + && self.intersects.is_none() + && self.ranges().iter().all(|r| r.is_empty()) + && self.anchors.is_empty() } - fn hits(&self, n: &Node) -> bool { + /// The limits read from the bounds. + fn ranges(&self) -> [FindRange; 4] { + [self.width, self.height, self.thickness, self.area] + } + + fn hits(&self, n: &Node, symbols: &SymbolBoxes) -> bool { + if self.within.is_some() || self.intersects.is_some() || self.ranges().iter().any(|r| !r.is_empty()) { + if matches!(n.kind, NodeKind::Layer { .. }) { + return false; + } + let Some(b) = n.geometric_bounds_with(symbols) else { return false }; + if self.within.is_some_and(|r| b.x0 < r.x0 || b.y0 < r.y0 || b.x1 > r.x1 || b.y1 > r.y1) { + return false; + } + if self.intersects.is_some_and(|r| b.x0 > r.x1 || b.x1 < r.x0 || b.y0 > r.y1 || b.y1 < r.y0) { + return false; + } + let (w, h) = (b.width(), b.height()); + if !(self.width.hits(w) && self.height.hits(h) && self.thickness.hits(w.min(h)) && self.area.hits(w * h)) { + return false; + } + } + if !self.anchors.is_empty() && !anchor_count(n).is_some_and(|c| self.anchors.hits(c as f64)) { + return false; + } + self.hits_text(n) + } + + fn hits_text(&self, n: &Node) -> bool { if let Some(needle) = &self.name && !n.display_name().to_lowercase().contains(needle) { @@ -332,31 +400,47 @@ impl FindFilter { } } +/// The anchor points of a path, or of a compound path's members together; `None` for any other +/// node. +fn anchor_count(n: &Node) -> Option { + match &n.kind { + NodeKind::Path { path, .. } => Some(path.anchor_count()), + NodeKind::Compound { children, .. } => Some(children.iter().filter_map(|c| anchor_count(c)).sum()), + _ => None, + } +} + /// Search the whole layer tree, top of the stack first like the Layers panel. /// Returns at most `limit` hits as `{id, name, kind, path}` (`path` is the ancestor -/// chain, layer first) plus the total hit count, so a capped reply stays explicit. -pub fn find_nodes(doc: &Document, filter: &FindFilter, limit: u64) -> (Vec, u64) { +/// chain, layer first) plus the ids of every hit, so a capped reply stays explicit. +pub fn find_nodes(doc: &Document, filter: &FindFilter, limit: u64) -> (Vec, Vec) { use std::sync::Arc; - let mut out = Vec::new(); - let mut total = 0u64; - let mut ancestors: Vec = Vec::new(); - fn walk(nodes: &[Arc], ancestors: &mut Vec, filter: &FindFilter, limit: u64, out: &mut Vec, total: &mut u64) { + struct Walk<'a> { + filter: &'a FindFilter, + symbols: SymbolBoxes<'a>, + limit: u64, + ancestors: Vec, + out: Vec, + ids: Vec, + } + fn walk(nodes: &[Arc], w: &mut Walk) { for n in nodes.iter().rev() { - if filter.hits(n) { - *total += 1; - if (out.len() as u64) < limit { - out.push(json!({"id": n.id.0, "name": n.display_name(), "kind": n.kind_label(), "path": ancestors})); + if w.filter.hits(n, &w.symbols) { + w.ids.push(n.id); + if (w.out.len() as u64) < w.limit { + w.out.push(json!({"id": n.id.0, "name": n.display_name(), "kind": n.kind_label(), "path": w.ancestors})); } } if let Some(ch) = n.children() { - ancestors.push(n.id.0); - walk(ch, ancestors, filter, limit, out, total); - ancestors.pop(); + w.ancestors.push(n.id.0); + walk(ch, w); + w.ancestors.pop(); } } } - walk(&doc.layers, &mut ancestors, filter, limit, &mut out, &mut total); - (out, total) + let mut w = Walk { filter, symbols: doc.symbol_boxes(), limit, ancestors: Vec::new(), out: Vec::new(), ids: Vec::new() }; + walk(&doc.layers, &mut w); + (w.out, w.ids) } /// Cap names as `stroke.set` takes them. diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index bbe618bf6..43403f26e 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1415,6 +1415,9 @@ pub struct Session { /// The depth of the command whose values [`Session::note_journal`] keeps: 1 (the top-level /// command), or a step's while a batch runs it ([`Session::execute_step`]). note_depth: u32, + /// The running top-level query command changed the selection and is journaled like any other + /// command ([`Session::journal_query`]). + journal_query: bool, /// While `command.batch` runs: the documents its steps closed or replaced (Revert), which an /// error brings back; `None` otherwise. pub(crate) batch_stash: Option>, @@ -1546,6 +1549,7 @@ impl Session { ui_language: None, journal_note: Default::default(), note_depth: 1, + journal_query: false, batch_stash: None, place_hand_off: None, queue_hand_off: None, @@ -1762,6 +1766,7 @@ impl Session { if self.depth == 0 { self.journal_note.clear(); self.note_depth = 1; + self.journal_query = false; self.batch_stash = None; } // A selection-only command given `ids` selects them first, in the same undo step (#2130); @@ -1792,7 +1797,8 @@ impl Session { if self.depth == 0 { self.inherit_new_art(); } - if spec.journal && self.depth == 0 && self.active().is_none_or(|d| d.interaction.is_none()) { + let journaled = spec.journal || (self.depth == 0 && std::mem::take(&mut self.journal_query)); + if journaled && self.depth == 0 && self.active().is_none_or(|d| d.interaction.is_none()) { let p = self.noted(params); self.journal.push((id.to_string(), p)); } @@ -1818,6 +1824,14 @@ impl Session { } } + /// Journal the running query command when it is the top-level command: it changed the + /// selection (`document.find {select: true}`), so an action recorded around it repeats that. + pub(crate) fn journal_query(&mut self) { + if self.depth == 1 { + self.journal_query = true; + } + } + /// Run `id` as a step of the running command, which journals its steps (`command.batch`): /// → the step's result and its params with what it noted ([`Session::note_journal`]), for the /// step in the running command's journal entry. diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index f0c081ef6..a96733cb4 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -2616,6 +2616,156 @@ fn document_find_searches_names_kinds_and_text() { assert_eq!(s.execute("document.find", &json!({"kind": "nope"})).unwrap()["total"], 0); } +/// `document.find` filters by geometric bounds (inside or touching a rectangle, width, height, +/// thickness and area) and by anchor count, and `select: true` selects the matches that can be +/// selected. +#[test] +fn document_find_filters_by_geometry_and_selects() { + let mut s = session(); + let big = rect(&mut s, 0.0, 0.0, 100.0, 100.0).0; + let sliver = rect(&mut s, 200.0, 0.0, 95.0, 0.9).0; + let star = s.execute("shape.star", &json!({"cx": 400, "cy": 50, "radius1": 40, "radius2": 20, "points": 5})).unwrap()["id"].as_u64().unwrap(); + let left = rect(&mut s, 600.0, 0.0, 10.0, 10.0).0; + let right = rect(&mut s, 620.0, 0.0, 10.0, 10.0).0; + let compound = s.execute("object.compoundPath.make", &json!({"ids": [left, right]})).unwrap()["id"].as_u64().unwrap(); + let hidden = rect(&mut s, 0.0, 300.0, 5.0, 5.0).0; + s.execute("object.hide", &json!({"ids": [hidden]})).unwrap(); + let locked = rect(&mut s, 20.0, 300.0, 5.0, 5.0).0; + s.execute("object.lock", &json!({"ids": [locked]})).unwrap(); + let dot = rect(&mut s, 40.0, 300.0, 5.0, 5.0).0; + let found = |s: &mut Session, p: Value| { + let v = s.execute("document.find", &p).unwrap(); + let mut ids: Vec = v["matches"].as_array().unwrap().iter().map(|m| m["id"].as_u64().unwrap()).collect(); + ids.sort_unstable(); + assert_eq!(v["total"], ids.len(), "{p}"); + ids + }; + let sorted = |mut v: Vec| { + v.sort_unstable(); + v + }; + + // Edges count as inside; a rectangle touching the bounds intersects them. + assert_eq!(found(&mut s, json!({"within": [0, 0, 100, 100]})), [big]); + assert_eq!(found(&mut s, json!({"within": [0, 0, 99, 100]})), Vec::::new()); + assert_eq!(found(&mut s, json!({"intersects": [100, 0, 100, 0.5], "kind": "rectangle"})), sorted(vec![big, sliver])); + // Size: a thin sliver of either orientation, and the bounds' area. + assert_eq!(found(&mut s, json!({"maxThickness": 1})), [sliver]); + assert_eq!(found(&mut s, json!({"maxHeight": 1})), [sliver]); + assert_eq!(found(&mut s, json!({"minWidth": 90, "maxWidth": 96})), [sliver]); + assert_eq!(found(&mut s, json!({"minArea": 9000, "maxArea": 10000})), [big]); + assert_eq!(found(&mut s, json!({"maxArea": 30})), sorted(vec![hidden, locked, dot])); + // Anchors: a compound path counts its members' together; the layer never matches. + assert_eq!(found(&mut s, json!({"minAnchors": 10, "maxAnchors": 10})), [star]); + assert_eq!(found(&mut s, json!({"minAnchors": 8, "maxAnchors": 8})), [compound]); + let four = sorted(vec![big, sliver, left, right, hidden, locked, dot]); + assert_eq!(found(&mut s, json!({"maxAnchors": 4})), four); + + // A find that only reads is not journaled; one that selects is, so an action replays it. + let journaled = s.journal.len(); + s.execute("document.find", &json!({"maxAnchors": 4})).unwrap(); + assert_eq!(s.journal.len(), journaled); + // select: every match except hidden, locked and compound members; the reply counts them. + let v = s.execute("document.find", &json!({"maxAnchors": 4, "select": true, "limit": 0})).unwrap(); + assert_eq!(s.journal_for_action(journaled), [("document.find".to_string(), json!({"maxAnchors": 4, "select": true, "limit": 0}))]); + assert_eq!((v["total"].clone(), v["selected"].clone()), (json!(7), json!(3))); + let selection = |s: &Session| sorted(s.doc().unwrap().selection.objects.iter().map(|i| i.0).collect()); + assert_eq!(selection(&s), sorted(vec![big, sliver, dot])); + // Without select the selection stays, and the reply has no `selected`. + let v = s.execute("document.find", &json!({"maxThickness": 1})).unwrap(); + assert!(v.get("selected").is_none(), "{v}"); + assert_eq!(selection(&s), sorted(vec![big, sliver, dot])); + // Select > Reselect repeats the find. + s.execute("select.none", &json!({})).unwrap(); + s.execute("select.reselect", &json!({})).unwrap(); + assert_eq!(selection(&s), sorted(vec![big, sliver, dot])); + // A compound path matched selects as itself. + s.execute("document.find", &json!({"minAnchors": 8, "maxAnchors": 8, "select": true})).unwrap(); + assert_eq!(selection(&s), [compound]); + // Inside an isolated compound path its members are what selects. + s.execute("object.isolate", &json!({"id": compound})).unwrap(); + let v = s.execute("document.find", &json!({"maxAnchors": 4, "select": true})).unwrap(); + assert_eq!(v["selected"], 2); + assert_eq!(selection(&s), sorted(vec![left, right])); + + for bad in [ + json!({"within": [0, 0, -1, 5]}), + json!({"within": [0, 0, 1]}), + json!({"intersects": "everywhere"}), + json!({"minArea": -1}), + json!({"maxWidth": "5 pt"}), + json!({"minWidth": 5, "maxWidth": 1}), + json!({"minAnchors": 1.5}), + json!({"maxThickness": 1, "select": "yes"}), + json!({"select": true}), + ] { + assert!(s.execute("document.find", &bad).is_err(), "{bad}"); + } +} + +/// `document.find {select: true}` counts the objects it selected: a guide path that Lock Guides +/// leaves out of the selection is a match, not a selected object. +#[test] +fn document_find_counts_the_objects_it_selected() { + let mut s = session(); + let guide = rect(&mut s, 0.0, 0.0, 5.0, 5.0); + s.execute("select.set", &json!({"ids": [guide.0]})).unwrap(); + s.execute("view.guides.make", &json!({})).unwrap(); + rect(&mut s, 20.0, 0.0, 5.0, 5.0); + s.execute("view.guides.lock", &json!({"locked": true})).unwrap(); + let v = s.execute("document.find", &json!({"maxArea": 30, "select": true})).unwrap(); + assert_eq!((v["total"].clone(), v["selected"].clone()), (json!(2), json!(1))); + assert_eq!(s.doc().unwrap().selection.objects.len(), 1); +} + +/// Select > Object reports the number of objects it selected: a guide path that Lock Guides +/// leaves out is not counted. +#[test] +fn select_object_counts_the_objects_it_selected() { + let mut s = session(); + let line = |s: &mut Session, x: f64| { + let id = s.execute("path.create", &json!({"anchors": [{"x": x, "y": 0}, {"x": x + 10.0, "y": 10}]})).unwrap()["id"].as_u64().unwrap(); + s.execute("brush.apply", &json!({"name": "Charcoal", "ids": [id]})).unwrap(); + id + }; + let guide = line(&mut s, 0.0); + s.execute("select.set", &json!({"ids": [guide]})).unwrap(); + s.execute("view.guides.make", &json!({})).unwrap(); + line(&mut s, 20.0); + s.execute("view.guides.lock", &json!({"locked": true})).unwrap(); + for cmd in ["select.object.openPaths", "select.object.brushStrokes"] { + let v = s.execute(cmd, &json!({})).unwrap(); + assert_eq!((v["count"].clone(), s.doc().unwrap().selection.objects.len()), (json!(1), 1), "{cmd}"); + } +} + +/// The geometric filters of `document.find` match objects only: a layer or sublayer whose bounds +/// fit is not a match, so `total` counts what `select: true` can select. The text filters still +/// find layers. +#[test] +fn document_find_geometric_filters_leave_out_layers() { + let mut s = session(); + let a = rect(&mut s, 0.0, 0.0, 5.0, 5.0).0; + let sub = s.execute("layer.newSublayer", &json!({})).unwrap()["id"].as_u64().unwrap(); + let b = rect(&mut s, 10.0, 0.0, 5.0, 5.0).0; + assert_eq!(s.doc().unwrap().doc.parent_of(NodeId(b)), Some(NodeId(sub))); + let found = |s: &mut Session, p: Value| { + let v = s.execute("document.find", &p).unwrap(); + let mut ids: Vec = v["matches"].as_array().unwrap().iter().map(|m| m["id"].as_u64().unwrap()).collect(); + ids.sort_unstable(); + assert_eq!(v["total"], ids.len(), "{p}"); + ids + }; + assert_eq!(found(&mut s, json!({"within": [0, 0, 15, 5]})), [a, b]); + assert_eq!(found(&mut s, json!({"intersects": [0, 0, 1, 1]})), [a]); + assert_eq!(found(&mut s, json!({"maxArea": 30})), [a, b]); + assert_eq!(found(&mut s, json!({"minWidth": 15})), Vec::::new()); + assert_eq!(found(&mut s, json!({"kind": "layer", "within": [0, 0, 15, 5]})), Vec::::new()); + assert_eq!(found(&mut s, json!({"kind": "layer"})).len(), 2); + let v = s.execute("document.find", &json!({"within": [0, 0, 15, 5], "select": true})).unwrap(); + assert_eq!((v["total"].clone(), v["selected"].clone()), (json!(2), json!(2))); +} + /// Type reports the paint its characters show, and its own object-level paint apart from it. #[test] fn inspect_reports_the_paint_of_types_characters() { diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 5110aa03e..35523a55b 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -4204,6 +4204,12 @@ imageTrace Outline Obrys @msg `{key}` must be between {min} and {max} (got {x}) `{key}` musí být mezi {min} a {max} (zadáno {x}) @msg `{key}` must be one of {_1} `{key}` musí být jedno z {_1} @msg `{key}` must be true or false `{key}` musí být true nebo false +@msg `select` must be true or false `select` musí být true nebo false +@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` a `maxAnchors` musí být celá čísla +@msg `{key}` must be a number not below 0 `{key}` musí být číslo nejméně 0 +@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` musí být [x, y, width, height]: čtyři konečná čísla, šířka a výška nesmí být záporné +@msg `{min_key}` is greater than `{max_key}` `{min_key}` je větší než `{max_key}` +@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` zadejte alespoň jeden z parametrů `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @msg `mode` must be grayscale, rgb, hsb, cmyk or websafe `mode` musí být grayscale, rgb, hsb, cmyk nebo websafe @msg `{k}` must be [x, y] `{k}` musí být [x, y] @msg `{name}` has no options `{name}` nemá žádné možnosti diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 899a9b7c4..7af235054 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -3825,6 +3825,12 @@ theme Appearance Erscheinungsbild @msg `{key}` must be between {min} and {max} (got {x}) `{key}` muss zwischen {min} und {max} liegen (erhalten: {x}) @msg `{key}` must be one of {_1} `{key}` muss einer der folgenden Werte sein: {_1} @msg `{key}` must be true or false `{key}` muss true oder false sein +@msg `select` must be true or false `select` muss true oder false sein +@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` und `maxAnchors` müssen ganze Zahlen sein +@msg `{key}` must be a number not below 0 `{key}` muss eine Zahl nicht kleiner als 0 sein +@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` muss [x, y, width, height] sein: vier endliche Zahlen, Breite und Höhe nicht negativ +@msg `{min_key}` is greater than `{max_key}` `{min_key}` ist größer als `{max_key}` +@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` geben Sie mindestens einen der Parameter `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` an @msg `mode` must be grayscale, rgb, hsb, cmyk or websafe `mode` muss grayscale, rgb, hsb, cmyk oder websafe sein @msg `{k}` must be [x, y] `{k}` muss [x, y] sein @msg `{name}` has no options `{name}` hat keine Optionen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 98bf3c71b..70b097d7c 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2167,6 +2167,12 @@ axis Both Και τα δύο @msg `{key}` must be between {min} and {max} (got {x}) Το `{key}` πρέπει να είναι μεταξύ {min} και {max} (δόθηκε {x}) @msg `{key}` must be one of {_1} Το `{key}` πρέπει να είναι ένα από τα {_1} @msg `{key}` must be true or false Το `{key}` πρέπει να είναι true ή false +@msg `select` must be true or false Το `select` πρέπει να είναι true ή false +@msg `minAnchors` and `maxAnchors` must be whole numbers Τα `minAnchors` και `maxAnchors` πρέπει να είναι ακέραιοι αριθμοί +@msg `{key}` must be a number not below 0 Το `{key}` πρέπει να είναι αριθμός όχι μικρότερος από 0 +@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative Το `{key}` πρέπει να είναι [x, y, width, height]: τέσσερις πεπερασμένοι αριθμοί, με πλάτος και ύψος όχι αρνητικά +@msg `{min_key}` is greater than `{max_key}` Το `{min_key}` είναι μεγαλύτερο από το `{max_key}` +@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` δώστε τουλάχιστον ένα από τα `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @msg `{k}` must be [x, y] Το `{k}` πρέπει να είναι [x, y] @msg `{name}` has no options Το `{name}` δεν έχει επιλογές @msg `{name}` is a built-in workspace; choose another name Το `{name}` είναι ενσωματωμένος χώρος εργασίας· επιλέξτε άλλο όνομα diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 3105cc9f9..36fba6c20 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -3901,6 +3901,12 @@ axis Both Ambos @msg `{key}` must be between {min} and {max} (got {x}) `{key}` debe estar entre {min} y {max} (se recibió {x}) @msg `{key}` must be one of {_1} `{key}` debe ser uno de estos: {_1} @msg `{key}` must be true or false `{key}` debe ser true o false +@msg `select` must be true or false `select` debe ser true o false +@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` y `maxAnchors` deben ser números enteros +@msg `{key}` must be a number not below 0 `{key}` debe ser un número no inferior a 0 +@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` debe ser [x, y, width, height]: cuatro números finitos, con anchura y altura no negativas +@msg `{min_key}` is greater than `{max_key}` `{min_key}` es mayor que `{max_key}` +@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` indica al menos uno de `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @msg `mode` must be grayscale, rgb, hsb, cmyk or websafe `mode` debe ser grayscale, rgb, hsb, cmyk o websafe @msg `{k}` must be [x, y] `{k}` debe ser [x, y] @msg `{name}` has no options `{name}` no tiene opciones diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 9a0c1d1d7..462114e75 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -4122,6 +4122,12 @@ imageTrace Outline Ääriviiva @msg `{key}` must be between {min} and {max} (got {x}) `{key}` on oltava väliltä {min}–{max} (saatiin {x}) @msg `{key}` must be one of {_1} `{key}` on oltava jokin seuraavista: {_1} @msg `{key}` must be true or false `{key}` on oltava true tai false +@msg `select` must be true or false `select` on oltava true tai false +@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` ja `maxAnchors` on oltava kokonaislukuja +@msg `{key}` must be a number not below 0 `{key}` on oltava luku, joka on vähintään 0 +@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` on oltava [x, y, width, height]: neljä äärellistä lukua, joista leveys ja korkeus eivät ole negatiivisia +@msg `{min_key}` is greater than `{max_key}` `{min_key}` on suurempi kuin `{max_key}` +@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` anna vähintään yksi parametreista `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @msg `{k}` must be [x, y] `{k}` on oltava [x, y] @msg `{name}` has no options `{name}`: ei asetuksia @msg `{name}` is a built-in workspace; choose another name `{name}` on sisäänrakennettu työtila; valitse toinen nimi diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index aa3e999f6..80f0a0699 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3898,6 +3898,12 @@ axis Both Les deux @msg `{key}` must be between {min} and {max} (got {x}) `{key}` doit être compris entre {min} et {max} (reçu : {x}) @msg `{key}` must be one of {_1} `{key}` doit valoir l’une de ces valeurs : {_1} @msg `{key}` must be true or false `{key}` doit valoir true ou false +@msg `select` must be true or false `select` doit valoir true ou false +@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` et `maxAnchors` doivent être des nombres entiers +@msg `{key}` must be a number not below 0 `{key}` doit être un nombre supérieur ou égal à 0 +@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` doit être [x, y, width, height] : quatre nombres finis, largeur et hauteur non négatives +@msg `{min_key}` is greater than `{max_key}` `{min_key}` est supérieur à `{max_key}` +@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` indiquez au moins l’un des paramètres `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @msg `mode` must be grayscale, rgb, hsb, cmyk or websafe `mode` doit valoir grayscale, rgb, hsb, cmyk ou websafe @msg `{k}` must be [x, y] `{k}` doit être [x, y] @msg `{name}` has no options `{name}` n’a pas d’options diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index a1f499126..e7e45cf15 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -4434,6 +4434,12 @@ imageTrace Outline Körvonal @msg `{key}` must be between {min} and {max} (got {x}) `{key}`: {min} és {max} közötti érték kell (kapott érték: {x}) @msg `{key}` must be one of {_1} `{key}`: ezek egyike kell: {_1} @msg `{key}` must be true or false `{key}`: true vagy false kell +@msg `select` must be true or false `select`: true vagy false kell +@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` és `maxAnchors`: egész szám kell +@msg `{key}` must be a number not below 0 `{key}`: legalább 0 értékű szám kell +@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}`: [x, y, width, height] kell, négy véges szám, a szélesség és a magasság nem negatív +@msg `{min_key}` is greater than `{max_key}` `{min_key}` nagyobb, mint `{max_key}` +@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` adj meg legalább egyet ezek közül: `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @msg `mode` must be grayscale, rgb, hsb, cmyk or websafe `mode`: grayscale, rgb, hsb, cmyk vagy websafe kell @msg `{k}` must be [x, y] `{k}`: [x, y] kell @msg `{name}` has no options ennek nincsenek beállításai: `{name}` diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 5c2ba5757..0a8678bdd 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -3941,6 +3941,12 @@ axis Both Entrambi @msg `{key}` must be between {min} and {max} (got {x}) `{key}` deve essere compreso tra {min} e {max} (ricevuto {x}) @msg `{key}` must be one of {_1} `{key}` deve essere uno dei seguenti: {_1} @msg `{key}` must be true or false `{key}` deve essere true o false +@msg `select` must be true or false `select` deve essere true o false +@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` e `maxAnchors` devono essere numeri interi +@msg `{key}` must be a number not below 0 `{key}` deve essere un numero non inferiore a 0 +@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` deve essere [x, y, width, height]: quattro numeri finiti, con larghezza e altezza non negative +@msg `{min_key}` is greater than `{max_key}` `{min_key}` è maggiore di `{max_key}` +@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` indica almeno uno tra `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @msg `mode` must be grayscale, rgb, hsb, cmyk or websafe `mode` deve essere grayscale, rgb, hsb, cmyk o websafe @msg `{k}` must be [x, y] `{k}` deve essere [x, y] @msg `{name}` has no options `{name}` non ha opzioni diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 6735831f0..daf1e2587 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4437,6 +4437,12 @@ swatch Bright Violet ビビッドバイオレット @msg `command` must be a command whose menu item opens a dialog (see ui.menuDialog) `command` には、メニュー項目がダイアログを開くコマンドを指定してください (ui.menuDialog を参照) @msg `mode` must be grayscale, rgb, hsb, cmyk or websafe `mode` には grayscale、rgb、hsb、cmyk、websafe のいずれかを指定してください @msg `{key}` must be true or false `{key}` には true または false を指定してください +@msg `select` must be true or false `select` には true または false を指定してください +@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` と `maxAnchors` は整数にしてください +@msg `{key}` must be a number not below 0 `{key}` には 0 以上の数値を指定してください +@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` は [x, y, width, height] の形式で指定してください: 有限の数値 4 つで、幅と高さは負の値にできません +@msg `{min_key}` is greater than `{max_key}` `{min_key}` が `{max_key}` より大きくなっています +@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` 次のうち少なくとも 1 つを指定してください: `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @msg brightness must be dark|mediumDark|mediumLight|light brightness には dark|mediumDark|mediumLight|light のいずれかを指定してください @msg index must be a number or first, previous, next or last index には数値、または first、previous、next、last のいずれかを指定してください @msg index must be a number or first, previous, next or last, not `{other}` index には数値、または first、previous、next、last のいずれかを指定してください (`{other}` は指定できません) diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index f41eb3993..c0f789a43 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -3695,6 +3695,12 @@ font-technology Variable ცვლადი @msg `{key}` must be between {min} and {max} (got {x}) `{key}` უნდა იყოს {min}-სა და {max}-ს შორის (მიღებულია {x}) @msg `{key}` must be one of {_1} `{key}` უნდა იყოს ერთ-ერთი: {_1} @msg `{key}` must be true or false `{key}` უნდა იყოს true ან false +@msg `select` must be true or false `select` უნდა იყოს true ან false +@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` და `maxAnchors` უნდა იყოს მთელი რიცხვები +@msg `{key}` must be a number not below 0 `{key}` უნდა იყოს რიცხვი, არანაკლებ 0 +@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` უნდა იყოს [x, y, width, height]: ოთხი სასრული რიცხვი, სიგანე და სიმაღლე არ უნდა იყოს უარყოფითი +@msg `{min_key}` is greater than `{max_key}` `{min_key}` მეტია, ვიდრე `{max_key}` +@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` მიუთითეთ ერთი მაინც: `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @msg `{k}` must be [x, y] `{k}` უნდა იყოს [x, y] @msg `{name}` has no options `{name}`-ს პარამეტრები არ აქვს @msg `{name}` is a built-in workspace; choose another name `{name}` ჩაშენებული სამუშაო სივრცეა; აირჩიეთ სხვა სახელი diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index ca100edf6..23318f6ed 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4369,6 +4369,12 @@ theme Appearance 외관 @msg `command` must be a command whose menu item opens a dialog (see ui.menuDialog) `command`는 메뉴 항목이 대화상자를 여는 명령이어야 합니다(ui.menuDialog 참조) @msg `mode` must be grayscale, rgb, hsb, cmyk or websafe `mode`은(는) grayscale, rgb, hsb, cmyk, websafe 중 하나여야 합니다 @msg `{key}` must be true or false `{key}`은(는) true 또는 false여야 합니다 +@msg `select` must be true or false `select`은(는) true 또는 false여야 합니다 +@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors`와 `maxAnchors`는 정수여야 합니다 +@msg `{key}` must be a number not below 0 `{key}`은(는) 0 이상의 숫자여야 합니다 +@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}`은(는) [x, y, width, height]여야 합니다: 유한한 숫자 네 개이며 너비와 높이는 음수가 아니어야 합니다 +@msg `{min_key}` is greater than `{max_key}` `{min_key}`이(가) `{max_key}`보다 큽니다 +@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` 다음 중 하나 이상을 지정하세요: `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @msg brightness must be dark|mediumDark|mediumLight|light brightness는 dark|mediumDark|mediumLight|light 중 하나여야 합니다 @msg index must be a number or first, previous, next or last index는 숫자이거나 first, previous, next, last 중 하나여야 합니다 @msg index must be a number or first, previous, next or last, not `{other}` index는 숫자이거나 first, previous, next, last 중 하나여야 하며 `{other}`은(는) 안 됩니다 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index b60324fd1..2b918e31f 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -4026,6 +4026,12 @@ imageTrace Outline Kontur @msg `{key}` must be between {min} and {max} (got {x}) `{key}` musi mieścić się w zakresie od {min} do {max} (podano {x}) @msg `{key}` must be one of {_1} `{key}` musi mieć jedną z wartości: {_1} @msg `{key}` must be true or false `{key}` musi mieć wartość true lub false +@msg `select` must be true or false `select` musi mieć wartość true lub false +@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` i `maxAnchors` muszą być liczbami całkowitymi +@msg `{key}` must be a number not below 0 `{key}` musi być liczbą nie mniejszą niż 0 +@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` musi mieć postać [x, y, width, height]: cztery skończone liczby, szerokość i wysokość nieujemne +@msg `{min_key}` is greater than `{max_key}` `{min_key}` jest większe niż `{max_key}` +@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` podaj co najmniej jeden z parametrów `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @msg `mode` must be grayscale, rgb, hsb, cmyk or websafe `mode` musi mieć wartość grayscale, rgb, hsb, cmyk lub websafe @msg `{k}` must be [x, y] `{k}` musi mieć postać [x, y] @msg `{name}` has no options `{name}` nie ma opcji diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 945e43bc3..1157f6868 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -3483,6 +3483,12 @@ axis Both Ambos @msg `{key}` must be between {min} and {max} (got {x}) `{key}` deve estar entre {min} e {max} (recebido {x}) @msg `{key}` must be one of {_1} `{key}` deve ser um de {_1} @msg `{key}` must be true or false `{key}` deve ser true ou false +@msg `select` must be true or false `select` deve ser true ou false +@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` e `maxAnchors` devem ser números inteiros +@msg `{key}` must be a number not below 0 `{key}` deve ser um número não inferior a 0 +@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` deve ser [x, y, width, height]: quatro números finitos, com largura e altura não negativas +@msg `{min_key}` is greater than `{max_key}` `{min_key}` é maior que `{max_key}` +@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` informe pelo menos um de `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @msg `{k}` must be [x, y] `{k}` deve ser [x, y] @msg `{name}` has no options `{name}` não tem opções @msg `{name}` is a built-in workspace; choose another name `{name}` é um espaço de trabalho integrado; escolha outro nome diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 2d9f41824..907f458c4 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -3937,6 +3937,12 @@ axis Both Оба @msg `{key}` must be between {min} and {max} (got {x}) `{key}` должно быть от {min} до {max} (получено {x}) @msg `{key}` must be one of {_1} `{key}` должно быть одним из: {_1} @msg `{key}` must be true or false `{key}` должно быть true или false +@msg `select` must be true or false `select` должно быть true или false +@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` и `maxAnchors` должны быть целыми числами +@msg `{key}` must be a number not below 0 `{key}` должно быть числом не меньше 0 +@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` должно быть [x, y, width, height]: четыре конечных числа, ширина и высота не отрицательные +@msg `{min_key}` is greater than `{max_key}` `{min_key}` больше, чем `{max_key}` +@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` укажите хотя бы один из параметров `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @msg `mode` must be grayscale, rgb, hsb, cmyk or websafe `mode` должно быть grayscale, rgb, hsb, cmyk или websafe @msg `{k}` must be [x, y] `{k}` должно быть [x, y] @msg `{name}` has no options `{name}` не имеет параметров diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index b8c197520..c493195b5 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -3821,6 +3821,12 @@ axis Both Båda @msg `{key}` must be between {min} and {max} (got {x}) `{key}` måste vara mellan {min} och {max} (fick {x}) @msg `{key}` must be one of {_1} `{key}` måste vara något av {_1} @msg `{key}` must be true or false `{key}` måste vara true eller false +@msg `select` must be true or false `select` måste vara true eller false +@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` och `maxAnchors` måste vara heltal +@msg `{key}` must be a number not below 0 `{key}` måste vara ett tal som inte är under 0 +@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` måste vara [x, y, width, height]: fyra ändliga tal, där bredd och höjd inte är negativa +@msg `{min_key}` is greater than `{max_key}` `{min_key}` är större än `{max_key}` +@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` ange minst en av `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @msg `{k}` must be [x, y] `{k}` måste vara [x, y] @msg `{name}` has no options `{name}` har inga alternativ @msg `{name}` is a built-in workspace; choose another name `{name}` är en inbyggd arbetsyta; välj ett annat namn diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 33874a03a..719fb0ca4 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -3705,6 +3705,12 @@ axis Both Her İkisi @msg `{key}` must be between {min} and {max} (got {x}) `{key}`, {min} ile {max} arasında olmalıdır ({x}'yi aldı) @msg `{key}` must be one of {_1} `{key}`, {_1}'den biri olmalıdır @msg `{key}` must be true or false "{key}" doğru veya yanlış olmalıdır +@msg `select` must be true or false `select` true veya false olmalıdır +@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` ve `maxAnchors` tam sayı olmalıdır +@msg `{key}` must be a number not below 0 `{key}` 0'dan küçük olmayan bir sayı olmalıdır +@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` [x, y, width, height] olmalıdır: dört sonlu sayı, genişlik ve yükseklik negatif olmamalı +@msg `{min_key}` is greater than `{max_key}` `{min_key}`, `{max_key}` değerinden büyük +@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` şunlardan en az birini verin: `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @msg `mode` must be grayscale, rgb, hsb, cmyk or websafe "mod" gri tonlamalı, rgb, hsb, cmyk veya web uyumlu olmalıdır @msg `{k}` must be [x, y] "{k}" [x, y] olmalıdır @msg `{name}` has no options `{name}`nin seçeneği yok diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index ea830de1c..05bd282b1 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -3925,6 +3925,12 @@ axis Both Обидві @msg `{key}` must be between {min} and {max} (got {x}) `{key}` має бути від {min} до {max} (отримано {x}) @msg `{key}` must be one of {_1} `{key}` має бути одним з: {_1} @msg `{key}` must be true or false `{key}` має бути true або false +@msg `select` must be true or false `select` має бути true або false +@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` і `maxAnchors` мають бути цілими числами +@msg `{key}` must be a number not below 0 `{key}` має бути числом, не меншим за 0 +@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` має бути [x, y, width, height]: чотири скінченні числа, ширина й висота невід’ємні +@msg `{min_key}` is greater than `{max_key}` `{min_key}` більше за `{max_key}` +@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` укажіть принаймні один із параметрів `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @msg `mode` must be grayscale, rgb, hsb, cmyk or websafe `mode` має бути grayscale, rgb, hsb, cmyk або websafe @msg `{k}` must be [x, y] `{k}` має бути [x, y] @msg `{name}` has no options `{name}` не має параметрів diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 53216bd00..ad1c84997 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4082,6 +4082,12 @@ theme Appearance Giao diện @msg `{key}` must be between {min} and {max} (got {x}) `{key}` phải nằm trong khoảng {min} đến {max} (nhận được {x}) @msg `{key}` must be one of {_1} `{key}` phải là một trong {_1} @msg `{key}` must be true or false `{key}` phải là true hoặc false +@msg `select` must be true or false `select` phải là true hoặc false +@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` và `maxAnchors` phải là số nguyên +@msg `{key}` must be a number not below 0 `{key}` phải là một số không nhỏ hơn 0 +@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` phải là [x, y, width, height]: bốn số hữu hạn, chiều rộng và chiều cao không âm +@msg `{min_key}` is greater than `{max_key}` `{min_key}` lớn hơn `{max_key}` +@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` hãy cung cấp ít nhất một trong `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @msg `mode` must be grayscale, rgb, hsb, cmyk or websafe `mode` phải là grayscale, rgb, hsb, cmyk hoặc websafe @msg `{k}` must be [x, y] `{k}` phải là [x, y] @msg `{name}` has no options `{name}` không có tùy chọn diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index f3b42de41..1ab5c4669 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -4162,6 +4162,12 @@ imageTrace Outline 轮廓 @msg `command` must be a command whose menu item opens a dialog (see ui.menuDialog) `command` 必须是其菜单项会打开对话框的命令 (请参阅 ui.menuDialog) @msg `mode` must be grayscale, rgb, hsb, cmyk or websafe `mode` 必须是 grayscale、rgb、hsb、cmyk 或 websafe @msg `{key}` must be true or false `{key}` 必须是 true 或 false +@msg `select` must be true or false `select` 必须是 true 或 false +@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` 和 `maxAnchors` 必须是整数 +@msg `{key}` must be a number not below 0 `{key}` 必须是不小于 0 的数字 +@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` 必须是 [x, y, width, height]:四个有限数字,宽度和高度不能为负 +@msg `{min_key}` is greater than `{max_key}` `{min_key}` 大于 `{max_key}` +@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` 请至少提供以下其中之一:`name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @msg brightness must be dark|mediumDark|mediumLight|light brightness 必须是 dark|mediumDark|mediumLight|light @msg index must be a number or first, previous, next or last index 必须是数字,或 first、previous、next 或 last @msg index must be a number or first, previous, next or last, not `{other}` index 必须是数字,或 first、previous、next 或 last,而不是 `{other}` diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index f090d9dad..95cab4a89 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2143,6 +2143,12 @@ file-filter Templates 範本 @msg `command` must be a command whose menu item opens a dialog (see ui.menuDialog) `command` 必須是選單項目會開啟對話框的指令 (請參閱 ui.menuDialog) @msg `mode` must be grayscale, rgb, hsb, cmyk or websafe `mode` 必須是 grayscale、rgb、hsb、cmyk 或 websafe @msg `{key}` must be true or false `{key}` 必須是 true 或 false +@msg `select` must be true or false `select` 必須是 true 或 false +@msg `minAnchors` and `maxAnchors` must be whole numbers `minAnchors` 和 `maxAnchors` 必須是整數 +@msg `{key}` must be a number not below 0 `{key}` 必須是不小於 0 的數字 +@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` 必須是 [x, y, width, height]:四個有限數字,寬度和高度不能為負 +@msg `{min_key}` is greater than `{max_key}` `{min_key}` 大於 `{max_key}` +@msg give at least one of `name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` 請至少提供下列其中之一:`name`, `kind`, `text`, `within`, `intersects`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minThickness`, `maxThickness`, `minArea`, `maxArea`, `minAnchors`, `maxAnchors` @msg brightness must be dark|mediumDark|mediumLight|light brightness 必須是 dark|mediumDark|mediumLight|light @msg index must be a number or first, previous, next or last index 必須是數字,或 first、previous、next 或 last @msg index must be a number or first, previous, next or last, not `{other}` index 必須是數字,或 first、previous、next 或 last,而非 `{other}` diff --git a/docs/mcp.md b/docs/mcp.md index ae4eb5778..d9f8250bd 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -219,7 +219,8 @@ node (layers and groups too) reports `opacity` when it is below 1 and `blend` wh On a large document, drill instead of dumping: `inspect_document {depth: 0}` (or `run_command document.inspect {depth?, childLimit?}`, same rules) is the skeleton (artboards with top layers and counts), `run_command -document.find {name?, kind?, text?, limit?}` locates nodes across the whole tree, and `document.node` reads the +document.find {name?, kind?, text?, within?, intersects?, minArea?, …, select?, limit?}` locates nodes across the +whole tree, and `document.node` reads the interesting ones sliced. `document.inspect` and the `doc_inspect` / `inspect_document` tools also scope the reply themselves: `ids: [id…]` lists just those nodes (any level, in the order given, an unknown id is an error, at most 10000), `kind` lists every node of one panel label (ignoring case; within the subtrees of `ids` when both @@ -230,9 +231,21 @@ top layers (`layers` and `total`). Without these options the reply is the whole `document.find` needs at least one filter and all given filters must match; matching ignores case, `name` is a substring of the Layers panel name, `text` a substring of type content and `kind` the exact panel label (`Layer`, `Group`, `Clip Group`, `Path`, `Compound Path`, `Type`, `Image`, `Rectangle`, ...). +The geometric filters read each object's geometric bounds, in the document points `document.inspect` reports +(groups included; layers, sublayers and a node without bounds do not match): `within: [x, y, width, height]` keeps +objects inside that rectangle (edges included), `intersects: [x, y, width, height]` objects touching or overlapping it, +`minWidth` / `maxWidth` and `minHeight` / `maxHeight` limit the bounds' size, `minThickness` / `maxThickness` +the smaller of width and height (thin slivers of either orientation), and `minArea` / `maxArea` width × height. +`minAnchors` / `maxAnchors` count a path's anchor points, a compound path's members together; other nodes do not +match them. Limits are inclusive, not negative, and a minimum above its maximum is an error. `select: true` +also selects the matches, leaving out hidden and locked ones, those inside another match, and a compound path's +members unless the compound path is isolated; Select › Reselect repeats the find, and an action being recorded +records it as a step (a find without `select` is not recorded). For example, +`document.find {maxThickness: 1, kind: "path", select: true}` followed by `edit.clear` deletes hairline slivers. It answers `{matches: [{id, name, kind, path}], total}` top of the stack first, where `path` is the ancestor -chain (layer first) and `limit` caps `matches` (default 100; `0` only counts). `document.json` stays whole: it is -the fidelity path, not the way to look around. +chain (layer first) and `limit` caps `matches` (default 100; `0` only counts); with `select` the reply adds +`selected`, the number of objects selected. `document.json` stays whole: it is the fidelity path, not the way to +look around. ### Completions