Select: select.color picks objects by fill and stroke colour, no object needed

Scripts had to know an object id before they could select art by colour
(select.same.* and select.magicWand start from one). select.color
{fill?, stroke?} takes colours as paint.setFill does and selects the
objects whose fill and stroke read as them in #rrggbb, as
document.inspect reports colours, with Select > Same's selection rules.

The label is translated in each of the 23 catalogs, which
complete_languages_translate_every_menu_string requires.
This commit is contained in:
MortenHusted
2026-10-18 23:50:57 +07:00
committed by bflatastic
parent 09a6fbe470
commit d25289a3be
25 changed files with 103 additions and 0 deletions
+25
View File
@@ -89,6 +89,15 @@ pub fn specs() -> Vec<CommandSpec> {
has_doc,
anchors_many
),
cmd!(
"select.color",
"Select by Color",
[],
None,
"{fill?: colour, stroke?: colour (as paint.setFill's color; at least one)} select the objects whose fill (and stroke) reads as that colour in #rrggbb, as document.inspect reports colours, whatever its swatch or tint; with both, the objects that match both. No object needs to be selected first; Select > Reselect repeats it → {count}",
has_doc,
select_color
),
cmd!(
"select.same.fillColor",
"Fill Color",
@@ -528,6 +537,22 @@ fn stroke_weight(n: &Node) -> f64 {
if paint.is_none() { 0.0 } else { width }
}
/// Select by Color: the objects whose fill and stroke read as the colours given, compared as the
/// `#rrggbb` `document.inspect` reports, so a script can pick art by colour without an object id.
fn select_color(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "select.color";
let hex = |key: &str| match p.get(key).filter(|v| !v.is_null()) {
None => Ok(None),
Some(v) => color_value(v).map(|c| Some(c.to_hex())).ok_or_else(|| bad(C, format!("`{key}`: not a colour: {v}"))),
};
let (fill, stroke) = (hex("fill")?, hex("stroke")?);
if fill.is_none() && stroke.is_none() {
return Err(bad(C, "give `fill`, `stroke` or both"));
}
let reads_as = |paint: Paint, hex: Option<&str>| hex.is_none_or(|h| paint.color().is_some_and(|c| c.to_hex() == h));
select_where(s, C, p, |_, n| !n.is_container() && reads_as(fill_of(n), fill.as_deref()) && reads_as(stroke_of(n).0, stroke.as_deref()))
}
fn same(s: &mut Session, cmd: &str, eq: impl Fn(&Node, &Node) -> bool) -> Result<Value> {
let st = s.doc()?;
let refn = st.selection.objects.first().and_then(|id| st.doc.node(*id)).cloned().ok_or_else(|| bad(cmd, "nothing selected"))?;
+32
View File
@@ -1026,6 +1026,38 @@ fn select_same_paint_reads_type_characters() {
assert_eq!(same(&mut s, t1, "select.same.fillAndStroke"), vec![t1, r]);
}
/// Select by Color needs no object to start from: it matches fills and strokes as
/// `document.inspect` reports them, `#rrggbb`.
#[test]
fn select_color_matches_fill_and_stroke_without_a_selection() {
let mut s = session();
let [a, b, c] = [0.0, 20.0, 40.0].map(|x| rect(&mut s, x, 0.0, 10.0, 10.0));
let t = text(&mut s, "a");
fill(&mut s, a, "#000000");
fill(&mut s, t, "#000000");
// A grey as a PDF stores it reads as #fefefe.
s.execute("paint.setFill", &json!({"ids": [b.0], "color": [0.996, 0.996, 0.996]})).unwrap();
fill(&mut s, c, "#ff0000");
s.execute("paint.setStroke", &json!({"ids": [a.0, c.0], "color": "#0000ff"})).unwrap();
s.execute("select.none", &json!({})).unwrap();
let pick = |s: &mut Session, p: Value| {
s.execute("select.color", &p).unwrap();
let mut v = selected(s);
v.sort();
v
};
assert_eq!(pick(&mut s, json!({"fill": "#000000"})), vec![a, t]);
assert_eq!(pick(&mut s, json!({"fill": "#fefefe"})), vec![b]);
assert_eq!(pick(&mut s, json!({"stroke": "#0000ff"})), vec![a, c]);
assert_eq!(pick(&mut s, json!({"fill": "#000000", "stroke": "#0000ff"})), vec![a]);
// Select > Reselect repeats it.
s.execute("select.none", &json!({})).unwrap();
s.execute("select.reselect", &json!({})).unwrap();
assert_eq!(selected(&s), vec![a]);
assert!(s.execute("select.color", &json!({})).is_err());
assert!(s.execute("select.color", &json!({"fill": "not a colour"})).is_err());
}
#[test]
fn select_direction_handles_selects_all_anchors() {
let mut s = session();