mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 18:51:35 +00:00
Merge pull request #5510 from dotCooCoo/fix/same-reference-skips-unreachable
Select: Same commands take their reference from an object that can be selected
This commit is contained in:
@@ -107,7 +107,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); 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",
|
||||
"{} 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; every select.same command that compares with the first selected object passes over a selected object that is hidden or locked, on a template layer or outside the isolated group",
|
||||
has_selection,
|
||||
|s, _| {
|
||||
let tint = s.prefs.select_same_tint_percent;
|
||||
@@ -686,12 +686,21 @@ fn paint_reported(d: &Document, n: &Node) -> bool {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// compound path. The object is the first selected one that [`is_selectable`] accepts (none that is
|
||||
/// hidden, locked, on a hidden, locked or template layer, or outside the isolated group), 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 locks = st.doc.locked_artboards();
|
||||
let first = st
|
||||
.selection
|
||||
.objects
|
||||
.iter()
|
||||
.find(|id| is_selectable(&st.doc, st.isolation, &locks, **id))
|
||||
.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,
|
||||
|
||||
@@ -2215,6 +2215,58 @@ fn select_same_matches_compound_paths_on_their_own_paint() {
|
||||
assert_eq!(same(&mut s, outer, "select.same.shapeType"), vec![outer, inner]);
|
||||
}
|
||||
|
||||
/// Select > Same compares the other objects with the first selected object that can be selected:
|
||||
/// one that is hidden or locked, on a template layer, or outside the isolated group is passed
|
||||
/// over. A selection that holds no such object has nothing to compare with.
|
||||
#[test]
|
||||
fn select_same_skips_a_reference_that_cannot_be_selected() {
|
||||
let mut s = session();
|
||||
let [red, blue, other_blue, other_red, grouped_red] = [0.0, 20.0, 40.0, 60.0, 80.0].map(|x| rect(&mut s, x, 0.0, 10.0, 10.0));
|
||||
for id in [red, other_red, grouped_red] {
|
||||
fill(&mut s, id, "#ff0000");
|
||||
}
|
||||
for id in [blue, other_blue] {
|
||||
fill(&mut s, id, "#0000ff");
|
||||
}
|
||||
let same = |s: &mut Session, from: &[NodeId], cmd: &str| {
|
||||
sel(s, from);
|
||||
assert_eq!(selected(s), from, "select.set keeps {from:?}");
|
||||
s.execute(cmd, &json!({})).unwrap();
|
||||
let mut v = selected(s);
|
||||
v.sort();
|
||||
v
|
||||
};
|
||||
// A hidden or locked red path selected first is not the reference.
|
||||
for (on, off) in [
|
||||
(json!({"id": red.0, "visible": false}), json!({"id": red.0, "visible": true})),
|
||||
(json!({"id": red.0, "locked": true}), json!({"id": red.0, "locked": false})),
|
||||
] {
|
||||
s.execute("layer.setProps", &on).unwrap();
|
||||
for cmd in ["select.same.fillColor", "select.same.fillAndStroke"] {
|
||||
assert_eq!(same(&mut s, &[red, blue], cmd), vec![blue, other_blue], "{cmd} {on}");
|
||||
}
|
||||
s.execute("layer.setProps", &off).unwrap();
|
||||
}
|
||||
// A red path on a template layer is not the reference.
|
||||
let l2 = id_of(&s.execute("layer.new", &json!({"top": true})).unwrap());
|
||||
let on_template = rect(&mut s, 100.0, 0.0, 10.0, 10.0);
|
||||
fill(&mut s, on_template, "#ff0000");
|
||||
s.execute("layer.setProps", &json!({"id": l2.0, "template": true})).unwrap();
|
||||
assert_eq!(same(&mut s, &[on_template, blue], "select.same.fillColor"), vec![blue, other_blue]);
|
||||
// A red path outside the isolated group is not the reference.
|
||||
sel(&mut s, &[blue, grouped_red]);
|
||||
let g = id_of(&s.execute("object.group", &json!({})).unwrap());
|
||||
s.execute("object.isolate", &json!({"id": g.0})).unwrap();
|
||||
assert_eq!(same(&mut s, &[other_red, blue], "select.same.fillColor"), vec![blue]);
|
||||
s.execute("object.exitIsolation", &json!({})).unwrap();
|
||||
// With no object that can be selected, there is nothing to compare with, and the selection stays.
|
||||
s.execute("layer.setProps", &json!({"id": red.0, "visible": false})).unwrap();
|
||||
sel(&mut s, &[red, on_template]);
|
||||
let r = s.execute("select.same.fillColor", &json!({}));
|
||||
assert!(matches!(&r, Err(EngineError::BadParams { msg, .. }) if *msg == "nothing selected"), "{r:?}");
|
||||
assert_eq!(selected(&s), vec![red, on_template]);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
||||
@@ -286,6 +286,32 @@ fn select_same_style_and_attribute_from_a_compound_path_member() {
|
||||
assert!(s.execute("select.same.graphicStyle", &json!({})).is_err());
|
||||
}
|
||||
|
||||
/// Select > Same > Graphic Style and Appearance Attribute pass over a hidden object selected first
|
||||
/// and compare the other objects with the next one.
|
||||
#[test]
|
||||
fn select_same_style_and_attribute_skip_a_hidden_reference() {
|
||||
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 b = rect(&mut s, 100.0);
|
||||
run(&mut s, "graphicStyle.apply", json!({"name": "Rough"}));
|
||||
let hidden = rect(&mut s, 200.0);
|
||||
set_fill(&mut s, hidden, "#0000ff");
|
||||
run(&mut s, "graphicStyle.new", json!({"name": "Blue"}));
|
||||
run(&mut s, "graphicStyle.apply", json!({"name": "Blue"}));
|
||||
rect(&mut s, 300.0);
|
||||
run(&mut s, "graphicStyle.apply", json!({"name": "Blue"}));
|
||||
run(&mut s, "layer.setProps", json!({"id": hidden.0, "visible": false}));
|
||||
for cmd in ["select.same.graphicStyle", "select.same.appearanceAttribute"] {
|
||||
run(&mut s, "select.set", json!({"ids": [hidden.0, a.0]}));
|
||||
run(&mut s, cmd, json!({}));
|
||||
assert_eq!(s.doc().unwrap().selection.objects, vec![a, b], "{cmd}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_info_lists_style_names() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -1459,6 +1459,12 @@ selected objects and the objects inside them, in selection order, as the one to
|
||||
inside a hidden or locked group or layer, on a template layer, or outside the isolated group is passed over. When the
|
||||
selection holds no other type, they return the error "select a text object".
|
||||
|
||||
`select.same.fillColor`, `strokeColor`, `fillAndStroke`, `strokeWeight`, `opacity`, `blendingMode`, `appearance`,
|
||||
`shapeType`, `graphicStyle` and `appearanceAttribute` compare the other objects with the first selected object, in
|
||||
selection order, that these commands could select. A selected object that is hidden or locked, inside a hidden or
|
||||
locked group or layer, on a template layer, or outside the isolated group is passed over. When the selection holds no other
|
||||
object, they return the error "nothing selected".
|
||||
|
||||
## Shaper Groups
|
||||
|
||||
`shaper.releaseSource {source, group?}` takes the original shape `source` out of its Shaper Group: the shape becomes a
|
||||
|
||||
@@ -924,6 +924,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Select › Same › Link Block Series selects every frame of the threads of the selected type; type in no thread selects itself (`select.same.linkBlockSeries`). The menu entry is listed in [Select objects by characteristics](https://helpx.adobe.com/illustrator/desktop/manage-objects/select-objects/select-objects-by-characteristics.html); selecting several threads at once and type in no thread selecting itself are VectorCraft's own behavior.
|
||||
Select › Same › Text Fill & Stroke Color selects the type whose characters match both the fill and the stroke of the selected type (`select.same.textFillAndStrokeColor`). The menu entry is listed in [Select objects by characteristics](https://helpx.adobe.com/illustrator/desktop/manage-objects/select-objects/select-objects-by-characteristics.html); requiring one run of characters to match both is VectorCraft's own behavior.
|
||||
The Select › Same font and text color commands take the type to match from the selected type that can be selected: type that is hidden or locked, on a template layer, or outside the isolated group is passed over. This is VectorCraft's own behavior.
|
||||
Select › Same › Fill Color, Stroke Color, Fill & Stroke, Stroke Weight, Opacity, Blending Mode, Appearance, Shape, Graphic Style and Appearance Attribute compare the other objects with the first selected object that can be selected: an object that is hidden or locked, on a template layer, or outside the isolated group is passed over. This is VectorCraft's own behavior.
|
||||
Object › Flatten Transparency takes a frame of threaded text out of the thread in the same way when Convert All Text to Outlines outlines it or when the frame becomes flat regions or an image, and the other frames keep the text they hold.
|
||||
New Symbol and Redefine Symbol on a frame of threaded text take it out of the thread with the text it shows, which the symbol's art holds, and the other frames keep the text they hold. When the last frame goes into the symbol, the text it has no room for goes after the text of the last frame that stays. When every frame of a thread goes into the symbol, the copy of the last frame keeps all its text.
|
||||
With Optical Margin Alignment on, or Roman Hanging Punctuation on in the first paragraph, the glyphs at the ends of left-to-right type on a path hang outside its brackets, measured along the path. The type hangs at the start bracket when its line is placed there, hangs at the end bracket when the line is placed there, and hangs at both when it is centered. Glyphs that hang past the end bracket do not make the type overflow, and a hang stops at an open path's end. SVG export writes such type as outlines.
|
||||
|
||||
Reference in New Issue
Block a user