Select: Bristle Brush Strokes selects objects by their brush's type

Select > Object > Bristle Brush Strokes selected the objects whose
brush name contains "bristle". It missed objects with a bristle brush
of any other name, and it selected objects whose brush is of another
type with "Bristle" in its name.

select.object.bristleBrushStrokes now looks up the brush of each
object's topmost stroke in the document's brush library and selects the
object when the brush is a bristle brush, whatever its name.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-21 15:42:36 +07:00
committed by bflatastic
parent 6bce3578ca
commit 4bcd14c9c2
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]