From a272d49ea7e9096f3ade0b56044cebf726c4f549 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 20 Oct 2026 17:28:28 -0700 Subject: [PATCH] Select: Same commands for paint, appearance and shape select compound paths Select > Same > Fill Color, Stroke Color, Stroke Weight, Fill & Stroke, Opacity, Blending Mode, Appearance and Shape skipped containers, so they never selected a compound path, even when the compound path was the object selected first. With a member path of a compound path selected first, these commands and Same Graphic Style and Appearance Attribute compared the other objects with the member, which has no paint or style of its own. Graphic Style then reported that the selection had no graphic style, and Appearance Attribute that the object had no fill, stroke or effect. The eight commands now match a compound path on its own paint and attributes, as select.color does, and skip its member paths. When a member path is selected first, all ten commands compare the other objects with its compound path. In isolation mode inside a compound path, the member paths are matched as before. --- crates/engine/src/cmd/select.rs | 35 ++++++++++++++++++++--------- crates/engine/src/tests_menucmds.rs | 34 ++++++++++++++++++++++++++++ crates/engine/src/tests_styles.rs | 30 +++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 11 deletions(-) diff --git a/crates/engine/src/cmd/select.rs b/crates/engine/src/cmd/select.rs index 8a7d220e3..6d8d7c4a5 100644 --- a/crates/engine/src/cmd/select.rs +++ b/crates/engine/src/cmd/select.rs @@ -8,6 +8,7 @@ use vectorcraft_doc::{Document, Node, NodeId, NodeKind}; use super::wand::{fill_of, stroke_of}; use super::*; +use crate::DocState; pub fn specs() -> Vec { vec![ @@ -663,10 +664,27 @@ fn paint_reported(d: &Document, n: &Node) -> bool { compound(n) || (!n.is_container() && !d.parent_of(n.id).and_then(|p| d.node(p)).is_some_and(compound)) } +/// The object Select > Same compares the others with, and whether isolation mode is inside a +/// compound path. The object is the first selected one, or its compound path when it is a member +/// of one; in isolation mode inside a compound path it is the member path itself. +fn same_reference<'a>(st: &'a DocState, cmd: &str) -> Result<(&'a Node, bool)> { + let compound = |n: &Node| matches!(n.kind, NodeKind::Compound { .. }); + let members = st.isolation.and_then(|i| st.doc.ancestry(i)).is_some_and(|a| a.iter().any(|i| st.doc.node(*i).is_some_and(compound))); + let first = st.selection.objects.first().and_then(|id| st.doc.node(*id)).ok_or_else(|| bad(cmd, "nothing selected"))?; + let refn = match st.doc.parent_of(first.id).and_then(|p| st.doc.node(p)) { + Some(p) if !members && compound(p) => p, + _ => first, + }; + Ok((refn, members)) +} + +/// Select > Same: the objects `eq` matches with the object [`same_reference`] returns, among those +/// [`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 st = s.doc()?; - let refn = st.selection.objects.first().and_then(|id| st.doc.node(*id)).cloned().ok_or_else(|| bad(cmd, "nothing selected"))?; - select_where(s, cmd, &json!({}), |_, n| !n.is_container() && eq(n, &refn)) + let (refn, members) = same_reference(s.doc()?, cmd)?; + 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 the objects [`walk_selectable`] visits that `f` accepts (not looking inside accepted @@ -690,13 +708,8 @@ fn select_where(s: &mut Session, cmd: &str, p: &Value, f: impl Fn(&Document, &No fn same_graphic_style(s: &mut Session, _: &Value) -> Result { let cmd = "select.same.graphicStyle"; let st = s.doc()?; - let id = st - .selection - .objects - .first() - .and_then(|id| super::style::linked_style(&st.doc, st.doc.node(*id)?)) - .map(|g| g.id) - .ok_or_else(|| bad(cmd, "the selection has no graphic style"))?; + let (refn, _) = same_reference(st, cmd)?; + let id = super::style::linked_style(&st.doc, refn).map(|g| g.id).ok_or_else(|| bad(cmd, "the selection has no graphic style"))?; select_where(s, cmd, &json!({}), |d, n| n.extras().graphic_style == Some(id) && super::style::linked_style(d, n).is_some()) } @@ -709,7 +722,7 @@ enum Attribute { fn same_attribute(s: &mut Session, p: &Value) -> Result { let cmd = "select.same.appearanceAttribute"; let st = s.doc()?; - let refn = st.selection.objects.first().and_then(|id| st.doc.node(*id)).ok_or_else(|| bad(cmd, "nothing selected"))?; + let (refn, _) = same_reference(st, cmd)?; let ap = &refn.appearance; let item = p.get("item").and_then(Value::as_u64).map(index).or(s.appearance_item()); let attr = match (item.and_then(|i| ap.items.get(i)), ap.effects.first()) { diff --git a/crates/engine/src/tests_menucmds.rs b/crates/engine/src/tests_menucmds.rs index c0fb6de45..8e57771b0 100644 --- a/crates/engine/src/tests_menucmds.rs +++ b/crates/engine/src/tests_menucmds.rs @@ -1303,6 +1303,40 @@ fn select_same_paint_reads_type_characters() { assert_eq!(same(&mut s, t1, "select.same.fillAndStroke"), vec![t1, r]); } +/// Select > Same matches a compound path on its own paint, as a whole: never its member paths. +/// When a member path is selected first, the other objects are compared with its compound path. +#[test] +fn select_same_matches_compound_paths_on_their_own_paint() { + let mut s = session(); + let [a, b] = [0.0, 20.0].map(|x| rect(&mut s, x, 0.0, 10.0, 10.0)); + let outer = rect(&mut s, 100.0, 0.0, 50.0, 50.0); + let inner = rect(&mut s, 110.0, 10.0, 20.0, 20.0); + let r = rect(&mut s, 40.0, 0.0, 10.0, 10.0); + for id in [a, b, outer, inner] { + fill(&mut s, id, "#000000"); + } + fill(&mut s, r, "#ff0000"); + sel(&mut s, &[outer, inner]); + let c = id_of(&s.execute("object.compoundPath.make", &json!({})).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 from in [a, c, outer] { + assert_eq!(same(&mut s, from, "select.same.fillColor"), vec![a, b, c], "from {from:?}"); + } + assert_eq!(same(&mut s, inner, "select.same.shapeType"), vec![c]); + fill(&mut s, c, "#ff0000"); + assert_eq!(same(&mut s, a, "select.same.fillColor"), vec![a, b]); + assert_eq!(same(&mut s, r, "select.same.fillColor"), vec![r, c]); + // Isolated in the compound path, its member paths are the objects to match. + s.execute("object.isolate", &json!({"id": c.0})).unwrap(); + assert_eq!(same(&mut s, outer, "select.same.shapeType"), vec![outer, inner]); +} + /// Select by Color needs no object to start from: it matches fills and strokes as /// `document.inspect` reports them, `#rrggbb`. #[test] diff --git a/crates/engine/src/tests_styles.rs b/crates/engine/src/tests_styles.rs index 1b464d564..2a8a379c8 100644 --- a/crates/engine/src/tests_styles.rs +++ b/crates/engine/src/tests_styles.rs @@ -256,6 +256,36 @@ fn select_same_appearance_attribute() { assert_eq!(s.doc().unwrap().selection.objects, vec![a, b]); } +/// With a member path of a compound path selected first, Select > Same > Graphic Style and +/// Appearance Attribute compare the other objects with the compound path, except in isolation mode +/// inside it. +#[test] +fn select_same_style_and_attribute_from_a_compound_path_member() { + let mut s = session(); + let a = rect(&mut s, 10.0); + set_fill(&mut s, a, "#ff0000"); + run(&mut s, "effect.apply", json!({"effect": "distort.roughen"})); + run(&mut s, "graphicStyle.new", json!({"name": "Rough"})); + run(&mut s, "graphicStyle.apply", json!({"name": "Rough"})); + let outer = rect(&mut s, 100.0); + let inner = rect(&mut s, 120.0); + run(&mut s, "select.set", json!({"ids": [outer.0, inner.0]})); + let c = NodeId(run(&mut s, "object.compoundPath.make", json!({}))["id"].as_u64().unwrap()); + run(&mut s, "graphicStyle.apply", json!({"name": "Rough"})); + rect(&mut s, 300.0); + for cmd in ["select.same.graphicStyle", "select.same.appearanceAttribute"] { + for from in [c, outer] { + run(&mut s, "select.set", json!({"ids": [from.0]})); + run(&mut s, cmd, json!({})); + assert_eq!(s.doc().unwrap().selection.objects, vec![a, c], "{cmd} from {from:?}"); + } + } + // Isolated in the compound path, its member paths are compared, and they carry no style. + run(&mut s, "object.isolate", json!({"id": c.0})); + run(&mut s, "select.set", json!({"ids": [outer.0]})); + assert!(s.execute("select.same.graphicStyle", &json!({})).is_err()); +} + #[test] fn document_info_lists_style_names() { let mut s = session();