diff --git a/crates/engine/src/cmd/select.rs b/crates/engine/src/cmd/select.rs index b8b04482f..462746771 100644 --- a/crates/engine/src/cmd/select.rs +++ b/crates/engine/src/cmd/select.rs @@ -5,6 +5,7 @@ use std::collections::BTreeSet; use serde_json::{Value, json}; use vectorcraft_color::Paint; use vectorcraft_doc::{Document, Node, NodeId, NodeKind}; +use vectorcraft_tools::builder::is_live_paint; use super::wand::{fill_of, stroke_of}; use super::{ @@ -106,11 +107,11 @@ pub fn specs() -> Vec { "Fill Color", ["Select", "Same"], None, - "{} the objects filled with the first selected object's fill colour (any tint of its global or spot swatch, the same tint with prefs selectSameTintPercent); a member path of a compound path selected first compares as its compound path, except in isolation mode inside that compound path", + "{} the objects filled with the first selected object's fill colour (any tint of its global or spot swatch, the same tint with prefs selectSameTintPercent); a member path of a compound path selected first compares as its compound path, except in isolation mode inside that compound path; from a Live Paint face, the faces of its Live Paint group only, and from any other object, no Live Paint face", has_selection, |s, _| { let tint = s.prefs.select_same_tint_percent; - same(s, "select.same.fillColor", |a, b| same_paint(&fill_of(a), &fill_of(b), tint)) + same_fill(s, "select.same.fillColor", |a, b| same_paint(&fill_of(a), &fill_of(b), tint)) } ), cmd!( @@ -135,11 +136,11 @@ pub fn specs() -> Vec { "Fill & Stroke", ["Select", "Same"], None, - "{} the objects with the first selected object's fill and stroke colours (tints and compound paths as select.same.fillColor)", + "{} the objects with the first selected object's fill and stroke colours (tints, compound paths and Live Paint faces as select.same.fillColor)", has_selection, |s, _| { let tint = s.prefs.select_same_tint_percent; - same(s, "select.same.fillAndStroke", |a, b| { + same_fill(s, "select.same.fillAndStroke", |a, b| { same_paint(&fill_of(a), &fill_of(b), tint) && same_paint(&stroke_of(a).0, &stroke_of(b).0, tint) }) } @@ -684,9 +685,30 @@ fn same_reference<'a>(st: &'a DocState, cmd: &str) -> Result<(&'a Node, bool)> { /// [`paint_reported`] accepts. A compound path matches as a whole. In isolation mode inside a /// compound path, its member paths are matched instead. fn same(s: &mut Session, cmd: &str, eq: impl Fn(&Node, &Node) -> bool) -> Result { - let (refn, members) = same_reference(s.doc()?, cmd)?; + same_in(s, cmd, false, eq) +} + +/// Select > Same > Fill Color and Fill & Stroke: [`same`], kept to one side of a Live Paint group. +/// From a face or edge of a Live Paint group, only the faces and edges of that group match; from +/// any other object, no face or edge of a Live Paint group does +/// (). +fn same_fill(s: &mut Session, cmd: &str, eq: impl Fn(&Node, &Node) -> bool) -> Result { + same_in(s, cmd, true, eq) +} + +fn same_in(s: &mut Session, cmd: &str, live_paint: bool, eq: impl Fn(&Node, &Node) -> bool) -> Result { + let st = s.doc()?; + let (refn, members) = same_reference(st, cmd)?; + let group = live_paint_group(&st.doc, refn.id); let refn = refn.clone(); - select_where(s, cmd, &json!({}), |d, n| (if members { !n.is_container() } else { paint_reported(d, n) }) && eq(n, &refn)) + select_where(s, cmd, &json!({}), |d, n| { + (!live_paint || live_paint_group(d, n.id) == group) && (if members { !n.is_container() } else { paint_reported(d, n) }) && eq(n, &refn) + }) +} + +/// The Live Paint group `id` is a face or edge of. +fn live_paint_group(d: &Document, id: NodeId) -> Option { + d.parent_of(id).filter(|p| d.node(*p).is_some_and(is_live_paint)) } /// Select the objects [`walk_selectable`] visits that `f` accepts (not looking inside accepted diff --git a/crates/engine/src/tests/menucmds.rs b/crates/engine/src/tests/menucmds.rs index df6f7db08..ff11009b1 100644 --- a/crates/engine/src/tests/menucmds.rs +++ b/crates/engine/src/tests/menucmds.rs @@ -1813,6 +1813,48 @@ fn select_same_matches_compound_paths_on_their_own_paint() { assert_eq!(same(&mut s, outer, "select.same.shapeType"), vec![outer, inner]); } +/// Select > Same > Fill Color and Fill & Stroke from a Live Paint face select the faces of that +/// Live Paint group with the same fill, and nothing outside it. From an object outside every Live +/// Paint group, they select no face. +#[test] +fn select_same_fill_keeps_live_paint_faces_to_their_group() { + let mut s = session(); + let live = |s: &mut Session, y: f64| { + let a = rect(s, 0.0, y, 100.0, 100.0); + let b = rect(s, 50.0, y, 100.0, 100.0); + sel(s, &[a, b]); + id_of(&s.execute("livePaint.make", &json!({})).unwrap()) + }; + let paint = |s: &mut Session, g: NodeId, x: f64, y: f64, hex: &str| { + NodeId(s.execute("livePaint.fill", &json!({"group": g.0, "point": [x, y], "color": hex})).unwrap()["face"].as_u64().unwrap()) + }; + let g1 = live(&mut s, 0.0); + let g2 = live(&mut s, 300.0); + let [left, right] = [25.0, 125.0].map(|x| paint(&mut s, g1, x, 50.0, "#ff0000")); + let middle = paint(&mut s, g1, 75.0, 50.0, "#0000ff"); + let other = paint(&mut s, g2, 25.0, 350.0, "#ff0000"); + let [r1, r2] = [400.0, 450.0].map(|x| rect(&mut s, x, 0.0, 10.0, 10.0)); + for r in [r1, r2] { + fill(&mut s, r, "#ff0000"); + s.execute("paint.setStroke", &json!({"none": true, "ids": [r.0]})).unwrap(); + } + let same = |s: &mut Session, from: NodeId, cmd: &str| { + sel(s, &[from]); + s.execute(cmd, &json!({})).unwrap(); + let mut v = selected(s); + v.sort(); + v + }; + for cmd in ["select.same.fillColor", "select.same.fillAndStroke"] { + let mut faces = vec![left, right]; + faces.sort(); + assert_eq!(same(&mut s, left, cmd), faces, "{cmd} from a face"); + assert_eq!(same(&mut s, middle, cmd), vec![middle], "{cmd} from the blue face"); + assert_eq!(same(&mut s, other, cmd), vec![other], "{cmd} from the other group's face"); + assert_eq!(same(&mut s, r1, cmd), vec![r1, r2], "{cmd} from a path outside"); + } +} + /// Select by Color needs no object to start from: it matches fills and strokes as /// `document.inspect` reports them, `#rrggbb`. #[test]