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.
This commit is contained in:
dotCooCoo
2026-10-24 17:29:32 +07:00
committed by bflatastic
parent 5e8d41fb8f
commit a272d49ea7
3 changed files with 88 additions and 11 deletions
+24 -11
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![
@@ -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();