Merge pull request #3337 from dotCooCoo/fix/select-bristle-by-kind

Select: Bristle Brush Strokes selects objects by their brush's type
This commit is contained in:
BFlat
2026-10-21 16:15:54 +07:00
committed by GitHub
4 changed files with 36 additions and 7 deletions
+12 -7
View File
@@ -83,13 +83,18 @@ pub fn specs() -> Vec<CommandSpec> {
"Bristle Brush Strokes",
["Select", "Object"],
None,
"{} objects whose brush name contains \"bristle\" → {count}",
"{} objects whose topmost stroke has a bristle brush (type bristle in brush.list), whatever the brush is named → {count}",
has_doc,
|s, _| by_kind(s, |n| n
.appearance
.stroke()
.and_then(|st| st.brush.as_deref())
.is_some_and(|b| b.to_ascii_lowercase().contains("bristle")))
|s, _| {
let brushes = vectorcraft_brush::library(&s.doc()?.doc);
by_kind(s, |n| {
n.appearance
.stroke()
.and_then(|st| st.brush.as_deref())
.and_then(|name| brushes.iter().find(|b| b.name == name))
.is_some_and(|b| matches!(b.kind, vectorcraft_brush::BrushKind::Bristle(_)))
})
}
),
cmd!(
"select.save",
@@ -305,7 +310,7 @@ fn same_text(s: &mut Session, cmd: &str, eq: fn(&CharStyle, &CharStyle) -> bool)
finish_same(s, cmd, ids)
}
fn by_kind(s: &mut Session, f: fn(&Node) -> bool) -> Result<Value> {
fn by_kind(s: &mut Session, f: impl Fn(&Node) -> bool) -> Result<Value> {
let ids = candidates(s, f)?;
let n = ids.len();
s.select(|_, sel| sel.set(ids))?;
+21
View File
@@ -1217,6 +1217,27 @@ fn select_point_and_area_text() {
assert_eq!(s.execute("select.object.brushStrokes", &json!({})).unwrap()["count"], 0);
}
/// Select › Object › Bristle Brush Strokes goes by the brush's type: a bristle brush counts
/// whatever it is named, and a brush of another type with "Bristle" in its name does not.
#[test]
fn bristle_brush_strokes_select_by_brush_type() {
let mut s = session();
let round = rect(&mut s, 0.0, 0.0, 10.0, 10.0);
let soft = rect(&mut s, 20.0, 0.0, 10.0, 10.0);
let nib = rect(&mut s, 40.0, 0.0, 10.0, 10.0);
let _plain = rect(&mut s, 60.0, 0.0, 10.0, 10.0);
s.execute("brush.apply", &json!({"name": "Bristle Round", "ids": [round.0]})).unwrap();
s.execute("brush.apply", &json!({"name": "Bristle Flat Fan", "ids": [soft.0]})).unwrap();
s.execute("brush.options", &json!({"name": "Bristle Flat Fan", "newName": "Soft"})).unwrap();
s.execute("brush.new", &json!({"type": "calligraphic", "name": "Bristle-like"})).unwrap();
s.execute("brush.apply", &json!({"name": "Bristle-like", "ids": [nib.0]})).unwrap();
let brush = |s: &Session, id: NodeId| s.doc().unwrap().doc.node(id).and_then(|n| n.appearance.stroke()).and_then(|st| st.brush.clone());
assert_eq!(brush(&s, soft).as_deref(), Some("Soft"));
assert_eq!(brush(&s, nib).as_deref(), Some("Bristle-like"));
assert_eq!(s.execute("select.object.bristleBrushStrokes", &json!({})).unwrap()["count"], 2);
assert_eq!(selected(&s), vec![round, soft]);
}
/// Select › Object and Select › Same leave out hidden and locked objects, and the objects in hidden
/// or locked groups and sublayers, as Select All does.
#[test]
+2
View File
@@ -1142,6 +1142,8 @@ and `select.recall`. They leave out hidden and locked objects with the objects i
locked or template layers and sublayers and, in isolation mode, everything outside the isolated group or layer. When
the isolated group or layer, or a group or layer it is in, is hidden, locked or a template layer, they also leave out
everything in it, and `select.all`, `select.allOnArtboard` and `select.inverse` select nothing.
`select.object.bristleBrushStrokes` selects the objects whose topmost stroke has a bristle brush (type `bristle` in
`brush.list`), whatever the brush is named; a brush of another type with "Bristle" in its name does not count.
`select.nextAbove` and `select.nextBelow` select the nearest such object above or below the first selected object
among the objects beside it in its group or layer, passing over hidden and locked objects and sublayers. When there
is no such object, the selection stays as it is.
+1
View File
@@ -534,6 +534,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
Agent selection commands validate every object id before changing the selection and report the resulting count and ids (#784).
Agents read the key object: `select.key` returns it, and `document.inspect` and the `ui.pointer` and `pointer_gesture` replies report it as `key`. `select.key` with an id that isn't a selected top-level object returns an error and leaves the key unchanged.
The Select menu's Same commands, its Object commands that select art, Next Object Above and Below, saved selections and the Magic Wand (#2655) select only objects that Select All selects and objects inside them: nothing hidden or locked, nothing in a hidden or locked group, layer or sublayer, nothing on a template layer, and in isolation mode nothing outside the isolated group or layer. In isolation mode Select All, All on Active Artboard and Inverse select nothing while the isolated group or layer, or a group or layer it is in, is hidden, locked or a template layer.
Select ▸ Object ▸ Bristle Brush Strokes selects the objects whose topmost stroke has a bristle brush, whatever the brush is named, and leaves out objects whose brush is of another type, also when its name has "Bristle" in it.
Align and Distribute Spacing keep a compound path or selected group that contains the key object in place, as they keep the key object itself.
Anchor Align moves the selected points to their bounds or the artboard (#1180). Repeating it, or moving by at most 1e-9 pt on both axes as whole-object Align does, leaves the document, Undo, Redo and saved state unchanged; actual moves still take one undo step. `crates/engine/src/tests.rs` covers repeated alignment, rounding-only alignment, unchanged handles, saved state, command recording and Undo/Redo.
A symbol instance is measured by its symbol's art, as its transform places it, for clicks, the marquee, the bounding box, the Transform panel, Align and Distribute (the art's strokes too with Use Preview Bounds), Fit to Artwork Bounds, exports of the art, thumbnails and canvas culling; one whose symbol is missing is measured as the 20 × 20 square around the origin.