Merge pull request #3943 from dotCooCoo/fix/select-same-matches-compound-paths

Select: Same commands for paint, appearance and shape select compound paths
This commit is contained in:
BFlat
2026-10-24 17:53:01 +07:00
committed by GitHub
3 changed files with 93 additions and 16 deletions
+29 -16
View File
@@ -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<CommandSpec> {
vec![
@@ -103,7 +104,7 @@ pub fn specs() -> Vec<CommandSpec> {
"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)",
"{} 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",
has_selection,
|s, _| {
let tint = s.prefs.select_same_tint_percent;
@@ -115,7 +116,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Stroke Color",
["Select", "Same"],
None,
"{} the objects stroked with the first selected object's stroke colour (tints as select.same.fillColor)",
"{} the objects stroked with the first selected object's stroke colour (tints and compound paths as select.same.fillColor)",
has_selection,
|s, _| {
let tint = s.prefs.select_same_tint_percent;
@@ -132,7 +133,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Fill & Stroke",
["Select", "Same"],
None,
"{} the objects with the first selected object's fill and stroke colours (tints as select.same.fillColor)",
"{} the objects with the first selected object's fill and stroke colours (tints and compound paths as select.same.fillColor)",
has_selection,
|s, _| {
let tint = s.prefs.select_same_tint_percent;
@@ -179,7 +180,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Graphic Style",
["Select", "Same"],
None,
"{} the objects linked to the graphic style the first selected object is linked to",
"{} the objects linked to the graphic style the first selected object is linked to (compound paths as select.same.fillColor)",
has_selection,
same_graphic_style
),
@@ -188,7 +189,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Appearance Attribute",
["Select", "Same"],
None,
"{item?: appearance item index} the objects sharing an appearance attribute of the first selected object: its fill or stroke `item` (default: the Appearance panel's active item), else its first effect, else its topmost fill",
"{item?: appearance item index} the objects sharing an appearance attribute of the first selected object: its fill or stroke `item` (default: the Appearance panel's active item), else its first effect, else its topmost fill (compound paths as select.same.fillColor)",
has_selection,
same_attribute
),
@@ -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<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"))?;
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<Value> {
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<Value> {
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()) {
+34
View File
@@ -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]
+30
View File
@@ -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();