Select: Same Symbol Instance takes its reference from an object that can be selected

Squashed from #5511 onto main.
This commit is contained in:
dotCooCoo
2026-11-04 15:13:32 +07:00
committed by bflatastic
parent 92640179e5
commit a79ab62712
5 changed files with 72 additions and 21 deletions
+12 -8
View File
@@ -11,7 +11,7 @@ use vectorcraft_geom::Vec2;
use super::edit::selected_roots;
use super::pathops::Beside;
use super::select::{is_selectable, selectable_ids, walk_selectable};
use super::select::{first_selectable, is_selectable, selectable_ids, walk_selectable};
use super::{
CommandSpec, EngineError, Point, Result, Session, bad, bool_or, checked_ids_param, cmd, f64_or, f64_req, has_doc, has_selection, id_param, ok,
str_param,
@@ -19,14 +19,18 @@ use super::{
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!("select.same.symbolInstance", "Symbol Instance", ["Select", "Same"], None, "{} → {count}", has_selection, |s, _| same(
s,
cmd!(
"select.same.symbolInstance",
|a, b| match (&a.kind, &b.kind) {
"Symbol Instance",
["Select", "Same"],
None,
"{} select the instances of the symbol of the first selected object that can be selected; objects that are hidden or locked, inside a hidden or locked object or layer, on a template layer, on a locked artboard while nothing is isolated, or outside the isolated group or layer are passed over → {count}",
has_selection,
|s, _| same(s, "select.same.symbolInstance", |a, b| match (&a.kind, &b.kind) {
(NodeKind::SymbolInstance { symbol: x, .. }, NodeKind::SymbolInstance { symbol: y, .. }) => x == y,
_ => false,
}
)),
})
),
cmd!(
"select.same.linkBlockSeries",
"Link Block Series",
@@ -297,9 +301,9 @@ fn finish_same(s: &mut Session, cmd: &str, ids: Vec<NodeId>) -> Result<Value> {
Ok(json!({ "count": st.selection.objects.len() }))
}
/// The objects `eq` matches with the [`first_selectable`] selected object.
fn same(s: &mut Session, cmd: &str, eq: fn(&Node, &Node) -> bool) -> Result<Value> {
let st = s.doc()?;
let r = st.selection.objects.first().and_then(|id| st.doc.node(*id)).cloned().ok_or_else(|| bad(cmd, "nothing selected"))?;
let r = first_selectable(s.doc()?).cloned().ok_or_else(|| bad(cmd, "nothing selected"))?;
let ids = candidates(s, |n| eq(n, &r))?;
finish_same(s, cmd, ids)
}
+7 -8
View File
@@ -267,6 +267,12 @@ pub(crate) fn is_selectable(d: &Document, iso: Option<NodeId>, locks: &LockedArt
}
}
/// The first selected object, in selection order, that [`is_selectable`] accepts.
pub(crate) fn first_selectable(st: &DocState) -> Option<&Node> {
let locks = st.doc.locked_artboards();
st.selection.objects.iter().find(|id| is_selectable(&st.doc, st.isolation, &locks, **id)).and_then(|id| st.doc.node(*id))
}
fn all(s: &mut Session, _: &Value) -> Result<Value> {
// While the Type tool edits text, Select All takes all of that text and leaves the art
// selection as it is (Illustrator: text cursor in a text object).
@@ -718,14 +724,7 @@ fn paint_reported(d: &Document, n: &Node) -> bool {
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 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 first = first_selectable(st).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,
+47
View File
@@ -2049,6 +2049,53 @@ fn select_same_font_takes_its_reference_from_the_isolated_group_on_a_selected_la
assert!(!got.contains(&outside));
}
/// Select › Same › Symbol Instance takes its reference from the first selected object that can be
/// selected: an instance that is hidden, locked, on a template layer or outside the isolated group
/// is passed over.
#[test]
fn select_same_symbol_instance_skips_objects_that_cannot_be_selected() {
const C: &str = "select.same.symbolInstance";
let mut s = session();
let place = |s: &mut Session, name: &str, x: f64| id_of(&s.execute("symbol.place", &json!({"name": name, "x": x, "y": 200})).unwrap());
rect(&mut s, 0.0, 0.0, 20.0, 20.0);
let a1 = id_of(&s.execute("symbol.new", &json!({"name": "A"})).unwrap());
rect(&mut s, 50.0, 0.0, 20.0, 20.0);
let b1 = id_of(&s.execute("symbol.new", &json!({"name": "B"})).unwrap());
let a2 = place(&mut s, "A", 100.0);
let b2 = place(&mut s, "B", 200.0);
// A hidden or locked instance of A, selected first, is not the reference.
for (on, off) in [
(json!({"id": a1.0, "visible": false}), json!({"id": a1.0, "visible": true})),
(json!({"id": a1.0, "locked": true}), json!({"id": a1.0, "locked": false})),
] {
s.execute("layer.setProps", &on).unwrap();
sel(&mut s, &[a1, b1]);
assert_eq!(s.execute(C, &json!({})).unwrap()["count"], 2, "{on}");
assert_eq!(selected(&s), vec![b1, b2], "{on}");
s.execute("layer.setProps", &off).unwrap();
}
// An instance on a template layer, selected by id, is not the reference.
let l2 = id_of(&s.execute("layer.new", &json!({"top": true})).unwrap());
let on_template = place(&mut s, "A", 300.0);
s.execute("layer.setProps", &json!({"id": l2.0, "template": true})).unwrap();
sel(&mut s, &[on_template, b1]);
s.execute(C, &json!({})).unwrap();
assert_eq!(selected(&s), vec![b1, b2]);
// An instance outside the isolated group is not the reference.
sel(&mut s, &[b1, b2]);
let g = id_of(&s.execute("object.group", &json!({})).unwrap());
s.execute("object.isolate", &json!({"id": g.0})).unwrap();
sel(&mut s, &[a2, b1]);
s.execute(C, &json!({})).unwrap();
assert_eq!(selected(&s), vec![b1, b2]);
s.execute("object.exitIsolation", &json!({})).unwrap();
// A selection whose only objects cannot be selected has no reference.
s.execute("layer.setProps", &json!({"id": a1.0, "visible": false})).unwrap();
sel(&mut s, &[a1, on_template]);
assert!(s.execute(C, &json!({})).is_err());
assert_eq!(selected(&s), vec![a1, on_template]);
}
/// Select › Same › Text Fill & Stroke Color selects the type with characters whose fill and stroke
/// both match the first character of the selected type: the same fill with another stroke, or the
/// same stroke with another fill, is left out, and in type with several runs one run has to match
+5 -4
View File
@@ -1465,10 +1465,11 @@ inside a hidden or locked group or layer, on a template layer, or outside the is
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".
`shapeType`, `graphicStyle`, `appearanceAttribute` and `symbolInstance` 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, on a locked artboard while nothing is isolated, or
outside the isolated group is passed over. When the selection holds no other object, they return the error "nothing
selected".
## Shaper Groups
+1 -1
View File
@@ -928,7 +928,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.
Select › Same › Fill Color, Stroke Color, Fill & Stroke, Stroke Weight, Opacity, Blending Mode, Appearance, Shape, Graphic Style, Appearance Attribute and Symbol Instance compare the other objects with the first selected object that can be selected: an object that is hidden or locked, on a template layer, on a locked artboard while nothing is isolated, 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.