diff --git a/crates/engine/src/cmd/globaledit.rs b/crates/engine/src/cmd/globaledit.rs index c13687880..6ff31dabe 100644 --- a/crates/engine/src/cmd/globaledit.rs +++ b/crates/engine/src/cmd/globaledit.rs @@ -11,6 +11,15 @@ //! every artboard and the canvas around them by default; it can be limited to a range of //! artboards (`1, 3-5`) and can leave out the objects that lie on no artboard. Images, type, //! masks, linked files and selections of more than one object are not supported. +//! - The help page "Edit groups with similar objects" +//! (): +//! with a group selected, Start Global Edit finds the groups like it, and an edit made to an +//! object inside one of them is made to the matching object in each of the others. Here a group +//! is like another when it holds as many objects, in the same order, each like the one at its +//! place (paths, compound paths, symbol instances and groups of them, clipping paths included), +//! and they lie the same way relative to each other: the group as a whole is moved or scaled. +//! With Appearance on, each of those objects must have the same fill and stroke too; with Size, +//! the groups the same width and height. //! - While Global Edit is on, an edit made to one of the found objects is made to all of them, //! each in proportion to its own size: halving the height of a 100 pt object halves a 20 pt //! match to 10 pt. Stop Global Edit (or Escape) ends it. @@ -42,8 +51,16 @@ //! takes the selection there is. Whenever the document is put back (Undo, Redo, a cancelled drag //! or scrub, an edit that failed), the members it lacks (or no longer supports) leave the Global //! Edit, and it ends when fewer than two are left ([`follow_history`]), so that an object made -//! later under a freed id is never edited. Rotated copies, groups (the help page "Edit groups with -//! similar objects") and excluding a member with a Shift-click are not covered. +//! later under a freed id is never edited. +//! +//! A member that is a group takes, of an edit made inside the model group, what changed about +//! each object in it, given by the same rules to the object at the same place in the member (its +//! hidden and locked objects are left as they are), and what changed about the group itself (its +//! opacity, blending, fills and effects). When the model group gains or loses objects, or one of +//! them becomes another kind of object, each member takes the model's objects at that level +//! whole, keeping its own ids in order. At most [`MAX_MEMBERS`] objects are taken, so that a huge +//! document can't stall the start or every edit. Rotated copies and excluding a member with a +//! Shift-click are not covered. use serde::Serialize; use serde_json::{Value, json}; @@ -55,6 +72,9 @@ use super::{CommandSpec, Result, Session, bad, bool_or, cmd, has_doc}; const C: &str = "select.globalEdit"; const OPTIONS: &str = "select.globalEditOptions"; +/// The most objects a Global Edit takes, the selected one included. +pub const MAX_MEMBERS: usize = 10_000; +const UNSUPPORTED: &str = "Global Edit works on paths, compound paths, symbol instances and groups of them"; pub fn specs() -> Vec { vec![ @@ -63,7 +83,7 @@ pub fn specs() -> Vec { "Start Global Edit", ["Select"], None, - "{appearance?: bool, size?: bool, artboards?: \"all\" | range such as \"1, 3-5\", includeCanvas?: bool} (each defaults to the Global Edit Options) while Global Edit is off: start it on the one selected object, with the objects of the same shape (and, with `appearance`, the same fill and stroke; with `size`, the same width and height) on the artboards given (and, with `includeCanvas`, off every artboard) → {active: true, count, ids}; while on: stop it (Escape does the same) → {active: false}", + "{appearance?: bool, size?: bool, artboards?: \"all\" | range such as \"1, 3-5\", includeCanvas?: bool} (each defaults to the Global Edit Options) while Global Edit is off: start it on the one selected object (a path, compound path, symbol instance or group of them), with the objects of the same shape (for a group: the groups holding objects like its own, at the same places) (and, with `appearance`, the same fill and stroke; with `size`, the same width and height) on the artboards given (and, with `includeCanvas`, off every artboard), at most 10000 → {active: true, count, ids}; while on: stop it (Escape does the same) → {active: false}", can_toggle, toggle ), @@ -144,10 +164,11 @@ pub struct GlobalEdit { } /// An outline reduced to its form: the anchors and handles placed in a unit box, with what tells -/// such outlines apart (kind, fill rule, subpaths and their anchor counts, open or closed). +/// such outlines apart (kind, fill rule, subpaths and their anchor counts, open or closed). A +/// group's is its objects' together, in order. struct Form { tag: Vec, - symbol: Option, + symbols: Vec, points: Vec, /// The unit box → the object. frame: Affine, @@ -155,62 +176,110 @@ struct Form { impl Form { fn of(n: &Node) -> Option { - let mut tag = vec![]; - let mut points = vec![]; - let mut symbol = None; - let mut push = |path: &PathData, tag: &mut Vec| { - tag.push(path.subpaths.len() as u64); - for sp in &path.subpaths { - tag.extend([u64::from(sp.closed), sp.anchors.len() as u64]); - points.extend(sp.anchors.iter().flat_map(|a| [a.p, a.h_in, a.h_out])); - } - }; - let rule = |r: vectorcraft_geom::FillRule| u64::from(r == vectorcraft_geom::FillRule::EvenOdd); - match &n.kind { - NodeKind::Path { path, rule: r, clipping: false, guide: false, .. } => { - tag.extend([0, rule(*r)]); - push(path, &mut tag); - } - NodeKind::Compound { children, rule: r } => { - tag.extend([1, rule(*r), children.len() as u64]); - for c in children { - push(c.path_data()?, &mut tag); - } - } - NodeKind::SymbolInstance { symbol: s, xf } => { - tag.push(2); - symbol = Some(s.clone()); - points.extend([*xf * Point::ZERO, *xf * Point::new(1.0, 0.0), *xf * Point::new(0.0, 1.0)]); - } - _ => return None, - } - let first = *points.first()?; - let b = points.iter().fold(Rect::from_points(first, first), |r, p| r.union_pt(*p)); + let mut f = Self { tag: vec![], symbols: vec![], points: vec![], frame: Affine::IDENTITY }; + f.add(n, 0)?; + let first = *f.points.first()?; + let b = f.points.iter().fold(Rect::from_points(first, first), |r, p| r.union_pt(*p)); let side = b.width().max(b.height()); if !(side.is_finite() && side > 1e-9) { return None; } - let frame = Affine::translate(b.origin().to_vec2()) * Affine::scale(side); - let back = frame.inverse(); - points.iter_mut().for_each(|p| *p = back * *p); - Some(Self { tag, symbol, points, frame }) + f.frame = Affine::translate(b.origin().to_vec2()) * Affine::scale(side); + let back = f.frame.inverse(); + f.points.iter_mut().for_each(|p| *p = back * *p); + Some(f) + } + + /// Add `n`, `depth` groups down from the selected object; `None` when it isn't supported. + fn add(&mut self, n: &Node, depth: usize) -> Option<()> { + let rule = |r: vectorcraft_geom::FillRule| u64::from(r == vectorcraft_geom::FillRule::EvenOdd); + match &n.kind { + // A clipping path only as part of a (clip) group. + NodeKind::Path { path, rule: r, clipping, guide: false, .. } if !*clipping || depth > 0 => { + self.tag.extend([0, rule(*r), u64::from(*clipping)]); + self.push(path); + } + NodeKind::Compound { children, rule: r } => { + self.tag.extend([1, rule(*r), children.len() as u64]); + for c in children { + self.push(c.path_data()?); + } + } + NodeKind::SymbolInstance { symbol, xf } => { + self.tag.push(2); + self.symbols.push(symbol.clone()); + self.points.extend([*xf * Point::ZERO, *xf * Point::new(1.0, 0.0), *xf * Point::new(0.0, 1.0)]); + } + // Documents are never deeper than `MAX_DEPTH`: the check only bounds the recursion. + NodeKind::Group { children, clip } if plain_group(n) && depth < vectorcraft_doc::MAX_DEPTH => { + self.tag.extend([3, u64::from(*clip), children.len() as u64]); + for c in children { + if c.extras().mask.is_some() { + return None; + } + self.add(c, depth + 1)?; + } + } + _ => return None, + } + Some(()) + } + + fn push(&mut self, path: &PathData) { + self.tag.push(path.subpaths.len() as u64); + for sp in &path.subpaths { + self.tag.extend([u64::from(sp.closed), sp.anchors.len() as u64]); + self.points.extend(sp.anchors.iter().flat_map(|a| [a.p, a.h_in, a.h_out])); + } } fn same(&self, o: &Form) -> bool { self.tag == o.tag - && self.symbol == o.symbol + && self.symbols == o.symbols && self.points.len() == o.points.len() && self.points.iter().zip(&o.points).all(|(a, b)| a.distance(*b) < 1e-6) } } -/// Global Edit supports paths, compound paths and symbol instances; not images, type, linked -/// files, objects with an opacity mask, clipping paths or guides (the help page lists images, -/// text, masks and links as unsupported). +/// Global Edit supports paths, compound paths, symbol instances and groups of them; not images, +/// type, linked files, objects with an opacity mask, clipping paths or guides (the help page lists +/// images, text, masks and links as unsupported). fn supported(n: &Node) -> bool { n.extras().mask.is_none() && Form::of(n).is_some() } +/// A plain group (a clip group too), not one that is live art (a graph, Shaper group, dimension, +/// objects on a path or a tracing). +fn plain_group(n: &Node) -> bool { + let x = n.extras(); + matches!(n.kind, NodeKind::Group { .. }) + && x.graph.is_none() + && x.shaper.is_none() + && x.dimension.is_none() + && x.objects_on_path.is_none() + && x.trace.is_none() +} + +/// Do `a` and `b` show the same fill and stroke, and so does each object in them (groups like each +/// other)? +fn same_look(a: &Node, b: &Node) -> bool { + let (sa, sb) = (stroke_of(a), stroke_of(b)); + super::select::same_paint(&fill_of(a), &fill_of(b), true) + && super::select::same_paint(&sa.0, &sb.0, true) + && close(sa.1, sb.1) + && match (&a.kind, &b.kind) { + (NodeKind::Group { children: x, .. }, NodeKind::Group { children: y, .. }) => x.iter().zip(y).all(|(p, q)| same_look(p, q)), + _ => true, + } +} + +/// Is `n`, or an object in it, among `ids`? +fn holds(n: &Node, ids: &[NodeId]) -> bool { + let mut found = false; + n.walk(&mut |c| found = found || ids.contains(&c.id)); + found +} + fn close(a: f64, b: f64) -> bool { (a - b).abs() <= 1e-6 * a.abs().max(b.abs()).max(1.0) } @@ -224,12 +293,12 @@ fn matches(s: &Session, o: &Options) -> Result> { let key = d.node(*id).ok_or_else(|| bad(C, "nothing selected"))?; let form = match Form::of(key) { Some(f) if supported(key) => f, - _ => return Err(bad(C, "Global Edit works on paths, compound paths and symbol instances")), + _ => return Err(bad(C, UNSUPPORTED)), }; let symbols = d.symbol_boxes(); let size = |n: &Node| n.geometric_bounds_with(&symbols).map(|b| (b.width(), b.height())); let key_size = size(key); - let (fill, stroke) = (fill_of(key), stroke_of(key)); + let (group, count) = (matches!(key.kind, NodeKind::Group { .. }), key.children().map(Vec::len)); let boards: Option> = match &o.artboards { None => None, Some(r) => Some( @@ -251,30 +320,31 @@ fn matches(s: &Session, o: &Options) -> Result> { let mut out = vec![(*id, Affine::IDENTITY)]; let back = form.frame.inverse(); super::select::walk_selectable(d, st.isolation, |n| { - if n.id == *id { + if n.id == *id || out.len() >= MAX_MEMBERS { return false; } - match &n.kind { - // Look into layers and plain groups (clip groups too: their art, not their clipping path). + // Look into layers and plain groups (clip groups too: their art, not their clipping path). + let look_in = match &n.kind { NodeKind::Layer { .. } => return true, - NodeKind::Group { .. } => { - let x = n.extras(); - return x.graph.is_none() && x.shaper.is_none() && x.dimension.is_none() && x.objects_on_path.is_none() && x.trace.is_none(); - } - _ => {} - } - if let Some(f) = Form::of(n).filter(|f| f.same(&form) && supported(n)) + NodeKind::Group { .. } if !plain_group(n) => return false, + NodeKind::Group { .. } if !group => return true, + NodeKind::Group { .. } => true, + // Only groups are like a group. + _ if group => return false, + _ => false, + }; + // The object count first: it turns most objects down before their form is made. + if n.children().map(Vec::len) == count + && let Some(f) = Form::of(n).filter(|f| f.same(&form) && supported(n)) && (!o.size || matches!((key_size, size(n)), (Some((w, h)), Some((w2, h2))) if close(w, w2) && close(h, h2))) - && (!o.appearance || { - let (f2, s2) = (fill_of(n), stroke_of(n)); - super::select::same_paint(&fill, &f2, true) && super::select::same_paint(&stroke.0, &s2.0, true) && close(stroke.1, s2.1) - }) + && (!o.appearance || same_look(key, n)) && in_scope(n) { out.push((n.id, f.frame * back)); + // A group taken is taken whole, as compound paths, symbol instances and the others are. + return false; } - // Compound paths, symbol instances and the other objects are taken whole. - false + look_in }); Ok(out) } @@ -287,9 +357,12 @@ fn can_toggle(s: &Session) -> std::result::Result<(), String> { let [id] = st.selection.objects.as_slice() else { return Err(if st.selection.is_empty() { "nothing selected" } else { "select one object" }.into()); }; + // A group's contents are checked when Global Edit starts: building its shape here would walk + // every object in it each time the menu or the Properties panel asks. match st.doc.node(*id) { + Some(n) if n.extras().mask.is_none() && plain_group(n) => Ok(()), Some(n) if supported(n) => Ok(()), - _ => Err("Global Edit works on paths, compound paths and symbol instances".into()), + _ => Err(UNSUPPORTED.into()), } } @@ -535,36 +608,62 @@ fn renumber(children: &mut [std::sync::Arc], own: &[NodeId], d: &mut Docum } } +/// Give member `n` what changed from `a` to `b` (the model before and after the edit), with the +/// values of `m` (`b` placed on `n`): in a group that kept its objects (as many, the group still a +/// group of the same kind), each object's change to the shown and unlocked object at its place; +/// otherwise as one object, whose new children keep `n`'s ids in order. +fn give(a: &Node, b: &Node, mut m: Node, n: &mut Node, d: &mut Document) { + let mut changed = Changed::between(a, b); + let len = a.children().map(Vec::len); + let kept = matches!((&a.kind, &b.kind, &n.kind), (NodeKind::Group { clip: x, .. }, NodeKind::Group { clip: y, .. }, NodeKind::Group { .. }) if x == y) + && [b, &m, &*n].iter().all(|x| x.children().map(Vec::len) == len); + if kept { + if let (Some(ac), Some(bc), Some(mc), Some(nc)) = (a.children(), b.children(), m.children(), n.children_mut()) { + for (((p, q), mq), t) in ac.iter().zip(bc).zip(mc).zip(nc.iter_mut()) { + if !std::sync::Arc::ptr_eq(p, q) && t.visible && !t.locked { + give(p, q, (**mq).clone(), std::sync::Arc::make_mut(t), d); + } + } + } + if a.bbox_angle != b.bbox_angle { + n.bbox_angle = m.bbox_angle; + } + changed.outline = false; + } else if changed.outline { + let own: Vec = n.children().map(|c| c.iter().map(|c| c.id).collect()).unwrap_or_default(); + if let Some(ch) = m.children_mut() { + renumber(ch, &own, d); + } + } + changed.apply(m, n); +} + /// After an edit made while Global Edit is on (`before` → `d`): the first member selected before -/// or after the edit that it changed is the model, and every other member that is shown and -/// unlocked gets what changed about it, placed by the members' transforms. → the members it -/// changed so (which [`Session::edit`] checks stay within the canvas, as it checks the selection). +/// or after the edit (or holding an object that is) that it changed is the model, and every other +/// member that is shown and unlocked gets what changed about it, placed by the members' +/// transforms. → the members it changed so (which [`Session::edit`] checks stay within the canvas, +/// as it checks the selection). pub(crate) fn propagate(before: &Document, d: &mut Document, ge: &GlobalEdit, selected: [&[NodeId]; 2]) -> Vec { - let model = ge.members.iter().filter(|(id, _)| selected.iter().any(|s| s.contains(id))).find_map(|(id, xf)| { + let model = ge.members.iter().find_map(|(id, xf)| { let (a, b) = (before.node(*id)?, d.node(*id)?); - if std::ptr::eq(a, b) { + if std::ptr::eq(a, b) || !Changed::between(a, b).any() || !selected.iter().any(|s| holds(a, s) || holds(b, s)) { return None; } - let changed = Changed::between(a, b); - changed.any().then(|| (*id, *xf, changed, b.clone())) + Some((*id, *xf, a, b.clone())) }); - let Some((model_id, model_xf, changed, model)) = model else { return vec![] }; + let Some((model_id, model_xf, was, model)) = model else { return vec![] }; let back = model_xf.inverse(); let mut edited = vec![]; for (id, xf) in &ge.members { if *id == model_id || !editable(d, *id) { continue; } + let Some(mut n) = d.node(*id).cloned() else { continue }; let mut placed = model.clone(); placed.transform(*xf * back, Scaling::default()); - if changed.outline { - let own: Vec = d.node(*id).and_then(Node::children).map(|c| c.iter().map(|c| c.id).collect()).unwrap_or_default(); - if let Some(ch) = placed.children_mut() { - renumber(ch, &own, d); - } - } - if let Some(n) = d.node_mut(*id) { - changed.apply(placed, n); + give(was, &model, placed, &mut n, d); + if let Some(slot) = d.node_mut(*id) { + *slot = n; edited.push(*id); } } @@ -585,9 +684,8 @@ pub fn highlights(s: &Session) -> Outlines { let out: Vec<_> = ge .members .iter() - .filter(|(id, _)| !st.selection.contains(*id)) .filter_map(|(id, _)| { - let n = st.doc.node(*id)?; + let n = st.doc.node(*id).filter(|n| !holds(n, &st.selection.objects))?; let path = match n.stroke_path() { Some(p) => p, None => n.geometric_bounds_with(&symbols)?.to_path(0.1), diff --git a/crates/engine/src/tests/globaledit.rs b/crates/engine/src/tests/globaledit.rs index e6ad7fac9..aeb426ac4 100644 --- a/crates/engine/src/tests/globaledit.rs +++ b/crates/engine/src/tests/globaledit.rs @@ -287,16 +287,18 @@ fn global_edit_needs_one_supported_object() { let e = s.execute("select.globalEdit", &json!({})).unwrap_err().to_string(); assert!(e.contains("no similar objects") && e.contains("rotated"), "{e}"); assert!(s.doc().unwrap().global_edit.is_none()); - // A group of two squares is refused, though another such group exists. + // A group holding type is refused, though another such group exists. let group = |s: &mut Session, x: f64| { - let (p, q) = (rect(s, x, 100.0, 10.0, 10.0), rect(s, x + 20.0, 100.0, 10.0, 10.0)); - sel(s, &[p, q]); - NodeId(s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap()) + let p = rect(s, x, 100.0, 10.0, 10.0); + let t = NodeId(s.execute("text.create", &json!({"x": x, "y": 150, "text": "Hi"})).unwrap()["id"].as_u64().unwrap()); + group_of(s, &[p, t]) }; let (g, _) = (group(&mut s, 600.0), group(&mut s, 700.0)); sel(&mut s, &[g]); + // The menu only checks that it is a group; starting refuses its contents. let e = s.execute("select.globalEdit", &json!({})).unwrap_err(); - assert!(matches!(e, EngineError::Disabled(..)) && e.to_string().contains("paths, compound paths and symbol instances"), "{e}"); + assert!(e.to_string().contains("symbol instances and groups of them"), "{e}"); + assert!(s.doc().unwrap().global_edit.is_none()); } #[test] @@ -443,3 +445,148 @@ fn an_edit_that_takes_a_member_past_the_canvas_is_refused_whole() { s.execute("object.move", &json!({"dx": 3000, "dy": 0})).unwrap(); assert!(close(bounds(&s, big), Rect::new(3.0e6, 0.0, 3.0e6 + 2000.0, 2000.0)), "{:?}", bounds(&s, big)); } + +fn group_of(s: &mut Session, objects: &[NodeId]) -> NodeId { + sel(s, objects); + NodeId(s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap()) +} + +/// A group of a square, a circle and a bar, `k` times the size, at `x` → the group and its objects. +fn card(s: &mut Session, x: f64, k: f64) -> (NodeId, [NodeId; 3]) { + let sq = rect(s, x, 0.0, 40.0 * k, 40.0 * k); + let dot = NodeId( + s.execute("shape.ellipse", &json!({"x": x + 50.0 * k, "y": 0, "width": 40.0 * k, "height": 40.0 * k})).unwrap()["id"].as_u64().unwrap(), + ); + let bar = rect(s, x, 50.0 * k, 90.0 * k, 20.0 * k); + (group_of(s, &[sq, dot, bar]), [sq, dot, bar]) +} + +fn kids(s: &Session, g: NodeId) -> Vec { + node(s, g).children().unwrap().iter().map(|c| c.id).collect() +} + +#[test] +fn start_global_edit_on_a_group_takes_the_groups_like_it() { + let mut s = session(); + let (g, [sq, ..]) = card(&mut s, 0.0, 1.0); + let (copy, _) = card(&mut s, 300.0, 1.0); + let (small, _) = card(&mut s, 500.0, 0.5); + // Two of the three objects; the same three with the bar moved; a lone square like the first. + let (p, q) = (rect(&mut s, 0.0, 300.0, 40.0, 40.0), rect(&mut s, 0.0, 400.0, 90.0, 20.0)); + let pair = group_of(&mut s, &[p, q]); + let (moved, [.., bar]) = card(&mut s, 300.0, 1.0); + sel(&mut s, &[bar]); + s.execute("object.move", &json!({"dx": 5, "dy": 0})).unwrap(); + rect(&mut s, 600.0, 300.0, 40.0, 40.0); + assert_eq!(start(&mut s, g, json!({})), [g, copy, small], "not the pair {pair} nor the group with the bar moved {moved}"); + // A group alone among groups of other contents: nothing to edit with it. + sel(&mut s, &[pair]); + s.execute("select.globalEdit", &json!({})).unwrap(); + let e = s.execute("select.globalEdit", &json!({})).unwrap_err().to_string(); + assert!(e.contains("no similar objects"), "{e}"); + // Size leaves out the smaller group; Appearance looks at each object in the groups. + assert_eq!(start(&mut s, g, json!({"size": true})), [g, copy]); + s.execute("select.globalEdit", &json!({})).unwrap(); + sel(&mut s, &[sq]); + s.execute("paint.setFill", &json!({"color": "#12ab34"})).unwrap(); + sel(&mut s, &[g]); + let e = s.execute("select.globalEdit", &json!({"appearance": true})).unwrap_err().to_string(); + assert!(e.contains("no similar objects"), "the square in the first group has another fill now: {e}"); +} + +#[test] +fn an_edit_inside_one_group_reaches_the_same_object_in_the_others_as_one_undo_step() { + let mut s = session(); + let (g, [sq, dot, bar]) = card(&mut s, 0.0, 1.0); + let (copy, [csq, cdot, cbar]) = card(&mut s, 300.0, 1.0); + let (_, [ssq, sdot, _]) = card(&mut s, 500.0, 0.5); + start(&mut s, g, json!({})); + let before = (*s.doc().unwrap().doc).clone(); + let steps = s.doc().unwrap().history.undo.len(); + let (sq_fill, bar_fill) = (fill(&s, csq), fill(&s, cbar)); + // The circle inside the first group, selected on its own. + sel(&mut s, &[dot]); + assert_eq!(crate::cmd::globaledit::highlights(&s).len(), 2, "the other groups are highlighted, not the one being edited"); + s.execute("paint.setFill", &json!({"color": "#12ab34"})).unwrap(); + assert_eq!((fill(&s, cdot), fill(&s, sdot)), ("#12ab34".to_string(), "#12ab34".to_string())); + assert_eq!((fill(&s, csq), fill(&s, cbar)), (sq_fill, bar_fill), "the other objects keep theirs"); + assert_eq!(s.doc().unwrap().history.undo.len(), steps + 1, "one undo step"); + // The square's height halved from the top: each copy's in proportion. + sel(&mut s, &[sq]); + s.execute("object.setBounds", &json!({"height": 20, "reference": 0})).unwrap(); + assert!(close(bounds(&s, csq), Rect::new(300.0, 0.0, 340.0, 20.0)), "{:?}", bounds(&s, csq)); + assert!(close(bounds(&s, ssq), Rect::new(500.0, 0.0, 520.0, 10.0)), "{:?}", bounds(&s, ssq)); + assert!(placed(&s, bar, cbar, Affine::translate((300.0, 0.0))), "the bar is left alone"); + assert_eq!(kids(&s, copy), [csq, cdot, cbar], "each object keeps its id"); + s.execute("edit.undo", &json!({})).unwrap(); + crate::tests_large_edits::undo_gives_back(&mut s, &before); +} + +#[test] +fn an_object_added_to_the_group_is_added_to_the_others() { + let mut s = session(); + let (g, [sq, ..]) = card(&mut s, 0.0, 1.0); + let (copy, [csq, ..]) = card(&mut s, 300.0, 1.0); + let (small, _) = card(&mut s, 500.0, 0.5); + start(&mut s, g, json!({})); + sel(&mut s, &[sq]); + let added = s + .edit("add", |d, _| { + let id = d.alloc_id(); + let path = vectorcraft_geom::shapes::rectangle(Rect::new(0.0, 80.0, 10.0, 90.0)); + d.insert(Some(g), usize::MAX, Node::path(id, path, vectorcraft_doc::Appearance::default_art())) + .map_err(|e| EngineError::Other(e.to_string())) + }) + .unwrap(); + let (theirs, smalls) = (kids(&s, copy), kids(&s, small)); + assert_eq!((theirs.len(), smalls.len()), (4, 4)); + assert_eq!(theirs.first(), Some(&csq), "the member keeps its own ids in order"); + assert!(placed(&s, added, theirs[3], Affine::translate((300.0, 0.0)))); + assert!(placed(&s, added, smalls[3], Affine::translate((500.0, 0.0)) * Affine::scale(0.5))); +} + +#[test] +fn global_edit_takes_at_most_max_members() { + use crate::cmd::globaledit::MAX_MEMBERS; + let mut s = session(); + let first = s + .edit("many", |d, _| { + let layer = d.default_layer(); + let mut first = None; + for i in 0..MAX_MEMBERS + 5 { + let (x, y) = ((i % 70) as f64 * 10.0, (i / 70 % 50) as f64 * 10.0); + let id = d.alloc_id(); + let path = vectorcraft_geom::shapes::rectangle(Rect::new(x, y, x + 8.0, y + 8.0)); + d.insert(layer, usize::MAX, Node::path(id, path, vectorcraft_doc::Appearance::default_art())) + .map_err(|e| EngineError::Other(e.to_string()))?; + first.get_or_insert(id); + } + first.ok_or_else(|| EngineError::Other("none".into())) + }) + .unwrap(); + assert_eq!(start(&mut s, first, json!({})).len(), MAX_MEMBERS); +} + +#[test] +fn groups_nested_as_deep_as_a_document_allows_are_edited_together() { + let mut s = session(); + let nest = |s: &mut Session, x: f64| { + let inner = rect(s, x, 0.0, 10.0, 10.0); + let side = rect(s, x + 20.0, 0.0, 10.0, 10.0); + let mut top = group_of(s, &[inner, side]); + // Group until the document refuses to go deeper. + for _ in 0..2 * vectorcraft_doc::MAX_DEPTH { + sel(s, &[top]); + match s.execute("object.group", &json!({})) { + Ok(r) => top = NodeId(r["id"].as_u64().unwrap()), + Err(_) => break, + } + } + (top, inner) + }; + let ((g, inner), (other, other_inner)) = (nest(&mut s, 0.0), nest(&mut s, 300.0)); + assert_eq!(start(&mut s, g, json!({})), [g, other]); + sel(&mut s, &[inner]); + s.execute("paint.setFill", &json!({"color": "#12ab34"})).unwrap(); + assert_eq!(fill(&s, other_inner), "#12ab34"); +} diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index f47d5b7b6..a89d71c13 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -5542,6 +5542,7 @@ axis Y Axis Y ос Create New Set Създаване на нов набор Stop Global Edit Край на глобалното редактиране Global Edit Options Опции за глобално редактиране +@msg Global Edit works on paths, compound paths, symbol instances and groups of them Глобалното редактиране работи с пътища, съставни пътища, екземпляри на символи и групи от тях Match Съвпадение On Artboards В работните области All Artboards Всички работни области diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 584d4cf40..e4ab0eac1 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -5571,7 +5571,7 @@ axis Y Axis Y osa Include Objects On Canvas Uključi objekte na platnu Result(s): {count} Rezultati: {count} @msg select one object odaberite jedan objekat -@msg Global Edit works on paths, compound paths and symbol instances Globalno uređivanje radi na putanjama, složenim putanjama i instancama simbola +@msg Global Edit works on paths, compound paths, symbol instances and groups of them Globalno uređivanje radi na putanjama, složenim putanjama, instancama simbola i njihovim grupama @msg no similar objects found (rotated or reflected copies are not matched); change the Global Edit Options and try again nisu pronađeni slični objekti (rotirane ili zrcaljene kopije se ne podudaraju); promijenite opcije globalnog uređivanja i pokušajte ponovo @msg `artboards`: all or a range such as 1, 3-5, not {v} `artboards`: all ili raspon poput 1, 3-5, a ne {v} Add to Symbols Dodaj u simbole diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 02c98c1b5..c4ecf209d 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -5004,7 +5004,7 @@ digits Malayalam Malaiàlam @msg give only one of onto, below and above doneu només un de onto, below i above @msg give the path `id` doneu l’`id` del traçat @msg give the path `id` and the width point `index` doneu el `id` del traçat i l’`index` del punt d’amplada -@msg Global Edit works on paths, compound paths and symbol instances L’edició global funciona amb traçats, traçats compostos i instàncies de símbol +@msg Global Edit works on paths, compound paths, symbol instances and groups of them L’edició global funciona amb traçats, traçats compostos, instàncies de símbol i grups d’aquests objectes @msg gradient “{name}” isn't defined in the file: a plain colour stands in for it el degradat «{name}» no està definit al fitxer: en lloc seu es fa servir un color pla @msg group must be true or false group ha de ser true o false @msg guides are locked les guies estan bloquejades diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 2f21d51d8..dbbae8784 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -5643,7 +5643,7 @@ axis Y Axis osa Y Include Objects On Canvas Zahrnout objekty na plátně Result(s): {count} Výsledky: {count} @msg select one object vyberte jeden objekt -@msg Global Edit works on paths, compound paths and symbol instances Globální úpravy pracují s cestami, složenými cestami a instancemi symbolů +@msg Global Edit works on paths, compound paths, symbol instances and groups of them Globální úpravy pracují s cestami, složenými cestami, instancemi symbolů a jejich skupinami @msg no similar objects found (rotated or reflected copies are not matched); change the Global Edit Options and try again nebyly nalezeny žádné podobné objekty (otočené a zrcadlené kopie se nevyhledávají); změňte možnosti globálních úprav a zkuste to znovu @msg `artboards`: all or a range such as 1, 3-5, not {v} `artboards`: all nebo rozsah, například 1, 3-5, ne {v} Add to Symbols Přidat do symbolů diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index df8c467e1..21f9192ba 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -5557,6 +5557,7 @@ axis Y Axis Y-akse Create New Set Opret nyt sæt Stop Global Edit Stop global redigering Global Edit Options Indstillinger for global redigering +@msg Global Edit works on paths, compound paths, symbol instances and groups of them Global redigering virker på stier, sammensatte stier, symbolforekomster og grupper af dem Match Match On Artboards På tegneflader All Artboards Alle tegneflader diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 95439eea6..503a54585 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5224,7 +5224,7 @@ axis Y Axis Y-Achse Include Objects On Canvas Objekte auf der Leinwand einschließen Result(s): {count} Ergebnis(se): {count} @msg select one object wählen Sie ein Objekt aus -@msg Global Edit works on paths, compound paths and symbol instances Globale Bearbeitung funktioniert mit Pfaden, zusammengesetzten Pfaden und Symbolinstanzen +@msg Global Edit works on paths, compound paths, symbol instances and groups of them Globale Bearbeitung funktioniert mit Pfaden, zusammengesetzten Pfaden, Symbolinstanzen und Gruppen daraus @msg no similar objects found (rotated or reflected copies are not matched); change the Global Edit Options and try again keine ähnlichen Objekte gefunden (gedrehte oder gespiegelte Kopien werden nicht erkannt); ändern Sie die Optionen für globale Bearbeitung und versuchen Sie es erneut @msg `artboards`: all or a range such as 1, 3-5, not {v} `artboards`: all oder ein Bereich wie 1, 3-5, nicht {v} Add to Symbols Zu Symbolen hinzufügen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 91f441ada..887bfe7d4 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -5594,7 +5594,7 @@ axis Y Axis Άξονας Y Include Objects On Canvas Συμπερίληψη αντικειμένων στον καμβά Result(s): {count} Αποτέλεσμα(τα): {count} @msg select one object επιλέξτε ένα αντικείμενο -@msg Global Edit works on paths, compound paths and symbol instances Η καθολική επεξεργασία λειτουργεί σε διαδρομές, σύνθετες διαδρομές και παρουσίες συμβόλων +@msg Global Edit works on paths, compound paths, symbol instances and groups of them Η καθολική επεξεργασία λειτουργεί σε διαδρομές, σύνθετες διαδρομές, παρουσίες συμβόλων και ομάδες τους @msg no similar objects found (rotated or reflected copies are not matched); change the Global Edit Options and try again δεν βρέθηκαν παρόμοια αντικείμενα (τα περιστραμμένα ή κατοπτρισμένα αντίγραφα δεν εντοπίζονται)· αλλάξτε τις επιλογές καθολικής επεξεργασίας και δοκιμάστε ξανά @msg `artboards`: all or a range such as 1, 3-5, not {v} `artboards`: all ή ένα εύρος όπως 1, 3-5, όχι {v} Add to Symbols Προσθήκη στα σύμβολα diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 8fe2d8401..78e3a7ce5 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -5584,7 +5584,7 @@ axis Y Axis Eje Y Include Objects On Canvas Incluir objetos en el lienzo Result(s): {count} Resultado(s): {count} @msg select one object seleccione un objeto -@msg Global Edit works on paths, compound paths and symbol instances La edición global funciona con trazados, trazados compuestos e instancias de símbolos +@msg Global Edit works on paths, compound paths, symbol instances and groups of them La edición global funciona con trazados, trazados compuestos, instancias de símbolos y grupos de ellos @msg no similar objects found (rotated or reflected copies are not matched); change the Global Edit Options and try again no se encontraron objetos similares (las copias giradas o reflejadas no coinciden); cambie las opciones de edición global e inténtelo de nuevo @msg `artboards`: all or a range such as 1, 3-5, not {v} `artboards`: all o un intervalo como 1, 3-5, no {v} Add to Symbols Agregar a símbolos diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 5fbec069a..ca6db1536 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -5673,7 +5673,7 @@ axis Y Axis Y-akseli Include Objects On Canvas Sisällytä kankaalla olevat objektit Result(s): {count} Tulokset: {count} @msg select one object valitse yksi objekti -@msg Global Edit works on paths, compound paths and symbol instances Yleismuokkaus toimii poluille, yhdistelmäpoluille ja symbolien esiintymille +@msg Global Edit works on paths, compound paths, symbol instances and groups of them Yleismuokkaus toimii poluille, yhdistelmäpoluille, symbolien esiintymille ja niiden ryhmille @msg no similar objects found (rotated or reflected copies are not matched); change the Global Edit Options and try again samankaltaisia objekteja ei löytynyt (kierrettyjä tai peilattuja kopioita ei tunnisteta); muuta yleismuokkauksen asetuksia ja yritä uudelleen @msg `artboards`: all or a range such as 1, 3-5, not {v} `artboards`: all tai alue, kuten 1, 3-5, ei {v} Add to Symbols Lisää symboleihin diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index d6ec59b6c..0102a7ad7 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -5640,7 +5640,7 @@ axis Y Axis Axe Y Include Objects On Canvas Inclure les objets sur la toile Result(s): {count} Résultat(s) : {count} @msg select one object sélectionnez un seul objet -@msg Global Edit works on paths, compound paths and symbol instances La modification globale s’applique aux tracés, aux tracés transparents et aux occurrences de symboles +@msg Global Edit works on paths, compound paths, symbol instances and groups of them La modification globale s’applique aux tracés, aux tracés transparents, aux occurrences de symboles et aux groupes de ces objets @msg no similar objects found (rotated or reflected copies are not matched); change the Global Edit Options and try again aucun objet similaire trouvé (les copies pivotées ou symétriques ne sont pas reconnues) ; modifiez les options de modification globale et réessayez @msg `artboards`: all or a range such as 1, 3-5, not {v} `artboards` : all ou une plage telle que 1, 3-5, et non {v} Add to Symbols Ajouter aux symboles diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index fe56efa82..ce55ee09d 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -5628,7 +5628,7 @@ axis Y Axis Y tengely Include Objects On Canvas Vásznon lévő objektumok is Result(s): {count} Eredmény(ek): {count} @msg select one object jelöljön ki egy objektumot -@msg Global Edit works on paths, compound paths and symbol instances A globális szerkesztés útvonalakon, összetett útvonalakon és szimbólumpéldányokon működik +@msg Global Edit works on paths, compound paths, symbol instances and groups of them A globális szerkesztés útvonalakon, összetett útvonalakon, szimbólumpéldányokon és ezek csoportjain működik @msg no similar objects found (rotated or reflected copies are not matched); change the Global Edit Options and try again nem található hasonló objektum (az elforgatott vagy tükrözött másolatok nem számítanak egyezésnek); módosítsa a globális szerkesztés beállításait, és próbálja újra @msg `artboards`: all or a range such as 1, 3-5, not {v} `artboards`: all vagy egy tartomány, például 1, 3-5, nem {v} Add to Symbols Hozzáadás a szimbólumokhoz diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index a37491b99..f111cc213 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -5557,6 +5557,7 @@ axis Y Axis Sumbu Y Create New Set Buat set baru Stop Global Edit Hentikan edit global Global Edit Options Opsi edit global +@msg Global Edit works on paths, compound paths, symbol instances and groups of them Edit global berfungsi pada jalur, jalur gabungan, instans simbol, dan grupnya Match Cocokkan On Artboards Di artboard All Artboards Semua artboard diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index dbf755d31..d01f8ebad 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5258,7 +5258,7 @@ axis Y Axis Asse Y Include Objects On Canvas Includi oggetti sulla tela Result(s): {count} Risultati: {count} @msg select one object selezionate un oggetto -@msg Global Edit works on paths, compound paths and symbol instances La modifica globale funziona su tracciati, tracciati composti e istanze di simboli +@msg Global Edit works on paths, compound paths, symbol instances and groups of them La modifica globale funziona su tracciati, tracciati composti, istanze di simboli e gruppi di essi @msg no similar objects found (rotated or reflected copies are not matched); change the Global Edit Options and try again nessun oggetto simile trovato (le copie ruotate o riflesse non vengono riconosciute); cambiate le opzioni di modifica globale e riprovate @msg `artboards`: all or a range such as 1, 3-5, not {v} `artboards`: all o un intervallo come 1, 3-5, non {v} Add to Symbols Aggiungi ai simboli diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 54e53749b..f1738d56b 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5325,7 +5325,7 @@ axis Y Axis Y 軸 Include Objects On Canvas カンバス上のオブジェクトを含める Result(s): {count} 結果: {count} @msg select one object オブジェクトを 1 つ選択してください -@msg Global Edit works on paths, compound paths and symbol instances グローバル編集はパス、複合パス、シンボルインスタンスに使用できます +@msg Global Edit works on paths, compound paths, symbol instances and groups of them グローバル編集はパス、複合パス、シンボルインスタンスとそれらのグループに使用できます @msg no similar objects found (rotated or reflected copies are not matched); change the Global Edit Options and try again 類似するオブジェクトが見つかりません(回転または反転したコピーは一致しません)。グローバル編集オプションを変更してもう一度試してください @msg `artboards`: all or a range such as 1, 3-5, not {v} `artboards`: all または 1, 3-5 のような範囲を指定してください ({v} ではなく) Add to Symbols シンボルに追加 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index a5f844aba..5ea64087e 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -5558,7 +5558,7 @@ axis Y Axis Y ღერძი Include Objects On Canvas ტილოზე არსებული ობიექტების გათვალისწინება Result(s): {count} შედეგი: {count} @msg select one object მონიშნეთ ერთი ობიექტი -@msg Global Edit works on paths, compound paths and symbol instances გლობალური რედაქტირება მუშაობს ტრაექტორიებზე, შედგენილ ტრაექტორიებსა და სიმბოლოს ეგზემპლარებზე +@msg Global Edit works on paths, compound paths, symbol instances and groups of them გლობალური რედაქტირება მუშაობს ტრაექტორიებზე, შედგენილ ტრაექტორიებზე, სიმბოლოს ეგზემპლარებსა და მათ ჯგუფებზე @msg no similar objects found (rotated or reflected copies are not matched); change the Global Edit Options and try again მსგავსი ობიექტები ვერ მოიძებნა (შემობრუნებული ან არეკლილი ასლები არ ემთხვევა); შეცვალეთ გლობალური რედაქტირების პარამეტრები და სცადეთ ხელახლა @msg `artboards`: all or a range such as 1, 3-5, not {v} `artboards`: all ან დიაპაზონი, მაგალითად 1, 3-5, და არა {v} Add to Symbols სიმბოლოებში დამატება diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 5dd77eaba..880456dee 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5221,7 +5221,7 @@ axis Y Axis Y축 Include Objects On Canvas 캔버스의 개체 포함 Result(s): {count} 결과: {count} @msg select one object 개체 하나를 선택하십시오 -@msg Global Edit works on paths, compound paths and symbol instances 전체 편집은 패스, 컴파운드 패스, 심볼 인스턴스에서 작동합니다 +@msg Global Edit works on paths, compound paths, symbol instances and groups of them 전체 편집은 패스, 컴파운드 패스, 심볼 인스턴스 및 이들의 그룹에서 작동합니다 @msg no similar objects found (rotated or reflected copies are not matched); change the Global Edit Options and try again 유사한 개체를 찾을 수 없습니다(회전하거나 반사한 복사본은 일치하지 않습니다). 전체 편집 옵션을 변경한 후 다시 시도하십시오 @msg `artboards`: all or a range such as 1, 3-5, not {v} `artboards`: all 또는 1, 3-5 같은 범위여야 합니다({v}이(가) 아님) Add to Symbols 심볼에 추가 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 4c60e7358..0d1fe8b2e 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -5601,6 +5601,7 @@ axis Y Axis Y-as Create New Set Nieuwe set maken Stop Global Edit Globale bewerking stoppen Global Edit Options Opties voor globale bewerking +@msg Global Edit works on paths, compound paths, symbol instances and groups of them Globale bewerking werkt op paden, samengestelde paden, symboolinstanties en groepen daarvan Match Overeenkomst On Artboards Op tekengebieden All Artboards Alle tekengebieden diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 593f7d2ab..5fb2f16f1 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -5496,7 +5496,7 @@ axis Y Axis Oś Y Include Objects On Canvas Uwzględnij obiekty na płótnie Result(s): {count} Wyniki: {count} @msg select one object zaznacz jeden obiekt -@msg Global Edit works on paths, compound paths and symbol instances Edycja globalna działa na ścieżkach, ścieżkach złożonych i wystąpieniach symboli +@msg Global Edit works on paths, compound paths, symbol instances and groups of them Edycja globalna działa na ścieżkach, ścieżkach złożonych, wystąpieniach symboli i ich grupach @msg no similar objects found (rotated or reflected copies are not matched); change the Global Edit Options and try again nie znaleziono podobnych obiektów (obrócone i odbite kopie nie są dopasowywane); zmień opcje edycji globalnej i spróbuj ponownie @msg `artboards`: all or a range such as 1, 3-5, not {v} `artboards`: all lub zakres, np. 1, 3-5, a nie {v} Add to Symbols Dodaj do symboli diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index e6e4599be..8cefbc8d3 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -5559,7 +5559,7 @@ axis Y Axis Eixo Y Include Objects On Canvas Incluir Objetos na Tela Result(s): {count} Resultado(s): {count} @msg select one object selecione um objeto -@msg Global Edit works on paths, compound paths and symbol instances A edição global funciona com demarcadores, demarcadores compostos e instâncias de símbolos +@msg Global Edit works on paths, compound paths, symbol instances and groups of them A edição global funciona com demarcadores, demarcadores compostos, instâncias de símbolos e grupos deles @msg no similar objects found (rotated or reflected copies are not matched); change the Global Edit Options and try again nenhum objeto semelhante encontrado (cópias giradas ou refletidas não são correspondidas); altere as opções de edição global e tente novamente @msg `artboards`: all or a range such as 1, 3-5, not {v} `artboards`: all ou um intervalo como 1, 3-5, não {v} Add to Symbols Adicionar aos Símbolos diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index fa9e68b5d..900f2361f 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -5254,7 +5254,7 @@ axis Y Axis Ось Y Include Objects On Canvas Включать объекты на холсте Result(s): {count} Результаты: {count} @msg select one object выберите один объект -@msg Global Edit works on paths, compound paths and symbol instances Глобальное редактирование работает с контурами, составными контурами и экземплярами символов +@msg Global Edit works on paths, compound paths, symbol instances and groups of them Глобальное редактирование работает с контурами, составными контурами, экземплярами символов и их группами @msg no similar objects found (rotated or reflected copies are not matched); change the Global Edit Options and try again похожие объекты не найдены (повёрнутые или отражённые копии не учитываются); измените параметры глобального редактирования и повторите попытку @msg `artboards`: all or a range such as 1, 3-5, not {v} `artboards`: all или диапазон, например 1, 3-5, а не {v} Add to Symbols Добавить в символы diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 23aa4028b..ef1a8500a 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -5666,7 +5666,7 @@ axis Y Axis Y-axel Include Objects On Canvas Ta med objekt på arbetsytan Result(s): {count} Resultat: {count} @msg select one object markera ett objekt -@msg Global Edit works on paths, compound paths and symbol instances Global redigering fungerar på banor, sammansatta banor och symbolinstanser +@msg Global Edit works on paths, compound paths, symbol instances and groups of them Global redigering fungerar på banor, sammansatta banor, symbolinstanser och grupper av dem @msg no similar objects found (rotated or reflected copies are not matched); change the Global Edit Options and try again inga liknande objekt hittades (roterade eller speglade kopior matchas inte); ändra alternativen för global redigering och försök igen @msg `artboards`: all or a range such as 1, 3-5, not {v} `artboards`: all eller ett intervall som 1, 3-5, inte {v} Add to Symbols Lägg till i symboler diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index c9a3175cb..c4980e5e5 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -5600,7 +5600,7 @@ axis Y Axis Y ekseni Include Objects On Canvas Tuvaldeki Nesneleri Dahil Et Result(s): {count} Sonuç(lar): {count} @msg select one object bir nesne seçin -@msg Global Edit works on paths, compound paths and symbol instances Genel düzenleme yollar, bileşik yollar ve sembol örnekleri üzerinde çalışır +@msg Global Edit works on paths, compound paths, symbol instances and groups of them Genel düzenleme yollar, bileşik yollar, sembol örnekleri ve bunların grupları üzerinde çalışır @msg no similar objects found (rotated or reflected copies are not matched); change the Global Edit Options and try again benzer nesne bulunamadı (döndürülmüş veya yansıtılmış kopyalar eşleşmez); Genel Düzenleme Seçeneklerini değiştirip yeniden deneyin @msg `artboards`: all or a range such as 1, 3-5, not {v} `artboards`: all ya da 1, 3-5 gibi bir aralık olmalı, {v} değil Add to Symbols Sembollere Ekle diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 39303f0a2..daee9a6d5 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5238,7 +5238,7 @@ axis Y Axis Вісь Y Include Objects On Canvas Включати об’єкти на полотні Result(s): {count} Результати: {count} @msg select one object виберіть один об’єкт -@msg Global Edit works on paths, compound paths and symbol instances Глобальне редагування працює з контурами, складеними контурами та екземплярами символів +@msg Global Edit works on paths, compound paths, symbol instances and groups of them Глобальне редагування працює з контурами, складеними контурами, екземплярами символів та їхніми групами @msg no similar objects found (rotated or reflected copies are not matched); change the Global Edit Options and try again схожих об’єктів не знайдено (повернуті чи віддзеркалені копії не враховуються); змініть параметри глобального редагування та спробуйте ще раз @msg `artboards`: all or a range such as 1, 3-5, not {v} `artboards`: all або діапазон, наприклад 1, 3-5, а не {v} Add to Symbols Додати до символів diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index bc4d856aa..ab2dd4721 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5241,7 +5241,7 @@ axis Y Axis Trục Y Include Objects On Canvas Bao gồm đối tượng trên vùng vẽ Result(s): {count} Kết quả: {count} @msg select one object hãy chọn một đối tượng -@msg Global Edit works on paths, compound paths and symbol instances Chỉnh sửa toàn cục hoạt động trên đường dẫn, đường dẫn phức hợp và phiên bản biểu tượng +@msg Global Edit works on paths, compound paths, symbol instances and groups of them Chỉnh sửa toàn cục hoạt động trên đường dẫn, đường dẫn phức hợp, phiên bản biểu tượng và nhóm của chúng @msg no similar objects found (rotated or reflected copies are not matched); change the Global Edit Options and try again không tìm thấy đối tượng tương tự (các bản sao đã xoay hoặc lật không được tính); hãy thay đổi tùy chọn chỉnh sửa toàn cục rồi thử lại @msg `artboards`: all or a range such as 1, 3-5, not {v} `artboards`: all hoặc một phạm vi như 1, 3-5, không phải {v} Add to Symbols Thêm vào ký hiệu diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 00839e4ef..58efd43a3 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -5482,7 +5482,7 @@ axis Y Axis Y 轴 Include Objects On Canvas 包括画布上的对象 Result(s): {count} 结果:{count} @msg select one object 请选择一个对象 -@msg Global Edit works on paths, compound paths and symbol instances 全局编辑适用于路径、复合路径和符号实例 +@msg Global Edit works on paths, compound paths, symbol instances and groups of them 全局编辑适用于路径、复合路径、符号实例及其编组 @msg no similar objects found (rotated or reflected copies are not matched); change the Global Edit Options and try again 未找到相似对象(旋转或镜像的副本不会匹配);请更改全局编辑选项后重试 @msg `artboards`: all or a range such as 1, 3-5, not {v} `artboards`:应为 all 或 1, 3-5 这样的范围,而不是 {v} Add to Symbols 添加到符号 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 1385a52c7..1067a2ec2 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5278,7 +5278,7 @@ axis Y Axis Y 軸 Include Objects On Canvas 包含畫布上的物件 Result(s): {count} 結果:{count} @msg select one object 請選取一個物件 -@msg Global Edit works on paths, compound paths and symbol instances 整體編輯適用於路徑、複合路徑和符號實體 +@msg Global Edit works on paths, compound paths, symbol instances and groups of them 整體編輯適用於路徑、複合路徑、符號實體及其群組 @msg no similar objects found (rotated or reflected copies are not matched); change the Global Edit Options and try again 找不到相似的物件(旋轉或鏡像的副本不會相符);請變更整體編輯選項後再試一次 @msg `artboards`: all or a range such as 1, 3-5, not {v} `artboards`:應為 all 或 1, 3-5 這類範圍,而不是 {v} Add to Symbols 新增至符號 diff --git a/docs/gaps.md b/docs/gaps.md index 072ee477a..9b93120be 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Gaps -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (G4: PDF import finds installed fonts whose PDF names hold hyphens or a composite font's CMap; estimate unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (G5: Start Global Edit on groups, #4939; estimate unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -517,7 +517,9 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall Color eyedropper for scatter, art and pattern brushes (Hue Shift colorization; samples canvas art); Select › Start Global Edit (#3521) on paths, compound paths and symbol instances: the objects of the same shape at any place and size, narrowed by Appearance, Size and the artboards to search, take an edit of any one of - them in proportion, as one undo step ([public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-similar-objects.html)). + them in proportion, as one undo step ([public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-similar-objects.html)); + on a group (#4939), the groups like it (the same objects at the same places, moved or scaled) take an edit + made to an object inside it at the object in the same place ([public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-groups-with-similar-objects.html)). Color eyedropper for scatter, art and pattern brushes (Hue Shift colorization; samples canvas art); symbol libraries ([public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-or-import-symbol-libraries.html)): Window › Symbol Libraries and the Symbols panel's Symbol Libraries Menu open each library in a library panel of its own (one original library generated in code, @@ -537,9 +539,8 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall as a `.vectorcraft` library into the user library folder (listed under User Defined) or a file (`brush.libraries`, `brush.addFromLibrary`, `brush.saveLibrary`, `brush.loadLibrary`). - **Missing:** the Art brush's Overlap; more built-in brush, symbol and graphic-style libraries (original, - generated in code, never Adobe's); dynamic symbols; Global Edit of similar groups (edits to an object inside a - group reaching the matching objects of similar groups, [public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-groups-with-similar-objects.html)), - rotated copies as matches, Shift-click to leave a match out and highlighting matches before the start; + generated in code, never Adobe's); dynamic symbols; Global Edit of groups holding type or + images, rotated copies as matches, Shift-click to leave a match out and highlighting matches before the start; drawing the 9-slice guides on the canvas in symbol edit mode and scaling by them. - **Estimate:** 15–26 h. @@ -676,6 +677,7 @@ trap, and the defaults (0.25 pt, 100%, 40%) are VectorCraft's own, the pages giv | Date | Change | Summary | |---|---|---| +| 2026-11-01 | minor | G5: Select › Start Global Edit on a group takes the groups like it (the same objects at the same places, moved or scaled; Appearance, Size and the artboard options apply) and gives an edit made to an object inside one to the object at its place in each of the others, in proportion, as one undo step (#4939); rotated copies and Shift-click exclusion remain. Estimate unchanged. | | 2026-10-31 | minor | G4: PDF import finds an installed font whose PDF name holds hyphens (`Noto-Sans-Bold` is Noto Sans, Bold: the longest part before a hyphen that names an installed family) and a composite font by its CIDFont's name rather than its base name, which ends in the CMap's (`Name-Identity-H`, [ISO 32000-1 §9.7.6.1](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf)); a missing font is split only before a style part (`Helvetica-BoldOblique`), so it is reported as `MS PGothic`, not `MS`. Estimate unchanged. | | 2026-10-31 | minor | G13: Direct Selection snaps the end of an existing straight line, dragged by its end anchor, square to straight paths and grazing curves (Smart Guides › Geometric Guides: Snap to Perpendicular and Snap to Tangent), the case the public Snap to Perpendicular and Snap to Tangent pages add to drawing with the Line Segment and Pen tools (#2530). Scores and estimates unchanged. | | 2026-10-31 | minor | G8: a gradient mesh read from a file is budgeted: at most about a million quads per tessellation, and a mesh with more patches than that is malformed (#3592); estimate unchanged. | diff --git a/docs/mcp.md b/docs/mcp.md index 4c893cff8..c6bbae8f6 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1450,22 +1450,28 @@ nearest to the pointer. Select › Start Global Edit is `select.globalEdit {appearance?, size?, artboards?, includeCanvas?}`. With one path, compound path or symbol instance selected, it finds the objects of the same shape (moved or scaled copies; not rotated -ones), among those `select.all` would take and the objects in their groups, and returns -`{active: true, count, ids}`, the selected object first. `appearance` also asks for the same fill and stroke, `size` -for the same width and height; `artboards` is `"all"` or a range such as `"1, 3-5"`, and `includeCanvas: false` -leaves out the objects on no artboard. Missing params come from `select.globalEditOptions` (the Global Edit Options, -kept for the session; it returns them with `count`, the objects a start would take now). The selection stays the -one object. Until the next `select.globalEdit` (Stop Global Edit, `{active: false}`), what an edit of a selected match -changed is given to the other matches, in the same undo step, and nothing else. A changed outline or anchors go to -each match in proportion to its size. A changed opacity or blending, and each changed setting of a fill or stroke -(paint, weight, opacity, blending, effects…), are copied as they are to the match's fill or stroke of the same kind -and rank; its other fills and strokes, and their unchanged settings, stay its own (a stroke weight change leaves the -fills). A stroke weight set on the selected match is given as it is, and resizing the outline in proportion never -scales a match's stroke weight. Hidden or locked matches are left as they are. A fill or stroke added or removed is -added or removed on the matches whose fills and strokes lay in the same order; the others, and any other change to the -list, take the whole list. Whenever the document is put back (Undo, Redo, a cancelled drag or scrub), the matches it no -longer has leave the Global Edit, which ends when fewer than two are left. An edit that would take any match past the -canvas fails with the coordinates-out-of-range error and changes nothing. Nothing of it is saved. +ones), among those `select.all` would take and the objects in their groups, and returns `{active: true, count, ids}`, +the selected object first (at most 10,000). With a group of such objects selected (nested groups and clipping paths +included; not type or images), it finds the groups like it: as many objects, in the same order, each of the same +shape, lying the same way relative to each other (the group moved or scaled as a whole). `appearance` also asks for +the same fill and stroke (for groups, of each object in them), `size` for the same width and height; `artboards` is +`"all"` or a range such as `"1, 3-5"`, and `includeCanvas: false` leaves out the objects on no artboard. Missing +params come from `select.globalEditOptions` (the Global Edit Options, kept for the session; it returns them with +`count`, the objects a start would take now). The selection stays the one object. Until the next `select.globalEdit` +(Stop Global Edit, `{active: false}`), what an edit of a selected match changed is given to the other matches, in the +same undo step, and nothing else. A changed outline or anchors go to each match in proportion to its size. A changed +opacity or blending, and each changed setting of a fill or stroke (paint, weight, opacity, blending, effects…), are +copied as they are to the match's fill or stroke of the same kind and rank; its other fills and strokes, and their +unchanged settings, stay its own (a stroke weight change leaves the fills). A stroke weight set on the selected match +is given as it is, and resizing the outline in proportion never scales a match's stroke weight. Hidden or locked +matches are left as they are. A fill or stroke added or removed is added or removed on the matches whose fills and +strokes lay in the same order; the others, and any other change to the list, take the whole list. In groups, an edit +made to an object inside one of them (selected on its own) is given by the same rules to the object at its place in +each of the others, whose hidden or locked objects are left as they are; when the group gains or loses objects, each +other group takes its objects at that level, keeping its own ids in order. Whenever the document is put back (Undo, +Redo, a cancelled drag or scrub), the matches it no longer has leave the Global Edit, which ends when fewer than two +are left. An edit that would take any match past the canvas fails with the coordinates-out-of-range error and changes +nothing. Nothing of it is saved. ## Saved selections diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 8e145f25a..51d9dd20d 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Document Tools add-on: New Symbols from Layers, #4927; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Start Global Edit on groups, #4939; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) ## Feature submission evidence @@ -120,7 +120,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de | Area | Weight | Done | Main gaps | One agent (h) | Kind | |---|---:|---:|---|---:|---| -| Selection, transform & align tools | 6 | 85% | Direct Selection segment click/Delete corrected (#1278); open-path anchor Delete keeps gaps; Select › Start Global Edit finds the paths, compound paths and symbol instances of the selected object's shape (Appearance, Size and artboard options) and gives an edit of one to all of them in proportion, as one undo step (#3521); the Align panel menu's Align to Glyph Bounds › Point Text and Area Text measure type by its glyphs' ink for Align and Distribute (#4231); remaining: Free Transform/Scale/Reflect along a rotated box, Global Edit of similar groups and excluding a match with Shift-click, transform nuances (no-op anchor Align history regression-tested, #1180 follow-up) | 4–6 | estimated; Global Edit regression-tested (`crates/engine/src/tests/globaledit.rs`; Quick Action, menu label and Escape in `panels::properties::tests::start_global_edit_quick_action_and_escape`) | +| Selection, transform & align tools | 6 | 85% | Direct Selection segment click/Delete corrected (#1278); open-path anchor Delete keeps gaps; Select › Start Global Edit finds the paths, compound paths and symbol instances of the selected object's shape (Appearance, Size and artboard options) and gives an edit of one to all of them in proportion, as one undo step (#3521), and on a group the groups like it, an edit inside one reaching the same object in the others (#4939); the Align panel menu's Align to Glyph Bounds › Point Text and Area Text measure type by its glyphs' ink for Align and Distribute (#4231); remaining: Free Transform/Scale/Reflect along a rotated box, Global Edit of rotated copies and excluding a match with Shift-click, transform nuances (no-op anchor Align history regression-tested, #1180 follow-up) | 4–6 | estimated; Global Edit regression-tested (`crates/engine/src/tests/globaledit.rs`; Quick Action, menu label and Escape in `panels::properties::tests::start_global_edit_quick_action_and_escape`) | | Drawing tools (Pen, Curvature, Pencil, shapes, Shaper) | 7 | 85% | Shaper construction widget, Pencil/Pen nuances, Blob Brush options, Touch Type, Dimension tool; see [ui-parity.md](ui-parity.md) | 8–13 | estimated; Curvature point display regression-tested (#1236, in part); Polygon/Star exact-size count persistence regression-covered; Live Corners' Relative rounding done (#3091), score unchanged | | Path operations, Pathfinder, Shape Builder, Live Paint | 5 | 80% | Shape Builder on the reporter's file (#937: art past 4,096 segments is cut by lines again; unconfirmed on the file itself); gap detection shares Live Paint’s finder, with a 72 pt custom cap and the existing options; Live Paint Gap Options | 4–8 | estimated; Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog (lowered from 86%: open issues); compound attributes regression-tested (#1197); single-result source names survive SVG export/import (#1293), with [SVG ID semantics](https://www.w3.org/TR/SVG2/struct.html#IDAttribute) and original-shape tests; Simplify component selection and preview regression-tested (#1272); Simplify of a Direct Selection part of a path regression-tested (`tests/simplify_part.rs`); Divide open-cutting-path geometry and appearance regression-tested (#1307); the Shape Builder's edge-source grid is bounded in memory and files a segment once per cell (`planar.rs` test `rect_index_holds_a_bounded_number_of_references`); live compound shapes (Make, Alt-click, member Shape Modes, Release, Expand, saved and exported) regression-tested (#4522) | | Colour, swatches, gradients, patterns, mesh, recolor | 7 | 92% | 30.0's gradient additions are done (dither, perceptual blending, presets); freeform and mixed spot/process gradients in SVG/PDF | 4–7 | estimated; Recolor model selection is available through the engine and MCP only (#2520); the dialog retains its existing controls (lowered from 96%: target moved); unpainted zero-width stroke restoration fixed (#1253), score and hours unchanged; Eyedropper source reads regression-tested (#1251); screen-bounded Recolor Edit chip list (#3416), score and hours unchanged | @@ -129,7 +129,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de | 3D and Materials (and Turntable) | 4 | ~25% | live Revolve with automatic smooth vector shading and default-on continuous circular caps, Extrude & Bevel (twist, taper, caps, five bevel shapes), Inflate and Rotate with Position presets (colored Inflate uses smooth membrane lighting; white-light Inflate retains its existing shading; Extrude & Bevel and Rotate retain planar shading; one to ten colored lights, shared neutral ambient, painter visibility); materials, light presets, shadows, ray tracing, 3D Classic, Turntable views, 3D export | 30–55 | estimated (Extrude & Bevel, Inflate and Rotate added 2026-10-12) | | Type core | 9 | 80% | Auto-leading style resets are implemented; Hyphenation… and Justification… options are done (#2310, #2755), and a paragraph's Auto Leading also sets the Character panel's Leading, the Alt+↑/↓ leading nudge and Fill with Placeholder Text; remaining: Optical Margin Alignment, Middle Eastern features beyond bidi layout, optional group font-size preservation; transformed character controls fixed (#1034), see [type-parity.md](type-parity.md) | 14–19 | estimated; full-name font matches regression-tested (#1258); mixed paragraph alignment indication implemented; headless regressions, full integration CI and visual review pass; localized legacy family-name fallback with loaded/catalog regressions (#2208) | | Type advanced (CJK, spelling, Touch Type, Retype) | 4 | 50% | CJK vertical composition (ruby, mojikumi), variable font axes, tab leaders, spell check, Touch Type, Retype | 20–28 | estimated; upright fallback for fullwidth `!` / `?` regression-tested; score and hours unchanged | -| Symbols, blends, envelopes, Repeat, perspective | 5 | 87% | 30.7's Blend panel with ease in/out, dynamic symbols, more built-in symbol libraries, Global Edit of similar groups, envelope Anti-Alias/Preserve Shape; Symbol Options › Registration, Export Type (Graphic / Movie Clip) and 9-slice guide settings (stored, not drawn) are done | 5–9 | estimated; Blend panel Type and Ramp control step easing with a separate color Shift (#2539); Blend Options retains spacing and orientation; polynomial curves are original approximations; interactive selected mirror axis implemented; bounding-box handles take priority over the axis line (focused regression and native checks passed; full CI blocked by an unrelated text test); #2544 spine Anchor Point conversion and picking regression-tested; scores and hours unchanged | +| Symbols, blends, envelopes, Repeat, perspective | 5 | 87% | 30.7's Blend panel with ease in/out, dynamic symbols, more built-in symbol libraries, envelope Anti-Alias/Preserve Shape; Symbol Options › Registration, Export Type (Graphic / Movie Clip) and 9-slice guide settings (stored, not drawn) are done | 5–9 | estimated; Blend panel Type and Ramp control step easing with a separate color Shift (#2539); Blend Options retains spacing and orientation; polynomial curves are original approximations; interactive selected mirror axis implemented; bounding-box handles take priority over the axis line (focused regression and native checks passed; full CI blocked by an unrelated text test); #2544 spine Anchor Point conversion and picking regression-tested; scores and hours unchanged | | Type advanced (CJK, spelling, Touch Type, Retype) | 4 | 50% | CJK vertical composition (ruby, mojikumi), tab leaders, spell check, Touch Type, Retype | 20–28 | estimated; upright fallback for fullwidth `!` / `?` regression-tested; score and hours unchanged | | Symbols, blends, envelopes, Repeat, perspective | 5 | 87% | 30.7's Blend panel with ease in/out, dynamic symbols, symbol libraries, Start Global Edit, envelope Anti-Alias/Preserve Shape; Symbol Options › Registration, Export Type (Graphic / Movie Clip) and 9-slice guide settings (stored, not drawn) are done | 5–9 | estimated; Blend panel Type and Ramp control step easing with a separate color Shift (#2539); Blend Options retains spacing and orientation; polynomial curves are original approximations; interactive selected mirror axis implemented; bounding-box handles take priority over the axis line (focused regression and native checks passed; full CI blocked by an unrelated text test); #2544 spine Anchor Point conversion and picking regression-tested; scores and hours unchanged | | Image Trace, graphs, image tools | 3 | 75% | graph Column designs, Image Trace Create Strokes depth (Create Object Mosaic is done; junction-returning piece survival has source-generated regressions in `image-trace-quality.md`; simpler contour walk, identical output; an edge's sub-pixel shift is the same from either side; notches read only their own piece; collapsed loops keep few segments, so Paths is monotone on the code-drawn sweep) | 5–8 | estimated (raised from 72%); quick presets reveal adjustment controls, Properties reopens them; Live trace result bounds and selection highlights exclude the retained bitmap except in source-image views (scores/hours unchanged) | @@ -140,7 +140,7 @@ dominate; 3D, graphs and automation are specialist). They sum to 105. Feature de | Type core | 9 | 80% | Auto-leading style resets are implemented; remaining: hyphenation and justification options, Middle Eastern features beyond bidi layout, optional group font-size preservation; transformed character controls fixed (#1034), see [type-parity.md](type-parity.md) | 14–19 | estimated; recent-font groups in font pickers (#3515); full-name font matches regression-tested (#1258); mixed paragraph alignment indication implemented; headless regressions, full integration CI and visual review pass; localized legacy family-name fallback with loaded/catalog regressions (#2208); Fractional Widths per type object regression-tested; colour emoji fonts with bitmap glyphs (`sbix`, `CBDT`) draw and export; scores and hours unchanged | | Type advanced (CJK, spelling, Touch Type, Retype) | 4 | 50% | CJK vertical composition (mojikumi), tab leaders, spell check, Touch Type, Retype | 20–28 | estimated; No Break keeps marked characters on one line; upright fallback for fullwidth `!` / `?` regression-tested; score and hours unchanged | | Raster effects (Effect Gallery) | 4 | 52% | the Effect Gallery's filter thumbnails and in-dialog preview area ([Filter Gallery overview](https://helpx.adobe.com/photoshop/desktop/effects-filters/get-started-with-filters/filter-gallery.html)) | 1–2 | estimated (raised from 49%: the Effect Gallery shipped, #2883, six categories, per-filter settings, ordered stack, live canvas preview, one undo step, `dialogs::effect_gallery::tests::stack_ok_is_one_undo_step`, `stack_preview_is_one_interaction`); measured count 57/57 (Artistic complete with Colored Pencil, Fresco, Paint Daubs, Palette Knife, Rough Pastels and Underpainting; Sketch complete with Torn Edges and Conté Crayon, `pixel::tests_sketch::conte_crayon_*`); texture redraw distances optimized (#1257), pixels verified; depth estimated | -| Symbols, blends, envelopes, Repeat, perspective | 5 | 87% | 30.7's Blend panel with ease in/out, dynamic symbols, more built-in symbol libraries, Global Edit of similar groups, envelope Anti-Alias/Preserve Shape; Symbol Options › Registration and Export Type (Graphic / Movie Clip) are done | 5–9 | estimated | +| Symbols, blends, envelopes, Repeat, perspective | 5 | 87% | 30.7's Blend panel with ease in/out, dynamic symbols, more built-in symbol libraries, envelope Anti-Alias/Preserve Shape; Symbol Options › Registration and Export Type (Graphic / Movie Clip) are done | 5–9 | estimated | | Image Trace, graphs, image tools | 3 | 75% | graph Column designs, Image Trace Create Strokes depth (Create Object Mosaic is done; junction-returning piece survival has source-generated regressions in `image-trace-quality.md`; large images trace with a warning; Auto Color keeps rare distinct colours) | 5–8 | estimated (raised from 72%); quick presets reveal adjustment controls, Properties reopens them | | Layers, artboards, document setup | 5 | 78% | Selected-artboard deletion is consistent across panel actions (all-selected retains artboard 1); history prunes removed indices while preserving valid multiselection; remaining: 30.0's artboard locking and colours and managing artboards from the canvas, Layers panel row context menus and Collect for Export (click-to-fit objects is done, #1227) | 5–8 | estimated (lowered from 82%: target moved); Artboard deletion keys keep artwork and the first of an all-selected set; Artboard Options' Show Center Mark and Show Cross Hairs draw on the canvas only (exports and print leave them out), regression-tested; Artboard Options' Show Video Safe Areas and Video Ruler Pixel Aspect Ratio (per artboard, native format) are done ([video display options](#evidence-what-ships-today-by-area)), score unchanged | | View & navigation | 3 | 70% | multiple windows and Arrange | 9–14 | estimated; document-tab artwork hover/copy and lossless transfer tested (2026-10-22; score/hours unchanged); cached canvas reprojection preserves scale during resize, rotation and DPI changes (regression and contributor original-file evidence below; scores/hours unchanged); cached canvas dimensions match the renderer limit before cache-key creation, view construction and worker submission (regression evidence below) | @@ -1419,6 +1419,7 @@ A VectorCraft addition, not a feature of the reference app, so no area score mov | Date | Change | Summary | |---|---|---| +| 2026-11-01 | minor | Selection: Select › Start Global Edit on a group takes the groups like it (the same objects at the same places, moved or scaled; Appearance, Size and the artboard options apply) and gives an edit made to an object inside one to the object at its place in each of the others, in proportion, as one undo step (#4939); Global Edit of rotated copies and Shift-click exclusion remain. Score unchanged at 85% (estimated), hours unchanged. | | 2026-11-01 | minor | Document Tools add-on (a VectorCraft addition): New Symbols from Layers (#4927); evidence added; scores unchanged. | | 2026-11-01 | minor | Document Tools add-on (a VectorCraft addition): Save a Timestamped Copy (#4926); evidence added; scores unchanged. | | 2026-11-01 | minor | Path Precision Tools add-on (a VectorCraft addition): Divide into Dashes (#4950); evidence added; scores unchanged. | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index e05eeafa8..9b4a0460a 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Document Tools add-on: New Symbols from Layers, #4927) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Start Global Edit on groups, #4939) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -296,7 +296,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po | Actions panel: a clicked action stays selected for Play Current Selection and Delete Selection | partial | #1853 (part of #1629); editing actions and action shortcuts remain | | Actions panel: Create New Set (bottom bar, ≡ › New Set…) names a new set and Set Options… renames the current one; a click on a set's name makes it current and a recording goes into the current set ([helpx: Automation with actions](https://helpx.adobe.com/illustrator/using/automation-actions.html)) | partial | #3712; the New Action dialog (name, set menu, shortcut, colour), dragging actions between sets and deleting a set remain | | Properties panel per context (every selection type's Quick Actions) | partial | one clip group: Edit Clipping Path and Edit Contents, at the top of the panel rather than in Quick Actions (G1); the overview's additional controls ([Properties panel overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/properties-panel-overview.html)): Blend Options (one blend selected, with any tool, #4817) and Merge Live Paint (a Live Paint group with more) in Quick Actions, the Width Profile list (Width tool or a variable-width stroke) and an applied effect's Delete icon in Appearance, Corner and Corner Radius for corners selected with Direct Selection; Variable Font was already there | -| Select › Start Global Edit / Stop Global Edit; Properties › Quick Actions with the Global Edit Options popover (Match: Appearance, Size; On Artboards: All Artboards or a range, Include Objects On Canvas; Result(s)); Escape stops it; the matched objects are highlighted in their layer colour while it runs | partial | #3521, `select.globalEdit` / `select.globalEditOptions`, `crates/engine/src/tests/globaledit.rs`; paths, compound paths and symbol instances; the Control panel's copy of the options, Shift-click to leave a match out, highlighting matches before the start and similar groups remain ([public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-similar-objects.html)) | +| Select › Start Global Edit / Stop Global Edit; Properties › Quick Actions with the Global Edit Options popover (Match: Appearance, Size; On Artboards: All Artboards or a range, Include Objects On Canvas; Result(s)); Escape stops it; the matched objects are highlighted in their layer colour while it runs | partial | #3521, `select.globalEdit` / `select.globalEditOptions`, `crates/engine/src/tests/globaledit.rs`; paths, compound paths, symbol instances and groups of them (similar groups, #4939: edits inside one group reach the same object in the others; [public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-groups-with-similar-objects.html)); the Control panel's copy of the options, Shift-click to leave a match out, rotated copies and highlighting matches before the start remain ([public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-similar-objects.html)) | | Contextual task bar | done | | | Control bar with Transform and Stroke popovers | done | | | Pathfinder shape results keep their source names and SVG IDs | done | #1293; Minus Front keeps the back name, Minus Back the front name; Unite/Intersect/Exclude use the backmost named input; compound output carries it once; geometry and paint unchanged | @@ -385,6 +385,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-11-01 | minor | Select › Start Global Edit on a group takes the groups like it (the same objects at the same places, moved or scaled; Appearance, Size and the artboard options apply) and gives an edit made to an object inside one to the object at its place in each of the others, in proportion, as one undo step (#4939). Scores and estimates unchanged | | 2026-11-01 | minor | Document Tools add-on (a VectorCraft addition): New Symbols from Layers and Convert Layers to Symbols (#4927); scores and estimates unchanged | | 2026-11-01 | minor | Document Tools add-on (a VectorCraft addition): Save a Timestamped Copy (#4926); scores and estimates unchanged | | 2026-11-01 | minor | Path Precision Tools add-on (a VectorCraft addition): Divide into Dashes (#4950); scores and estimates unchanged |