diff --git a/crates/engine/src/cmd/addons.rs b/crates/engine/src/cmd/addons.rs index 02f886442..ac9597cbd 100644 --- a/crates/engine/src/cmd/addons.rs +++ b/crates/engine/src/cmd/addons.rs @@ -7,6 +7,7 @@ mod color; mod document; mod pathtools; mod pen; +mod select; mod typetools; use super::CommandSpec; @@ -19,6 +20,7 @@ pub fn specs() -> Vec { v.extend(document::specs()); v.extend(pathtools::specs()); v.extend(pen::specs()); + v.extend(select::specs()); v.extend(typetools::specs()); v } diff --git a/crates/engine/src/cmd/addons/select.rs b/crates/engine/src/cmd/addons/select.rs new file mode 100644 index 000000000..ce2869abc --- /dev/null +++ b/crates/engine/src/cmd/addons/select.rs @@ -0,0 +1,63 @@ +//! Selection Tools (`addon.select.*`), a built-in add-on: Select by Artboards selects the objects +//! overlapping, fully inside or fully outside one or more chosen artboards (#5525). Its submenu +//! (Object › Plug-ins › Selection Tools) and dialog are the UI's. + +#[cfg(test)] +mod tests; + +use serde_json::{Value, json}; +use vectorcraft_doc::NodeId; +use vectorcraft_geom::Point; + +use super::super::select::{on_artboard, selectable}; +use super::super::{CommandSpec, bad, cmd, has_doc, point_indices, text_param}; +use crate::{Result, Session}; + +/// How Select by Artboards relates an object to the chosen artboards (`relation`). +pub const RELATIONS: [&str; 3] = ["overlap", "inside", "outside"]; + +pub fn specs() -> Vec { + vec![cmd!( + "addon.select.byArtboards", + "Select by Artboards", + ["Object", "Plug-ins", "Selection Tools"], + None, + "{artboards: [index…] (0-based, at least one), relation?: \"overlap\" (default: on at least one of them, as select.allOnArtboard takes it: the bounds overlap the artboard or the centre is on it) | \"inside\" (the bounds lie wholly within one of them, edges included) | \"outside\" (on none of them)} select those objects among the ones Select All takes (in isolation mode, the isolated container's); hidden and locked objects, objects on hidden, locked or template layers and art held by a locked artboard are left out. Bounds are geometric, a symbol instance's measured by its art → {count}", + has_doc, + by_artboards + )] +} + +fn by_artboards(s: &mut Session, p: &Value) -> Result { + const C: &str = "addon.select.byArtboards"; + let relation = text_param(p, "relation", C)?.unwrap_or("overlap"); + if !RELATIONS.contains(&relation) { + return Err(bad(C, "`relation` must be overlap, inside or outside")); + } + let listed = p.get("artboards").ok_or_else(|| bad(C, "missing `artboards`"))?; + let indices = point_indices(listed).ok_or_else(|| bad(C, "`artboards` must be a list of artboard indices"))?; + if indices.is_empty() { + return Err(bad(C, "choose at least one artboard")); + } + let st = s.doc()?; + let rects = + indices.iter().map(|i| st.doc.artboards.get(*i).map(|a| a.rect)).collect::>>().ok_or_else(|| bad(C, "no such artboard"))?; + let symbols = st.doc.symbol_boxes(); + let ids: Vec = selectable(&st.doc, st.isolation) + .into_iter() + .filter(|id| { + let Some(n) = st.doc.node(*id) else { return false }; + match relation { + "inside" => n.geometric_bounds_with(&symbols).is_some_and(|b| { + rects.iter().any(|r| { + vectorcraft_doc::artboard_holds(*r, Point::new(b.x0, b.y0)) && vectorcraft_doc::artboard_holds(*r, Point::new(b.x1, b.y1)) + }) + }), + "outside" => !rects.iter().any(|r| on_artboard(n, &symbols, *r)), + _ => rects.iter().any(|r| on_artboard(n, &symbols, *r)), + } + }) + .collect(); + s.select(|_, sel| sel.set(ids))?; + Ok(json!({ "count": s.doc()?.selection.objects.len() })) +} diff --git a/crates/engine/src/cmd/addons/select/tests.rs b/crates/engine/src/cmd/addons/select/tests.rs new file mode 100644 index 000000000..bea05ff9e --- /dev/null +++ b/crates/engine/src/cmd/addons/select/tests.rs @@ -0,0 +1,128 @@ +//! Select by Artboards. + +use serde_json::{Value, json}; +use vectorcraft_doc::NodeId; + +use crate::Session; + +const C: &str = "addon.select.byArtboards"; + +/// A document with three 100 × 100 artboards in a row, 50 pt apart: 0 at x 0, 1 at x 150, 2 at x 300. +fn three_boards() -> Session { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); + for x in [150, 300] { + s.execute("artboard.new", &json!({"x": x, "y": 0, "width": 100, "height": 100})).unwrap(); + } + s +} + +fn rect(s: &mut Session, x: f64, y: f64, w: f64, h: f64) -> NodeId { + NodeId(s.execute("shape.rectangle", &json!({"x": x, "y": y, "width": w, "height": h})).unwrap()["id"].as_u64().unwrap()) +} + +fn pick(s: &mut Session, p: Value) -> Vec { + s.execute("select.none", &json!({})).unwrap(); + let r = s.execute(C, &p).unwrap(); + let mut ids = s.doc().unwrap().selection.objects.clone(); + assert_eq!(r["count"], json!(ids.len())); + ids.sort(); + ids +} + +#[test] +fn selects_by_overlap_inside_and_outside_several_artboards() { + let mut s = three_boards(); + let a = rect(&mut s, 10.0, 10.0, 20.0, 20.0); // inside 0 + let ab = rect(&mut s, 90.0, 40.0, 80.0, 20.0); // across 0 and 1 + let b = rect(&mut s, 160.0, 10.0, 20.0, 20.0); // inside 1 + let c = rect(&mut s, 310.0, 10.0, 20.0, 20.0); // inside 2 + let off = rect(&mut s, 0.0, 200.0, 20.0, 20.0); // on no board + let edge = rect(&mut s, 0.0, 0.0, 100.0, 100.0); // flush with board 0 + let want = |mut v: Vec| { + v.sort(); + v + }; + assert_eq!(pick(&mut s, json!({"artboards": [0, 1]})), want(vec![a, ab, b, edge])); + assert_eq!(pick(&mut s, json!({"artboards": [0, 1], "relation": "overlap"})), want(vec![a, ab, b, edge])); + // Wholly within one board, edges included; straddling two boards is not inside either. + assert_eq!(pick(&mut s, json!({"artboards": [0, 1], "relation": "inside"})), want(vec![a, b, edge])); + assert_eq!(pick(&mut s, json!({"artboards": [0, 1], "relation": "outside"})), want(vec![c, off])); + assert_eq!(pick(&mut s, json!({"artboards": [2], "relation": "outside"})), want(vec![a, ab, b, off, edge])); + // One board does what All on Active Artboard does. + for i in 0..3u64 { + s.execute("select.allOnArtboard", &json!({"artboard": i})).unwrap(); + let mut on = s.doc().unwrap().selection.objects.clone(); + on.sort(); + assert_eq!(pick(&mut s, json!({"artboards": [i]})), on, "board {i}"); + } + // A board listed twice counts once. + assert_eq!(pick(&mut s, json!({"artboards": [2, 2]})), vec![c]); +} + +#[test] +fn hidden_locked_and_isolated_art_follow_select_all() { + let mut s = three_boards(); + let a = rect(&mut s, 10.0, 10.0, 20.0, 20.0); + let hidden = rect(&mut s, 40.0, 10.0, 20.0, 20.0); + let locked = rect(&mut s, 70.0, 10.0, 20.0, 20.0); + let far = rect(&mut s, 0.0, 300.0, 20.0, 20.0); + s.execute("object.setProps", &json!({"ids": [hidden.0], "visible": false})).unwrap(); + s.execute("object.setProps", &json!({"ids": [locked.0], "locked": true})).unwrap(); + assert_eq!(pick(&mut s, json!({"artboards": [0]})), vec![a]); + assert_eq!(pick(&mut s, json!({"artboards": [0], "relation": "outside"})), vec![far]); + // Art held by a locked artboard is left out. + s.execute("artboard.setProps", &json!({"index": 0, "locked": true})).unwrap(); + assert_eq!(pick(&mut s, json!({"artboards": [0]})), vec![]); + s.execute("artboard.setProps", &json!({"index": 0, "locked": false})).unwrap(); + // In isolation mode only the isolated group's objects are looked at. + let b = rect(&mut s, 20.0, 50.0, 10.0, 10.0); + s.execute("select.none", &json!({})).unwrap(); + s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap(); + let g = s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap(); + s.execute("object.isolate", &json!({"id": g})).unwrap(); + let mut both = vec![a, b]; + both.sort(); + assert_eq!(pick(&mut s, json!({"artboards": [0], "relation": "inside"})), both); + assert_eq!(pick(&mut s, json!({"artboards": [1]})), vec![]); +} + +#[test] +fn bad_params_are_errors_and_leave_the_selection() { + let mut s = three_boards(); + let a = rect(&mut s, 10.0, 10.0, 20.0, 20.0); + for p in [ + json!({}), + json!({"artboards": []}), + json!({"artboards": [3]}), + json!({"artboards": [-1]}), + json!({"artboards": [0.5]}), + json!({"artboards": "0"}), + json!({"artboards": [0], "relation": "near"}), + json!({"artboards": [0], "relation": 1}), + json!({"artboards": [0], "relatoin": "inside"}), + ] { + s.execute("select.set", &json!({"ids": [a.0]})).unwrap(); + assert!(s.execute(C, &p).is_err(), "{p}"); + assert_eq!(s.doc().unwrap().selection.objects, vec![a], "{p}"); + } +} + +#[test] +fn a_symbol_instance_is_measured_by_its_art() { + // The symbol's art, (100, 60) to (140, 80), drawn 50 pt up and left: (50, 10) to (90, 30), on + // the 200 × 120 artboard, while the 20 × 20 square around the instance's origin lies off it. + let svg = r##""##; + let mut s = Session::new(); + s.execute("document.open", &json!({"name": "far.svg", "dataBase64": vectorcraft_format::base64_encode(svg.as_bytes())})).unwrap(); + let mut instance = None; + s.doc().unwrap().doc.walk(|n| { + if matches!(n.kind, vectorcraft_doc::NodeKind::SymbolInstance { .. }) { + instance = Some(n.id); + } + }); + let instance = instance.unwrap(); + assert_eq!(pick(&mut s, json!({"artboards": [0]})), vec![instance]); + assert_eq!(pick(&mut s, json!({"artboards": [0], "relation": "inside"})), vec![instance]); + assert_eq!(pick(&mut s, json!({"artboards": [0], "relation": "outside"})), vec![]); +} diff --git a/crates/engine/src/cmd/select.rs b/crates/engine/src/cmd/select.rs index 25e0123ca..0f16246df 100644 --- a/crates/engine/src/cmd/select.rs +++ b/crates/engine/src/cmd/select.rs @@ -321,21 +321,19 @@ fn all_on_artboard(s: &mut Session, p: &Value) -> Result { let st = s.doc()?; let i = p.get("artboard").and_then(Value::as_u64).map_or(0, index); let r = st.doc.artboards.get(i).map(|a| a.rect).ok_or_else(|| bad("select.allOnArtboard", "no such artboard"))?; - // Symbol instances are measured by their art. let symbols = st.doc.symbol_boxes(); - let ids: Vec = selectable(&st.doc, st.isolation) - .into_iter() - .filter(|id| { - st.doc - .node(*id) - .and_then(|n| n.geometric_bounds_with(&symbols)) - .is_some_and(|b| b.intersect(r).area() > 0.0 || vectorcraft_doc::artboard_holds(r, b.center())) - }) - .collect(); + let ids: Vec = + selectable(&st.doc, st.isolation).into_iter().filter(|id| st.doc.node(*id).is_some_and(|n| on_artboard(n, &symbols, r))).collect(); s.select(|_, sel| sel.set(ids.iter().copied()))?; ok() } +/// Is `n` on the artboard at `r`, as All on Active Artboard takes it: its geometric bounds (a +/// symbol instance's measured by its art) overlap the artboard, or its center is on it. +pub(crate) fn on_artboard(n: &Node, symbols: &vectorcraft_doc::SymbolBoxes, r: vectorcraft_geom::Rect) -> bool { + n.geometric_bounds_with(symbols).is_some_and(|b| b.intersect(r).area() > 0.0 || vectorcraft_doc::artboard_holds(r, b.center())) +} + fn none(s: &mut Session, _: &Value) -> Result { s.select(|_, sel| sel.clear())?; ok() diff --git a/crates/engine/tests/command_sweep.rs b/crates/engine/tests/command_sweep.rs index 45f6b4768..3010901ca 100644 --- a/crates/engine/tests/command_sweep.rs +++ b/crates/engine/tests/command_sweep.rs @@ -236,6 +236,9 @@ fn structured_junk() { ("select.anchorsMany", json!({"items": [{"id": 2, "anchors": [[0, 0]]}], "mode": "subtract"})), ("select.anchorsMany", json!({"items": [], "mode": 7})), ("select.toggle", json!({"ids": [2, u64::MAX, "x"]})), + ("addon.select.byArtboards", json!({"artboards": [0, u64::MAX, -1, "x", null, 1.5], "relation": "inside"})), + ("addon.select.byArtboards", json!({"artboards": [1e308], "relation": [1]})), + ("addon.select.byArtboards", json!({"artboards": {"a": 1}, "relation": "outside"})), ("object.transform", json!({"matrix": [0, 0, 0, 0, 0, 0]})), ("object.transform", json!({"matrix": [1e308, 1e308, 1e308, 1e308, 1e308, 1e308]})), ("object.transform", json!({"matrix": [1, 2, 3]})), diff --git a/crates/ui-egui/src/addons.rs b/crates/ui-egui/src/addons.rs index 92895fea0..d9c86240a 100644 --- a/crates/ui-egui/src/addons.rs +++ b/crates/ui-egui/src/addons.rs @@ -7,6 +7,7 @@ pub mod document; pub mod enclosing_lasso; pub mod pathtools; pub mod pen; +pub mod select; pub mod typetools; use crate::menus::Item; @@ -20,6 +21,7 @@ pub fn plugin_menu() -> Vec { Item::Sub("Document Tools", document::menu()), Item::Sub("Enclosing Lasso", enclosing_lasso::menu()), Item::Sub("Path Precision Tools", pathtools::menu()), + Item::Sub("Selection Tools", select::menu()), Item::Sub("Smart Pen", pen::menu()), Item::Sub("Type Tools", typetools::menu()), Item::Sep, diff --git a/crates/ui-egui/src/addons/select.rs b/crates/ui-egui/src/addons/select.rs new file mode 100644 index 000000000..3429fd49a --- /dev/null +++ b/crates/ui-egui/src/addons/select.rs @@ -0,0 +1,196 @@ +//! Selection Tools in the interface: its submenu (Object › Plug-ins › Selection Tools) and the +//! Select by Artboards dialog (#5548), which lists the artboards with their number, name and size, +//! filters them by name, picks several, fits one in the window on double-click (#5549), and on OK +//! runs `addon.select.byArtboards` with the relation chosen. + +use egui::{Sense, Ui, vec2}; +use serde_json::{Value, json}; + +use crate::VectorcraftApp; +use crate::dialogs::{DialogSpec, form}; +use crate::menus::Item; +use crate::state::Dialog; +use crate::theme::Tokens; +use crate::widgets; + +/// Select by Artboards, and its dialog's kind. +pub const COMMAND: &str = "addon.select.byArtboards"; +pub const KIND: &str = "selectByArtboards"; +/// The relations the dialog offers: (value, label). +const RELATIONS: [(&str, &str); 3] = [("overlap", "Overlapping"), ("inside", "Fully Inside"), ("outside", "Fully Outside")]; +const LABEL_W: f32 = 70.0; +const ROW_H: f32 = 22.0; +const NUMBER_W: f32 = 28.0; +const LIST_H: f32 = 220.0; + +/// Object › Plug-ins › Selection Tools. +pub fn menu() -> Vec { + vec![Item::Cmd("Select by Artboards…", COMMAND, Value::Null)] +} + +/// The dialog's fields to start with: the active artboard chosen, Overlapping. +pub(crate) fn fields(app: &VectorcraftApp) -> Value { + json!({ "artboards": [app.view().map_or(0, |v| v.artboard)], "relation": "overlap" }) +} + +pub(crate) const SPEC: DialogSpec = DialogSpec { + heading: |_| tl!("Select by Artboards").to_string(), + body, + confirm, + min_width: 360.0, + ok_enabled: Some(|_, d| !chosen(d).is_empty()), + ..DialogSpec::FORM +}; + +/// The artboards chosen in dialog `d`, in order. +fn chosen(d: &Dialog) -> Vec { + let mut v: Vec = + d.fields.get("artboards").and_then(Value::as_array).into_iter().flatten().filter_map(|i| usize::try_from(i.as_u64()?).ok()).collect(); + v.sort_unstable(); + v.dedup(); + v +} + +/// The indices of `names` whose name holds `query`, ignoring case (all of them for an empty query). +pub(crate) fn shown(names: &[String], query: &str) -> Vec { + let q = query.trim().to_lowercase(); + names.iter().enumerate().filter(|(_, n)| q.is_empty() || n.to_lowercase().contains(&q)).map(|(i, _)| i).collect() +} + +/// A click on row `i` of the rows `shown`: alone it chooses only that artboard, with Cmd/Ctrl it +/// toggles it, with Shift it adds the rows from the last one clicked (`anchor`) to it. Returns the +/// new anchor. +pub(crate) fn click(chosen: &mut Vec, anchor: Option, i: usize, shown: &[usize], shift: bool, command: bool) -> Option { + let at = |j: usize| shown.iter().position(|k| *k == j); + match (shift, anchor.and_then(at), at(i)) { + (true, Some(a), Some(b)) => { + chosen.extend(shown.iter().skip(a.min(b)).take(a.abs_diff(b) + 1)); + chosen.sort_unstable(); + chosen.dedup(); + return anchor; + } + _ if command => match chosen.iter().position(|j| *j == i) { + Some(p) => { + chosen.remove(p); + } + None => chosen.push(i), + }, + _ => *chosen = vec![i], + } + chosen.sort_unstable(); + Some(i) +} + +fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool { + let t = Tokens::get(ui.ctx()); + let unit = app.session.general_unit(); + let boards: Vec<(String, f64, f64)> = app + .session + .active() + .map(|st| st.doc.artboards.iter().map(|a| (a.name.clone(), a.rect.width(), a.rect.height())).collect()) + .unwrap_or_default(); + let names: Vec = boards.iter().map(|b| b.0.clone()).collect(); + let query = widgets::search_field(ui, ui.scope_id().with("select-by-artboards-search"), "Search artboards"); + d.fields.insert("__search".into(), json!(query)); + let rows = shown(&names, &query); + let picked = chosen(d); + let (mut clicked, mut double) = (None, None); + ui.add_space(4.0); + egui::ScrollArea::vertical().max_height(LIST_H).auto_shrink([false, true]).show(ui, |ui| { + if rows.is_empty() { + ui.label(egui::RichText::new(tl!("No artboards match.")).color(t.text_dim)); + } + for &i in &rows { + let Some((name, w, h)) = boards.get(i) else { continue }; + let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), ROW_H), Sense::click()); + let on = picked.contains(&i); + if on { + ui.painter().rect_filled(r, 0.0, t.row_selected); + } else if resp.hovered() { + ui.painter().rect_filled(r, 0.0, t.hover); + } + let font = egui::FontId::proportional(12.0); + let mid = r.left_center(); + let p = ui.painter(); + p.text(mid + vec2(6.0, 0.0), egui::Align2::LEFT_CENTER, (i + 1).to_string(), font.clone(), t.text_dim); + let size = format!("{} × {}", unit.number(*w), unit.format(*h)); + let size_at = r.right_center() - vec2(6.0, 0.0); + p.text(size_at, egui::Align2::RIGHT_CENTER, &size, font.clone(), t.text_dim); + p.with_clip_rect(r.with_max_x(size_at.x - 80.0)).text(mid + vec2(NUMBER_W, 0.0), egui::Align2::LEFT_CENTER, name, font, t.text); + resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::ListItem, true, on, format!("{} {name} {size}", i + 1))); + if resp.double_clicked() { + double = Some(i); + } else if resp.clicked() { + clicked = Some((i, ui.input(|inp| inp.modifiers))); + } + } + }); + if let Some((i, m)) = clicked { + row_clicked(d, i, &rows, m); + } + if let Some(i) = double { + row_double_clicked(app, d, i); + } + let mut picked = chosen(d); + ui.add_space(4.0); + ui.horizontal(|ui| { + if widgets::flat_button_fit(ui, "Select All", 70.0).clicked() { + choose_shown(&mut picked, &rows, true); + } + if widgets::flat_button_fit(ui, "None", 50.0).clicked() { + choose_shown(&mut picked, &rows, false); + } + // Rows hidden by the search stay chosen: the count says how many are. + let count = crate::i18n::fmt(tl!("Chosen: {n} of {total}"), &[("n", &picked.len().to_string()), ("total", &boards.len().to_string())]); + ui.label(egui::RichText::new(count).color(t.text_dim)); + }); + d.fields.insert("artboards".into(), json!(picked)); + ui.add_space(6.0); + form::choice(ui, d, "relation", tl!("Select:"), (LABEL_W, 160.0), &RELATIONS); + false +} + +/// Select All (`on`) or None: choose every row `shown`, or none of them. Rows the search hides +/// keep their state. +pub(crate) fn choose_shown(chosen: &mut Vec, shown: &[usize], on: bool) { + if on { + chosen.extend(shown.iter().copied()); + chosen.sort_unstable(); + chosen.dedup(); + } else { + chosen.retain(|i| !shown.contains(i)); + } +} + +/// A click on row `i` of the rows `shown`, with modifier keys `m`: [`click`] on dialog `d`'s +/// chosen artboards. +pub(crate) fn row_clicked(d: &mut Dialog, i: usize, shown: &[usize], m: egui::Modifiers) { + let mut picked = chosen(d); + // What a double-click's first click changed, for its second to put back. + d.fields.insert("__before".into(), json!(picked)); + let anchor = d.fields.get("__anchor").and_then(Value::as_u64).and_then(|a| usize::try_from(a).ok()); + if let Some(a) = click(&mut picked, anchor, i, shown, m.shift, m.command) { + d.fields.insert("__anchor".into(), json!(a)); + } + d.fields.insert("artboards".into(), json!(picked)); +} + +/// A double-click on row `i`: artboard `i` becomes active and fits in the window, as a +/// double-click on its number in the Artboards panel does (`view.goToArtboard`). The dialog stays +/// open, and the artboards chosen are those chosen before the double-click. +pub(crate) fn row_double_clicked(app: &mut VectorcraftApp, d: &mut Dialog, i: usize) { + if let Some(before) = d.fields.remove("__before") { + d.fields.insert("artboards".into(), before); + } + // An artboard the list showed a frame ago can't be missing, and a failure only leaves the view. + app.run("view.goToArtboard", json!({ "index": i })).ok(); +} + +/// OK: select the objects and close. +fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { + app.ui.dialog = None; + app.run(COMMAND, form::params(d)) +} + +#[cfg(test)] +mod tests; diff --git a/crates/ui-egui/src/addons/select/tests.rs b/crates/ui-egui/src/addons/select/tests.rs new file mode 100644 index 000000000..f28dedca5 --- /dev/null +++ b/crates/ui-egui/src/addons/select/tests.rs @@ -0,0 +1,102 @@ +use serde_json::json; +use vectorcraft_engine::Session; + +use super::{choose_shown, click, row_clicked, row_double_clicked, shown}; +use crate::tests_labels::painted_text; +use crate::{VectorcraftApp, menus}; + +fn names(v: &[&str]) -> Vec { + v.iter().map(|s| s.to_string()).collect() +} + +#[test] +fn the_search_filters_artboards_by_name_ignoring_case() { + let n = names(&["Cover", "Page 2", "Back cover", "Inside"]); + assert_eq!(shown(&n, ""), vec![0, 1, 2, 3]); + assert_eq!(shown(&n, " COVER "), vec![0, 2]); + assert_eq!(shown(&n, "zzz"), Vec::::new()); +} + +#[test] +fn select_all_and_none_act_on_the_rows_shown() { + // Board 4 is chosen but hidden by the search: neither button touches it. + let mut c = vec![1, 4]; + choose_shown(&mut c, &[0, 1, 2], true); + assert_eq!(c, vec![0, 1, 2, 4]); + choose_shown(&mut c, &[0, 1, 2], false); + assert_eq!(c, vec![4]); +} + +#[test] +fn clicks_choose_toggle_and_extend_rows() { + let rows = [0, 2, 3, 5]; + let mut c = vec![]; + let a = click(&mut c, None, 2, &rows, false, false); + assert_eq!((c.clone(), a), (vec![2], Some(2))); + // Shift adds the rows shown between the last one clicked and this one. + let a = click(&mut c, a, 5, &rows, true, false); + assert_eq!((c.clone(), a), (vec![2, 3, 5], Some(2))); + // Cmd/Ctrl toggles a row. + let a = click(&mut c, a, 3, &rows, false, true); + assert_eq!((c.clone(), a), (vec![2, 5], Some(3))); + let a = click(&mut c, a, 0, &rows, false, true); + assert_eq!((c.clone(), a), (vec![0, 2, 5], Some(0))); + // A plain click chooses that row alone; Shift without an anchor does too. + let a = click(&mut c, a, 3, &rows, false, false); + assert_eq!((c.clone(), a), (vec![3], Some(3))); + let a = click(&mut c, None, 5, &rows, true, false); + assert_eq!((c, a), (vec![5], Some(5))); +} + +#[test] +fn select_by_artboards_lists_the_artboards_and_selects_on_ok() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 100, "height": 100})).unwrap(); + app.run("artboard.new", json!({"x": 150, "y": 0, "width": 200, "height": 50, "name": "Banner"})).unwrap(); + let inside = app.run("shape.rectangle", json!({"x": 160, "y": 10, "width": 20, "height": 20})).unwrap()["id"].as_u64().unwrap(); + app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap(); + app.run("select.none", json!({})).unwrap(); + let entries = menus::menu_entries(&app); + let item = entries.iter().find(|e| e.path == ["Object", "Plug-ins", "Selection Tools"] && e.label == "Select by Artboards…").unwrap(); + let (cmd, p) = menus::click_target(&item.label, item.command.as_deref().unwrap(), &item.params); + menus::invoke(&mut app, &cmd, p); + let d = app.ui.dialog.as_ref().unwrap(); + assert_eq!(d.kind, super::KIND); + assert_eq!(d.fields["artboards"], json!([0]), "the active artboard is chosen"); + let shown = painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx())); + for s in ["Select by Artboards", "Artboard 1", "Banner", "100 × 100 pt", "200 × 50 pt", "Chosen: 1 of 2", "Overlapping", "Select All", "None"] { + assert!(shown.contains(s), "{s} in {shown}"); + } + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert("artboards".into(), json!([])); + assert!(!(super::SPEC.ok_enabled.unwrap())(&app, app.ui.dialog.as_ref().unwrap()), "OK is off with nothing chosen"); + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert("artboards".into(), json!([1])); + d.fields.insert("relation".into(), json!("inside")); + crate::dialogs::confirm(&mut app).unwrap(); + assert!(app.ui.dialog.is_none()); + assert_eq!(app.session.active().unwrap().selection.objects, vec![vectorcraft_doc::NodeId(inside)]); +} + +#[test] +fn double_clicking_a_row_fits_its_artboard_and_keeps_the_choice() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 100, "height": 100})).unwrap(); + for x in [150, 300] { + app.run("artboard.new", json!({"x": x, "y": 0, "width": 100, "height": 100})).unwrap(); + } + app.run("view.goToArtboard", json!({"index": 0})).unwrap(); + menus::invoke(&mut app, super::COMMAND, serde_json::Value::Null); + let mut d = app.ui.dialog.take().unwrap(); + d.fields.insert("artboards".into(), json!([0, 1])); + // The search shows boards 0 and 2: row 2 is the second row, but board 2 is the one fitted. + let rows = [0, 2]; + row_clicked(&mut d, 2, &rows, egui::Modifiers::NONE); + assert_eq!(d.fields["artboards"], json!([2]), "the first click chooses the row"); + // The dialog frame lends the dialog to its body while the app still shows it. + app.ui.dialog = Some(d.clone()); + row_double_clicked(&mut app, &mut d, 2); + assert_eq!(d.fields["artboards"], json!([0, 1]), "the double-click puts the choice back"); + assert_eq!(app.view().unwrap().artboard, 2, "the artboard is active"); + assert!(app.ui.dialog.is_some(), "the dialog stays open"); +} diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 3e029b247..099d6db58 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -416,6 +416,7 @@ registry! { NearestSwatch: [crate::addons::color::nearest::KIND] => crate::addons::color::nearest::SPEC, SwatchLegend: [crate::addons::color::legend::KIND] => crate::addons::color::legend::SPEC, TextTweaker: [crate::addons::typetools::KIND] => crate::addons::typetools::SPEC, + SelectByArtboards: [crate::addons::select::KIND] => crate::addons::select::SPEC, ArrangeTools: [crate::addons::arrange::GRID, crate::addons::arrange::REPLACE, crate::addons::arrange::SIZE, crate::addons::arrange::OPACITY, crate::addons::arrange::SORT] => crate::addons::arrange::SPEC, ShortcutImport: [shortcut_import::KIND] => DialogSpec::window(shortcut_import::show, shortcut_import::confirm), } diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 7a7a8a348..284e70e09 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -6187,3 +6187,13 @@ breakText Glyphs Глифи Spike Връх Skip Spaces Пропускане на интервалите @msg The art is too large to copy as a picture: other apps get no PNG of it Графиката е твърде голяма, за да се копира като изображение: другите приложения не получават PNG от нея + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards Избиране по работни области + Select by Artboards… Избиране по работни области… + Selection Tools Инструменти за избиране + Search artboards Търсене в работните области + Fully Inside Изцяло вътре + Fully Outside Изцяло извън + No artboards match. Няма съвпадащи работни области. + Chosen: {n} of {total} Избрани: {n} от {total} diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 5ccea4d95..d6e1bdb8e 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -6221,3 +6221,13 @@ shape Open Otvoren Spike Šiljak Skip Spaces Preskoči razmake @msg The art is too large to copy as a picture: other apps get no PNG of it Grafika je prevelika da bi se kopirala kao slika: druge aplikacije ne dobijaju njen PNG + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards Odabir po radnim pločama + Select by Artboards… Odabir po radnim pločama… + Selection Tools Alati za odabir + Search artboards Pretraži radne ploče + Fully Inside Potpuno unutra + Fully Outside Potpuno vani + No artboards match. Nijedna radna ploča ne odgovara. + Chosen: {n} of {total} Odabrano: {n} od {total} diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index d95f73b70..60c149326 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -6321,3 +6321,13 @@ breakText Glyphs Glifs Spike Pic Skip Spaces Omet els espais @msg The art is too large to copy as a picture: other apps get no PNG of it La il·lustració és massa gran per copiar-la com a imatge: les altres aplicacions no en reben cap PNG + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards Selecciona per taulers + Select by Artboards… Selecciona per taulers… + Selection Tools Eines de selecció + Search artboards Cerca taulers + Fully Inside Totalment a dins + Fully Outside Totalment fora + No artboards match. Cap tauler no coincideix. + Chosen: {n} of {total} Triats: {n} de {total} diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 152b95424..4065ce864 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6335,3 +6335,13 @@ breakText Glyphs Glyfy Spike Špička Skip Spaces Přeskočit mezery @msg The art is too large to copy as a picture: other apps get no PNG of it Kresba je příliš velká na zkopírování jako obrázek: ostatní aplikace z ní nedostanou PNG + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards Vybrat podle pracovních ploch + Select by Artboards… Vybrat podle pracovních ploch… + Selection Tools Nástroje výběru + Search artboards Hledat pracovní plochy + Fully Inside Zcela uvnitř + Fully Outside Zcela vně + No artboards match. Žádná pracovní plocha neodpovídá. + Chosen: {n} of {total} Zvoleno: {n} z {total} diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index b8e2a8b50..3388daba9 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -6203,3 +6203,13 @@ breakText Glyphs Glyffer Spike Spids Skip Spaces Spring mellemrum over @msg The art is too large to copy as a picture: other apps get no PNG of it Grafikken er for stor til at blive kopieret som et billede: andre programmer får ingen PNG af den + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards Markér efter tegneflader + Select by Artboards… Markér efter tegneflader… + Selection Tools Markeringsværktøjer + Search artboards Søg i tegneflader + Fully Inside Helt indenfor + Fully Outside Helt udenfor + No artboards match. Ingen tegneflader passer. + Chosen: {n} of {total} Valgt: {n} af {total} diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 6cc71b5a2..49a151bd2 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5921,3 +5921,13 @@ breakText Glyphs Glyphen Spike Spitze Skip Spaces Leerzeichen überspringen @msg The art is too large to copy as a picture: other apps get no PNG of it Das Bildmaterial ist zu groß, um es als Bild zu kopieren: Andere Apps erhalten kein PNG davon + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards Nach Zeichenflächen auswählen + Select by Artboards… Nach Zeichenflächen auswählen… + Selection Tools Auswahlwerkzeuge + Search artboards Zeichenflächen suchen + Fully Inside Vollständig innen + Fully Outside Vollständig außen + No artboards match. Keine Zeichenfläche passt. + Chosen: {n} of {total} Gewählt: {n} von {total} diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 579ee3062..048282060 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -6287,3 +6287,13 @@ breakText Glyphs Γλύφοι Spike Αιχμή Skip Spaces Παράλειψη κενών @msg The art is too large to copy as a picture: other apps get no PNG of it Το γραφικό είναι πολύ μεγάλο για αντιγραφή ως εικόνα: οι άλλες εφαρμογές δεν λαμβάνουν PNG του + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards Επιλογή κατά πίνακες σχεδίασης + Select by Artboards… Επιλογή κατά πίνακες σχεδίασης… + Selection Tools Εργαλεία επιλογής + Search artboards Αναζήτηση πινάκων σχεδίασης + Fully Inside Πλήρως εντός + Fully Outside Πλήρως εκτός + No artboards match. Κανένας πίνακας σχεδίασης δεν ταιριάζει. + Chosen: {n} of {total} Επιλεγμένοι: {n} από {total} diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index c45f7e58f..505635585 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -6277,3 +6277,13 @@ breakText Glyphs Glifos Spike Pico Skip Spaces Omitir espacios @msg The art is too large to copy as a picture: other apps get no PNG of it La ilustración es demasiado grande para copiarla como imagen: las demás aplicaciones no reciben un PNG de ella + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards Seleccionar por mesas de trabajo + Select by Artboards… Seleccionar por mesas de trabajo… + Selection Tools Herramientas de selección + Search artboards Buscar mesas de trabajo + Fully Inside Totalmente dentro + Fully Outside Totalmente fuera + No artboards match. Ninguna mesa de trabajo coincide. + Chosen: {n} of {total} Elegidas: {n} de {total} diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 59a76fc83..006b82b8c 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6365,3 +6365,13 @@ breakText Glyphs Merkit Spike Piikki Skip Spaces Ohita välilyönnit @msg The art is too large to copy as a picture: other apps get no PNG of it Grafiikka on liian suuri kopioitavaksi kuvana: muut sovellukset eivät saa siitä PNG-kuvaa + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards Valitse piirtoalustojen mukaan + Select by Artboards… Valitse piirtoalustojen mukaan… + Selection Tools Valintatyökalut + Search artboards Hae piirtoalustoja + Fully Inside Kokonaan sisällä + Fully Outside Kokonaan ulkona + No artboards match. Mikään piirtoalusta ei vastaa hakua. + Chosen: {n} of {total} Valittu: {n}/{total} diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index f792a533f..2b6e6d931 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6330,3 +6330,13 @@ breakText Glyphs Glyphes Spike Pointe Skip Spaces Ignorer les espaces @msg The art is too large to copy as a picture: other apps get no PNG of it L’illustration est trop grande pour être copiée en tant qu’image : les autres applications n’en reçoivent pas de PNG + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards Sélectionner par plans de travail + Select by Artboards… Sélectionner par plans de travail… + Selection Tools Outils de sélection + Search artboards Rechercher des plans de travail + Fully Inside Entièrement à l’intérieur + Fully Outside Entièrement à l’extérieur + No artboards match. Aucun plan de travail ne correspond. + Chosen: {n} of {total} Choisis : {n} sur {total} diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 35f360496..ea0e4a48e 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6319,3 +6319,13 @@ breakText Glyphs Glifák Spike Csúcs Skip Spaces Szóközök kihagyása @msg The art is too large to copy as a picture: other apps get no PNG of it A grafika túl nagy ahhoz, hogy képként másolható legyen: más alkalmazások nem kapnak róla PNG-t + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards Kijelölés rajztáblák szerint + Select by Artboards… Kijelölés rajztáblák szerint… + Selection Tools Kijelölőeszközök + Search artboards Rajztáblák keresése + Fully Inside Teljesen belül + Fully Outside Teljesen kívül + No artboards match. Nincs egyező rajztábla. + Chosen: {n} of {total} Kiválasztva: {n} / {total} diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index bbd1cd732..8fc99dbdf 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -6203,3 +6203,13 @@ breakText Glyphs Glif Spike Puncak Skip Spaces Lewati Spasi @msg The art is too large to copy as a picture: other apps get no PNG of it Gambar terlalu besar untuk disalin sebagai gambar: aplikasi lain tidak mendapatkan PNG darinya + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards Pilih menurut Artboard + Select by Artboards… Pilih menurut Artboard… + Selection Tools Alat pemilihan + Search artboards Cari artboard + Fully Inside Sepenuhnya di dalam + Fully Outside Sepenuhnya di luar + No artboards match. Tidak ada artboard yang cocok. + Chosen: {n} of {total} Dipilih: {n} dari {total} diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 8a23b7d8e..87ead8045 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5955,3 +5955,13 @@ breakText Glyphs Glifi Spike Picco Skip Spaces Salta gli spazi @msg The art is too large to copy as a picture: other apps get no PNG of it La grafica è troppo grande per essere copiata come immagine: le altre app non ne ricevono un PNG + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards Seleziona per tavole da disegno + Select by Artboards… Seleziona per tavole da disegno… + Selection Tools Strumenti di selezione + Search artboards Cerca tavole da disegno + Fully Inside Completamente interno + Fully Outside Completamente esterno + No artboards match. Nessuna tavola da disegno corrisponde. + Chosen: {n} of {total} Scelte: {n} di {total} diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 3af0bdb02..4b3afc203 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -6020,3 +6020,13 @@ breakText Glyphs 字形 Spike 山形 Skip Spaces スペースをスキップ @msg The art is too large to copy as a picture: other apps get no PNG of it アートワークが大きすぎるため画像としてコピーできません:他のアプリには PNG が渡されません + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards アートボードで選択 + Select by Artboards… アートボードで選択… + Selection Tools 選択ツール + Search artboards アートボードを検索 + Fully Inside 完全に内側 + Fully Outside 完全に外側 + No artboards match. 一致するアートボードはありません。 + Chosen: {n} of {total} 選択中: {n} / {total} diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index bf2684eb9..7093ad663 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -6346,3 +6346,13 @@ font-script Japanese იაპონური Spike პიკი Skip Spaces ჰარების გამოტოვება @msg The art is too large to copy as a picture: other apps get no PNG of it ნახატი ზედმეტად დიდია სურათად დასაკოპირებლად: სხვა აპები მის PNG-ს ვერ მიიღებენ + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards სამუშაო დაფებით არჩევა + Select by Artboards… სამუშაო დაფებით არჩევა… + Selection Tools მონიშვნის ხელსაწყოები + Search artboards სამუშაო დაფების ძებნა + Fully Inside მთლიანად შიგნით + Fully Outside მთლიანად გარეთ + No artboards match. შესაბამისი სამუშაო დაფა არ არის. + Chosen: {n} of {total} არჩეულია: {n} / {total} diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 4d86c3321..a41ea3cf8 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5916,3 +5916,13 @@ breakText Glyphs 글리프 Spike 뾰족한 모양 Skip Spaces 공백 건너뛰기 @msg The art is too large to copy as a picture: other apps get no PNG of it 아트워크가 너무 커서 이미지로 복사할 수 없습니다: 다른 앱에는 PNG가 전달되지 않습니다 + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards 아트보드로 선택 + Select by Artboards… 아트보드로 선택… + Selection Tools 선택 도구 + Search artboards 아트보드 검색 + Fully Inside 완전히 안쪽 + Fully Outside 완전히 바깥쪽 + No artboards match. 일치하는 아트보드가 없습니다. + Chosen: {n} of {total} 선택됨: {n} / {total} diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index d79824a47..02004bb0c 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -6247,3 +6247,13 @@ breakText Glyphs Glyphs Spike Piek Skip Spaces Spaties overslaan @msg The art is too large to copy as a picture: other apps get no PNG of it Het illustratiewerk is te groot om als afbeelding te kopiëren: andere apps krijgen er geen PNG van + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards Selecteren op tekengebieden + Select by Artboards… Selecteren op tekengebieden… + Selection Tools Selectiegereedschappen + Search artboards Tekengebieden zoeken + Fully Inside Volledig binnen + Fully Outside Volledig buiten + No artboards match. Geen tekengebied komt overeen. + Chosen: {n} of {total} Gekozen: {n} van {total} diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 83046c58a..0052a5661 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -6190,3 +6190,13 @@ breakText Glyphs Glify Spike Szpic Skip Spaces Pomiń spacje @msg The art is too large to copy as a picture: other apps get no PNG of it Grafika jest zbyt duża, aby skopiować ją jako obraz: inne aplikacje nie otrzymają jej w formacie PNG + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards Zaznacz według obszarów kompozycji + Select by Artboards… Zaznacz według obszarów kompozycji… + Selection Tools Narzędzia zaznaczania + Search artboards Szukaj obszarów kompozycji + Fully Inside Całkowicie wewnątrz + Fully Outside Całkowicie na zewnątrz + No artboards match. Żaden obszar kompozycji nie pasuje. + Chosen: {n} of {total} Wybrano: {n} z {total} diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 4199f00a6..b52c17817 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -6408,3 +6408,13 @@ swatch Yellow Green Verde-amarelado Spike Pico Skip Spaces Ignorar espaços @msg The art is too large to copy as a picture: other apps get no PNG of it A arte é grande demais para ser copiada como imagem: outros aplicativos não recebem um PNG dela + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards Selecionar por mesas de trabalho + Select by Artboards… Selecionar por mesas de trabalho… + Selection Tools Ferramentas de seleção + Search artboards Pesquisar mesas de trabalho + Fully Inside Totalmente dentro + Fully Outside Totalmente fora + No artboards match. Nenhuma mesa de trabalho corresponde. + Chosen: {n} of {total} Escolhidas: {n} de {total} diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 004d92746..e05a5173c 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -6424,3 +6424,13 @@ shape Open Открытый Spike Пик Skip Spaces Пропускать пробелы @msg The art is too large to copy as a picture: other apps get no PNG of it Графика слишком велика для копирования в виде изображения: другие приложения не получат её PNG + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards Выделить по монтажным областям + Select by Artboards… Выделить по монтажным областям… + Selection Tools Инструменты выделения + Search artboards Поиск монтажных областей + Fully Inside Полностью внутри + Fully Outside Полностью снаружи + No artboards match. Нет подходящих монтажных областей. + Chosen: {n} of {total} Выбрано: {n} из {total} diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 81e308099..20d8d6e1d 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -6391,3 +6391,13 @@ digits Hindi Hindi Spike Spets Skip Spaces Hoppa över mellanslag @msg The art is too large to copy as a picture: other apps get no PNG of it Teckningen är för stor för att kopieras som en bild: andra appar får ingen PNG av den + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards Markera efter ritytor + Select by Artboards… Markera efter ritytor… + Selection Tools Markeringsverktyg + Search artboards Sök ritytor + Fully Inside Helt inuti + Fully Outside Helt utanför + No artboards match. Ingen rityta matchar. + Chosen: {n} of {total} Valda: {n} av {total} diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 3ca7e892d..5f34e576c 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -6292,3 +6292,13 @@ breakText Glyphs Glifler Spike Sivri Uç Skip Spaces Boşlukları Atla @msg The art is too large to copy as a picture: other apps get no PNG of it Çizim, resim olarak kopyalanamayacak kadar büyük: diğer uygulamalar bunun PNG'sini almaz + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards Çalışma Yüzeylerine Göre Seç + Select by Artboards… Çalışma Yüzeylerine Göre Seç… + Selection Tools Seçim Araçları + Search artboards Çalışma yüzeylerinde ara + Fully Inside Tamamen içeride + Fully Outside Tamamen dışarıda + No artboards match. Eşleşen çalışma yüzeyi yok. + Chosen: {n} of {total} Seçilen: {n}/{total} diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 74e866c72..316aa0af3 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5935,3 +5935,13 @@ breakText Glyphs Гліфи Spike Пік Skip Spaces Пропускати пробіли @msg The art is too large to copy as a picture: other apps get no PNG of it Графіка завелика, щоб скопіювати її як зображення: інші програми не отримають її PNG + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards Виділити за монтажними областями + Select by Artboards… Виділити за монтажними областями… + Selection Tools Інструменти виділення + Search artboards Пошук монтажних областей + Fully Inside Повністю всередині + Fully Outside Повністю зовні + No artboards match. Немає відповідних монтажних областей. + Chosen: {n} of {total} Вибрано: {n} з {total} diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 8742397b4..c17b073d5 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5936,3 +5936,13 @@ breakText Glyphs Glyph Spike Đỉnh nhọn Skip Spaces Bỏ qua khoảng trắng @msg The art is too large to copy as a picture: other apps get no PNG of it Hình vẽ quá lớn để sao chép dưới dạng ảnh: các ứng dụng khác không nhận được PNG của nó + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards Chọn theo bảng vẽ + Select by Artboards… Chọn theo bảng vẽ… + Selection Tools Công cụ chọn + Search artboards Tìm bảng vẽ + Fully Inside Hoàn toàn bên trong + Fully Outside Hoàn toàn bên ngoài + No artboards match. Không có bảng vẽ nào khớp. + Chosen: {n} of {total} Đã chọn: {n} trên {total} diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index a69069d5f..dd0eb1e15 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -6176,3 +6176,13 @@ breakText Glyphs 字形 Spike 尖峰 Skip Spaces 跳过空格 @msg The art is too large to copy as a picture: other apps get no PNG of it 图稿太大,无法作为图片复制:其他应用不会获得它的 PNG + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards 按画板选择 + Select by Artboards… 按画板选择… + Selection Tools 选择工具 + Search artboards 搜索画板 + Fully Inside 完全在内 + Fully Outside 完全在外 + No artboards match. 没有匹配的画板。 + Chosen: {n} of {total} 已选:{n} / {total} diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index a4f887993..0d6b7c407 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5973,3 +5973,13 @@ breakText Glyphs 字符 Spike 尖峰 Skip Spaces 略過空格 @msg The art is too large to copy as a picture: other apps get no PNG of it 圖稿太大,無法作為圖片複製:其他應用程式不會取得它的 PNG + +# Selection Tools add-on: Select by Artboards (Object › Plug-ins › Selection Tools, #5548). + Select by Artboards 依工作區域選取 + Select by Artboards… 依工作區域選取… + Selection Tools 選取工具 + Search artboards 搜尋工作區域 + Fully Inside 完全在內 + Fully Outside 完全在外 + No artboards match. 沒有符合的工作區域。 + Chosen: {n} of {total} 已選:{n} / {total} diff --git a/crates/ui-egui/src/menus/invoke.rs b/crates/ui-egui/src/menus/invoke.rs index 0b30e0dc6..18e77b792 100644 --- a/crates/ui-egui/src/menus/invoke.rs +++ b/crates/ui-egui/src/menus/invoke.rs @@ -72,6 +72,7 @@ pub(super) fn menu_dialog(app: &VectorcraftApp, id: &str) -> Option<(&'static st crate::addons::color::nearest::COMMAND => (crate::addons::color::nearest::KIND, crate::addons::color::nearest::fields()), crate::addons::color::legend::COMMAND => (crate::addons::color::legend::KIND, crate::addons::color::legend::fields()), crate::addons::typetools::COMMAND => (crate::addons::typetools::KIND, crate::addons::typetools::fields(app)), + crate::addons::select::COMMAND => (crate::addons::select::KIND, crate::addons::select::fields(app)), "artboard.rearrange" => (crate::dialogs::rearrange_artboards::KIND, crate::dialogs::rearrange_artboards::open_fields(app)), _ => return None, }) diff --git a/docs/mcp.md b/docs/mcp.md index 6b6b1f465..08464ef6d 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -6794,6 +6794,29 @@ on the object or on one of its fills or strokes; a group carrying one is selecte `grayImages` (images whose pixels are RGB, CMYK, or grayscale or 1-bit, read from the image's header). It returns the `count` selected. +### Selection Tools + +Selection Tools is a built-in add-on: its commands are `addon.select.*`, and in the desktop app it lives in Object › +Plug-ins › Selection Tools. + +`addon.select.byArtboards {artboards, relation?}` (Select by Artboards…, #5525) selects objects by where they sit +relative to one or more artboards. `artboards` lists 0-based artboard indices (at least one; one listed twice counts +once). `relation` is `overlap` (the default: on at least one of them, as `select.allOnArtboard` takes it, the bounds +overlapping the artboard or the centre on it), `inside` (the bounds wholly within one of them, edges included; an object +straddling two touching artboards is inside neither) or `outside` (on none of them). It looks at the objects Select All +takes, or the isolated container's in isolation mode, so hidden and locked objects, objects on hidden, locked or template +layers and art held by a locked artboard are left out. Bounds are geometric, a symbol instance's measured by its art. It +returns the `count` selected. An empty list, an index with no artboard, a value that isn't an index, or another +relation is an error, and the selection is left as it was. + +```json +{"name":"run_command","arguments":{"command":"addon.select.byArtboards","params":{"artboards":[2,6,11],"relation":"inside"}}} +``` + +The dialog lists every artboard with its number, name and size in the document's units. Search filters the list by +name; a click chooses one artboard, Shift-click adds a range and Cmd/Ctrl-click toggles one, and Select All / None act +on the rows shown (rows hidden by the search stay chosen). It opens with the active artboard chosen and Overlapping. + ### Vector Halftone (live) `addon.color.halftone` (Effect › Color Effects › Vector Halftone…, also in Object › Plug-ins › Color Effects) draws its diff --git a/docs/roadmap.md b/docs/roadmap.md index cf8cf6a5d..527bb286e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-04 · **Change:** minor (New Document's units belong to the chosen preset; Recent and Saved cards keep their own units; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-04 · **Change:** minor (Select by Artboards add-on selects by overlap, inside or outside several artboards; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -28,6 +28,7 @@ M7 type: Touch Type moves a right-to-left character along its line and up from i | Date | Change | Summary | |---|---|---| +| 2026-11-04 | minor | Selection Tools add-on: Select by Artboards selects the objects overlapping, fully inside or fully outside several chosen artboards, from a dialog that lists, searches and multi-selects the artboards and fits one in the window on double-click (#5525, #5548, #5549, a VectorCraft addition); scores and estimates unchanged. | | 2026-11-04 | minor | New Document: the units chosen in Preset Details belong to the chosen preset; Recent and Saved cards keep the units their document or preset was made in, and the built-in tabs still follow the chosen units (#2078). Scores and estimates unchanged. | | 2026-11-03 | minor | M1: Enclosing Lasso provides whole-object enclosure selection as an add-on with no default shortcut or Basic toolbar slot; estimates unchanged. | | 2026-11-03 | minor | New gradients use CMYK default stops in CMYK documents, including Gradient tool creation. Scores and estimates unchanged. | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index f33830c48..44aede640 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-04 · **Change:** minor (New Document's units belong to the chosen preset; Recent and Saved cards keep their own units; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-04 · **Change:** minor (Select by Artboards add-on selects by overlap, inside or outside several artboards; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) ## Feature submission evidence @@ -23,6 +23,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-04 | minor | Selection Tools add-on: Select by Artboards selects the objects overlapping, fully inside or fully outside several chosen artboards, from a dialog that lists, searches and multi-selects the artboards and fits one in the window on double-click (#5525, #5548, #5549, a VectorCraft addition); scores and estimates unchanged. | | 2026-11-04 | minor | New Document: the units chosen in Preset Details belong to the chosen preset, as the public page lists the pane's options for the selected preset; the chosen card shows them, every other Recent and Saved card keeps the units its document or preset was made in, and the built-in tabs still follow the chosen units (#2078). Scores and estimates unchanged. | | 2026-11-04 | minor | Document Tools add-on (a VectorCraft addition): Batch Trace Folder under Object › Plug-ins (`addon.document.batchTraceFolder`, #4921); evidence added; scores unchanged. | | 2026-11-04 | minor | Object › Expand turns selected symbol instances into their art as Break Link places it, one level deep, with the Expand dialog's Fill and Stroke reaching that art. Scores and estimates unchanged. | @@ -718,6 +719,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur With the Selection or Group Selection tool, pressing E while dragging a marquee makes the marquee select only the objects that lie entirely inside it. Pressing E again switches the marquee back to its default selection. The Lasso picks anchor points only on art a click can pick, and the Add Anchor Point, Delete Anchor Point, Anchor Point and Scissors tools pick anchor points and segments only on the selected paths and on art a click can pick: not on a Shaper Group's original shapes outside Construction mode, and in isolation mode only inside the isolated group or object. Smart Guides and Snap to Point do not snap to a Shaper Group's original shapes, and the reference point of the transform tools snaps to one only when it is selected, or under the pointer in Construction mode. Enclosing Lasso provides whole-object enclosure selection under Object › Plug-ins and its own heading in All Tools. It has no default shortcut or Basic toolbar slot. It follows path curves and keeps partially enclosed objects out, including objects crossing a concave part of the loop. Ordinary group members can be selected individually; compound paths remain whole. This is a VectorCraft addition; scores and estimates are unchanged. + Select by Artboards (Object › Plug-ins › Selection Tools, a VectorCraft addition: #5525, #5548, #5549) selects the objects overlapping, fully inside or fully outside one or more chosen artboards (`addon.select.byArtboards {artboards, relation}`). Overlapping is All on Active Artboard's test, shared as `select::on_artboard`; Fully inside means within one artboard, edges included; Fully outside means on none. It looks at what Select All takes, so hidden, locked and locked-artboard art stays out. Its dialog lists each artboard's number, name and size in the document's units, filters by name, chooses rows with click, Shift-click and Cmd/Ctrl-click, and fits a row's artboard in the window on double-click (`view.goToArtboard`). Tests: `crates/engine/src/cmd/addons/select/tests.rs`, `crates/ui-egui/src/addons/select/tests.rs`, `crates/engine/tests/command_sweep.rs`. A marquee selects a clip group, or an object on a layer with a clipping mask, only when it reaches the area inside the clipping path, and it measures the art only within that path's bounds. Within the bounds of a mask that is not a rectangle, it can still select art that the mask hides. Direct Selection's marquee still selects the hidden anchor points inside it (#2345). A marquee selects a Shaper Group only when it reaches the art the group shows, not the original shapes kept under it, and Direct Selection's clicks and marquee do not select those shapes' anchor points. In Construction mode a marquee selects the original shapes, and Direct Selection selects their anchor points. While an opacity mask or a pattern is edited, a Shaper double-click on a Shaper Group in the mask or pattern art selects the original shape under the pointer without entering Construction mode, the Shaper Group's mode widget does not show, and a Shaper scribble, Release and Expand keep the editing layer isolated. diff --git a/docs/ui-parity.md b/docs/ui-parity.md index bbfcdc069..86abdbafe 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-04 · **Change:** minor (File › Export › Export As PNG exports the active artboard, or each one the Artboard tool selected, not always the first, #5502; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-04 · **Change:** minor (Select by Artboards add-on selects by overlap, inside or outside several artboards; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -30,6 +30,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-04 | minor | Selection Tools add-on: Select by Artboards selects the objects overlapping, fully inside or fully outside several chosen artboards, from a dialog that lists, searches and multi-selects the artboards and fits one in the window on double-click (#5525, #5548, #5549, a VectorCraft addition); scores and estimates unchanged. | | 2026-11-04 | minor | File › Export › Export As PNG… exports the active artboard, or each artboard the Artboard tool has selected (one file each), instead of always the first (#5502); a choice the caller gives is kept; scores and estimates unchanged | | 2026-11-04 | minor | Document Tools add-on (a VectorCraft addition): Object › Plug-ins › Document Tools › Batch Trace Folder asks for a folder of images and a destination folder and traces each image into it (#4921); scores and estimates unchanged | | 2026-11-03 | minor | The Action Options row cites the public Actions panel page and describes its controls. Scores and estimates remain unchanged. | @@ -382,6 +383,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po | Artwork Cleanup add-on text tools (VectorCraft addition): the panel's Text row, which the panel can hide, and Object › Plug-ins › Artwork Cleanup set the alignment of point type keeping its place, break type into lines (Shift-click: paragraphs), words or glyphs, split type at the cursor, reset a text transform (Alt-click: and its horizontal scale), combine point type, remove line or paragraph breaks and replace missing fonts, each one undo step | done | `addon.cleanup.*` text commands, `crates/ui-egui/src/addons/cleanup.rs` and its tests; a VectorCraft addition, so it is not scored | | Color Effects add-on (VectorCraft addition): live Exposure, Desaturate and Invert Colors in Effect › Color Effects and Object › Plug-ins › Color Effects; its panel picks a colour adjustment and its values and adds it as a live effect or applies it as a filter, into the colours, as one undo step; Select by Color Kind selects the objects that use spot colours, carry colour effects, or are RGB, CMYK or grayscale images; Vector Halftone draws an object as halftone dots live (the dots Object › Vector Halftone makes), which Expand Appearance and the exporters write as paths; Duotone prints each colour's tone with one to four inks (a tint of a spot tone's swatch, or the CMYK mix of several), live or as a filter, its dialog a row per tone and the picked tone's curve; Print Ink Fixes (a CMYK document's ink limit, white overprint removed, black overprint set) opens its options from the submenu and the panel's Inks | done | `addon.color.*`, `crates/ui-egui/src/addons/color.rs` and its tests; not a feature of the reference app, so it is not scored | | Color Effects add-on (VectorCraft addition): Object › Plug-ins › Color Effects › Find Nearest Swatch… picks a Library (Document Swatches, a built-in library, or a library or colour book the user saved or loaded) and Fill and Stroke, lists each colour of the selection with its three closest swatches (chip, name, ΔE 2000), and Apply gives each colour its nearest swatch (global and spot swatches linked; a library's swatches added to the document first), one undo step | done | #4923; `addon.color.nearestSwatch`, `addon.color.applyNearestSwatch`, `crates/engine/src/cmd/addons/color/nearest.rs`, `crates/ui-egui/src/addons/color/nearest.rs` and their tests; not a feature of the reference app, so it is not scored | +| Selection Tools add-on (VectorCraft addition): Object › Plug-ins › Selection Tools › Select by Artboards… lists every artboard (number, name, width × height in the document's units), filters them by name, chooses several (click, Shift-click for a range, Cmd/Ctrl-click to toggle; Select All / None act on the rows shown; a count of those chosen), fits a row's artboard in the window on double-click and makes it active, and on OK selects the objects overlapping, fully inside or fully outside the chosen artboards (Overlapping is All on Active Artboard's test) | done | #5525, #5548, #5549; `addon.select.byArtboards`, `crates/engine/src/cmd/addons/select.rs`, `crates/ui-egui/src/addons/select.rs` and their tests; not a feature of the reference app | ## Look and feel