Merge pull request #5633 from ruchirguitar/feat/5525-select-by-artboards

Select by Artboards: select objects overlapping, inside or outside chosen artboards (#5525, #5548, #5549)
This commit is contained in:
BFlat
2026-11-04 23:23:04 +07:00
committed by GitHub
40 changed files with 797 additions and 13 deletions
+2
View File
@@ -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<CommandSpec> {
v.extend(document::specs());
v.extend(pathtools::specs());
v.extend(pen::specs());
v.extend(select::specs());
v.extend(typetools::specs());
v
}
+63
View File
@@ -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<CommandSpec> {
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<Value> {
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::<Option<Vec<_>>>().ok_or_else(|| bad(C, "no such artboard"))?;
let symbols = st.doc.symbol_boxes();
let ids: Vec<NodeId> = 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() }))
}
@@ -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<NodeId> {
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<NodeId>| {
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##"<svg xmlns="http://www.w3.org/2000/svg" width="200" height="120"><defs><symbol id="Far"><rect x="100" y="60" width="40" height="20" fill="#f00"/></symbol></defs><use href="#Far" x="-50" y="-50"/></svg>"##;
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![]);
}
+8 -10
View File
@@ -321,21 +321,19 @@ fn all_on_artboard(s: &mut Session, p: &Value) -> Result<Value> {
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<NodeId> = 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<NodeId> =
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<Value> {
s.select(|_, sel| sel.clear())?;
ok()
+3
View File
@@ -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]})),
+2
View File
@@ -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> {
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,
+196
View File
@@ -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<Item> {
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<usize> {
let mut v: Vec<usize> =
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<usize> {
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<usize>, anchor: Option<usize>, i: usize, shown: &[usize], shift: bool, command: bool) -> Option<usize> {
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<String> = 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<usize>, 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<Value, String> {
app.ui.dialog = None;
app.run(COMMAND, form::params(d))
}
#[cfg(test)]
mod tests;
+102
View File
@@ -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<String> {
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::<usize>::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");
}
+1
View File
@@ -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),
}
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 163 and column 5.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2235 and column 5.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -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}
Can't render this file because it is too large.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 6 and column 47.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 5 and column 25.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 5 and column 25.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 177 and column 5.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 199 and column 5.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -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,
})
+23
View File
@@ -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
+2 -1
View File
@@ -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. |
+3 -1
View File
@@ -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.
+3 -1
View File
@@ -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