mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 13:23:22 +00:00
Select by Artboards: select objects overlapping, inside or outside several artboards (#5525)
addon.select.byArtboards {artboards, relation} in a new Selection Tools
add-on. Overlapping is All on Active Artboard's test, now one shared
helper (select::on_artboard); Fully inside is within one artboard, edges
included; Fully outside is on none. Objects are those Select All takes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
aee5671734
commit
7e016a0bcc
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,109 @@
|
||||
//! 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}");
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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]})),
|
||||
|
||||
+23
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user