Control bar: Select Similar Objects selects by the attribute its menu keeps

With something selected, the Control bar shows a Select Similar Objects
button and, beside it, the Select Similar Options menu. The button
selects the objects like the first selected one by the attribute kept
for the session; a menu item selects by its attribute and keeps it,
and the kept item is checked. The items are All, Fill Color, Fill &
Stroke, Stroke Color, Stroke Weight, Opacity, Blending Mode,
Appearance, Graphic Style, Shape and Symbol Instance.

- select.similar {by?} runs the select.same command of that name, or
  for "all" matches fill and stroke colors, stroke weight, opacity and
  blending mode together. An unknown by is an error that keeps the
  selection and the kept attribute. The journal records the attribute
  used, so a recorded action repeats it, and a failed script or plug-in
  call puts the kept attribute back.
- The button and menu belong to the Object Options group and sit at
  the right end of the bar, after the X, Y, W and H fields. They take
  only the room the other controls leave, so the bar leaves them out
  before any other control.
- The Paragraph tests' control_bar_node looks for a label before a
  value, so a link's text run listed first no longer stands in for
  the link.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-11-14 18:41:14 -08:00
parent dafd993395
commit f05d481247
42 changed files with 436 additions and 13 deletions
+1
View File
@@ -828,6 +828,7 @@ pub(crate) use edit::{settle_interaction, typing_in_progress};
pub(crate) use object::bounds_rewrite;
pub(crate) use object::{keep_aligned_in, refresh_pixel_aligned};
pub use plugin_call::{Location, ParamsPlace, key_location, location_in, location_keys};
pub use select::SIMILAR;
pub(crate) use vectorcraft_doc::range::parse_range;
/// A date param in Unix seconds that defaults to the clock: `None` when absent (now, see
+3
View File
@@ -540,6 +540,7 @@ struct Saved {
recent_colors: Vec<vectorcraft_color::Color>,
recent_urls: Vec<String>,
global_edit_options: super::globaledit::Options,
select_similar: &'static str,
envelope_defaults: Option<(vectorcraft_doc::live::EnvelopeOptions, f64)>,
}
@@ -564,6 +565,7 @@ impl Saved {
recent_colors: s.recent_colors.clone(),
recent_urls: s.recent_urls.clone(),
global_edit_options: s.global_edit_options.clone(),
select_similar: s.select_similar,
envelope_defaults: s.envelope_defaults,
}
}
@@ -587,6 +589,7 @@ impl Saved {
s.recent_colors = self.recent_colors;
s.recent_urls = self.recent_urls;
s.global_edit_options = self.global_edit_options;
s.select_similar = self.select_similar;
s.envelope_defaults = self.envelope_defaults;
s.pending_paint = None;
}
@@ -536,6 +536,7 @@ pub(super) const ALLOWED: &[&str] = &[
"select.same.textStrokeColor",
"select.segments",
"select.set",
"select.similar",
"select.storedPath",
"select.toggle",
"select.update",
+60
View File
@@ -225,9 +225,69 @@ pub fn specs() -> Vec<CommandSpec> {
has_selection,
same_attribute
),
cmd!(
"select.similar",
"Select Similar Objects",
[],
None,
"{by?: \"all\"|\"fillColor\"|\"fillAndStroke\"|\"strokeColor\"|\"strokeWeight\"|\"opacity\"|\"blendingMode\"|\"appearance\"|\"graphicStyle\"|\"shapeType\"|\"symbolInstance\"} the Control bar's Select Similar Objects button and its menu: selects the objects like the first selected object by `by`, running the select.same command of that name; `all` selects the objects with its fill and stroke colours, stroke weight, opacity and blending mode (tints, compound paths and Live Paint faces as select.same.fillColor). A `by` given is kept for the session once the selection is made; without it, the kept one (`all` until one is given). Any other `by` is an error that keeps the selection and the kept attribute → {count, by}",
has_selection,
similar
),
]
}
/// The attributes Select Similar Objects (`select.similar`) selects by, in its menu's order, each
/// with the Select › Same command it runs; `all` compares the fill and stroke colours, the stroke
/// weight, the opacity and the blending mode at once, with no command of its own.
pub const SIMILAR: [(&str, Option<&str>); 11] = [
("all", None),
("fillColor", Some("select.same.fillColor")),
("fillAndStroke", Some("select.same.fillAndStroke")),
("strokeColor", Some("select.same.strokeColor")),
("strokeWeight", Some("select.same.strokeWeight")),
("opacity", Some("select.same.opacity")),
("blendingMode", Some("select.same.blendingMode")),
("appearance", Some("select.same.appearance")),
("graphicStyle", Some("select.same.graphicStyle")),
("shapeType", Some("select.same.shapeType")),
("symbolInstance", Some("select.same.symbolInstance")),
];
fn similar(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "select.similar";
let by = match text_param(p, "by", C)? {
Some(name) => *SIMILAR.iter().find(|(n, _)| *n == name).ok_or_else(|| {
let names: Vec<&str> = SIMILAR.iter().map(|(n, _)| *n).collect();
bad(C, format!("unknown `by` \"{name}\": expected one of {}", names.join(", ")))
})?,
None => *SIMILAR.iter().find(|(n, _)| *n == s.select_similar).unwrap_or(&SIMILAR[0]),
};
let mut r = match by.1 {
Some(same_cmd) => s.execute(same_cmd, &json!({}))?,
None => {
let tint = s.prefs.select_same_tint_percent;
let r = same_fill(s, C, |a, b| {
same_paint(&fill_of(a), &fill_of(b), tint)
&& same_paint(&stroke_of(a).0, &stroke_of(b).0, tint)
&& (stroke_weight(a) - stroke_weight(b)).abs() < 1e-9
&& (a.opacity - b.opacity).abs() < 1e-6
&& a.blend == b.blend
})?;
// Select › Reselect repeats `all`, whichever attribute is kept by then.
if let Some(last) = s.doc_mut()?.last_selection_cmd.as_mut() {
last.1 = json!({ "by": by.0 });
}
r
}
};
s.select_similar = by.0;
// A recorded action or the journal repeats the attribute used, not the one kept by then.
s.note_journal("by", json!(by.0));
r["by"] = json!(by.0);
Ok(r)
}
/// Selectable objects: children of visible, unlocked layers (or of the isolation container).
/// The objects Select All takes: those of the isolated container, else of every layer (looking
/// through sublayers, which are not objects). In isolation mode there are none while the isolated
+3
View File
@@ -344,6 +344,9 @@ pub struct Session {
/// The Global Edit Options Start Global Edit matches with (`select.globalEditOptions`); not
/// saved.
pub global_edit_options: cmd::globaledit::Options,
/// The attribute Select Similar Objects selects by (`select.similar`), one of
/// [`cmd::SIMILAR`]; not saved.
pub select_similar: &'static str,
/// Set while [`Session::tool_typed_text`] passes keyboard input to the active tool
/// ([`vectorcraft_tools::ToolContext::typed_from_keyboard`]).
keyboard_text: bool,
+5 -1
View File
@@ -429,7 +429,7 @@ struct World {
}
/// The choices outside the documents that a script's commands can change: the preferences, the
/// global edit options, the recent URLs, the chosen gradient stop, freeform point and appearance
/// global edit options, the Select Similar Objects attribute, the recent URLs, the chosen gradient stop, freeform point and appearance
/// item, the Eyedropper's sample, the envelope defaults and the Caps Lock state.
#[derive(Clone)]
struct Choices {
@@ -437,6 +437,7 @@ struct Choices {
caps_lock: bool,
eyedropper_sample: Option<Arc<vectorcraft_doc::Node>>,
global_edit_options: cmd::globaledit::Options,
select_similar: &'static str,
recent_urls: Vec<String>,
gradient_stop: Option<(usize, cmd::gradient::StopOwner)>,
freeform_point: Option<(usize, cmd::gradient::StopOwner)>,
@@ -451,6 +452,7 @@ impl Choices {
caps_lock: s.caps_lock,
eyedropper_sample: s.eyedropper_sample.clone(),
global_edit_options: s.global_edit_options.clone(),
select_similar: s.select_similar,
recent_urls: s.recent_urls.clone(),
gradient_stop: s.gradient_stop,
freeform_point: s.freeform_point,
@@ -464,6 +466,7 @@ impl Choices {
s.caps_lock = self.caps_lock;
s.eyedropper_sample = self.eyedropper_sample;
s.global_edit_options = self.global_edit_options;
s.select_similar = self.select_similar;
s.recent_urls = self.recent_urls;
s.gradient_stop = self.gradient_stop;
s.freeform_point = self.freeform_point;
@@ -477,6 +480,7 @@ impl Choices {
swap(&mut self.caps_lock, &mut s.caps_lock);
swap(&mut self.eyedropper_sample, &mut s.eyedropper_sample);
swap(&mut self.global_edit_options, &mut s.global_edit_options);
swap(&mut self.select_similar, &mut s.select_similar);
swap(&mut self.recent_urls, &mut s.recent_urls);
swap(&mut self.gradient_stop, &mut s.gradient_stop);
swap(&mut self.freeform_point, &mut s.freeform_point);
+1
View File
@@ -141,6 +141,7 @@ impl Session {
link_stamps: Default::default(),
link_scan: None,
global_edit_options: Default::default(),
select_similar: crate::cmd::SIMILAR[0].0,
keyboard_text: false,
script_key: None,
script_engines: crate::script::EnginesKey::new(),
+67
View File
@@ -2789,6 +2789,73 @@ fn select_same_fill_keeps_live_paint_faces_to_their_group() {
}
}
/// Select Similar Objects (`select.similar`) runs the Select > Same command of the attribute it is
/// given and keeps that attribute for its next run without one; `all`, the attribute until one is
/// given, matches the fill and stroke colours, the stroke weight, the opacity and the blending mode
/// together. An unknown attribute is an error that keeps the selection and the kept attribute.
#[test]
fn select_similar_selects_by_the_kept_attribute() {
let mut s = session();
let [a, b, faded, heavy, multiplied, blue] = [0.0, 20.0, 40.0, 60.0, 80.0, 100.0].map(|x| rect(&mut s, x, 0.0, 10.0, 10.0));
for id in [a, b, faded, heavy, multiplied] {
fill(&mut s, id, "#ff0000");
}
fill(&mut s, blue, "#0000ff");
s.execute("transparency.set", &json!({"ids": [faded.0], "opacity": 50, "item": null})).unwrap();
s.execute("transparency.set", &json!({"ids": [multiplied.0], "blend": "multiply", "item": null})).unwrap();
sel(&mut s, &[heavy]);
s.execute("stroke.set", &json!({"weight": 3})).unwrap();
let similar = |s: &mut Session, p: Value| {
sel(s, &[a]);
let r = s.execute("select.similar", &p).unwrap();
let mut v = selected(s);
v.sort();
assert_eq!(r["count"], v.len(), "{p}");
(r["by"].as_str().unwrap().to_string(), v)
};
assert_eq!(s.select_similar, "all");
assert_eq!(similar(&mut s, json!({})), ("all".into(), vec![a, b]));
assert_eq!(similar(&mut s, json!({"by": "fillColor"})), ("fillColor".into(), vec![a, b, faded, heavy, multiplied]));
assert_eq!(similar(&mut s, json!({})), ("fillColor".into(), vec![a, b, faded, heavy, multiplied]));
// The journal, and an action recorded around it, keep the attribute used.
let last = s.journal.last().unwrap();
assert_eq!((last.0.as_str(), &last.1), ("select.similar", &json!({"by": "fillColor"})));
assert_eq!(similar(&mut s, json!({"by": "opacity"})), ("opacity".into(), vec![a, b, heavy, multiplied, blue]));
assert_eq!(similar(&mut s, json!({"by": "blendingMode"})), ("blendingMode".into(), vec![a, b, faded, heavy, blue]));
assert_eq!(similar(&mut s, json!({"by": "strokeWeight"})), ("strokeWeight".into(), vec![a, b, faded, multiplied, blue]));
assert_eq!(similar(&mut s, json!({"by": "all"})).1, vec![a, b]);
// Select > Reselect repeats `all`, whichever attribute is kept by then.
s.select_similar = "fillColor";
s.execute("select.reselect", &json!({})).unwrap();
assert_eq!(selected(&s), vec![a, b]);
// Every attribute names a command that runs. A path with no graphic style has none to match,
// which is an error that keeps the attribute as it was.
for (by, _) in crate::cmd::SIMILAR {
sel(&mut s, &[a]);
let was = s.select_similar;
let r = s.execute("select.similar", &json!({ "by": by }));
if by == "graphicStyle" {
assert!(r.is_err(), "{r:?}");
assert_eq!(s.select_similar, was);
} else {
r.unwrap_or_else(|e| panic!("{by}: {e:?}"));
assert_eq!(s.select_similar, by);
}
}
// An unknown or malformed attribute changes nothing.
s.select_similar = "opacity";
for bad in [json!("color"), json!(""), json!("FillColor"), json!(3), json!(true), json!(["all"])] {
sel(&mut s, &[a]);
let r = s.execute("select.similar", &json!({ "by": bad }));
assert!(matches!(r, Err(EngineError::BadParams { .. })), "{bad}: {r:?}");
assert_eq!((selected(&s), s.select_similar), (vec![a], "opacity"), "{bad}");
}
// With nothing selected it does not run, and keeps the attribute.
sel(&mut s, &[]);
assert!(s.execute("select.similar", &json!({"by": "fillColor"})).is_err());
assert_eq!(s.select_similar, "opacity");
}
/// Select by Color needs no object to start from: it matches fills and strokes as
/// `document.inspect` reports them, `#rrggbb`.
#[test]
+5
View File
@@ -290,6 +290,11 @@ fn a_failed_run_puts_back_the_choices_its_commands_made() {
run(&mut s, source).unwrap();
assert_eq!(s.global_edit_options, before, "the failed command's choice went back");
let source = "vc.run('shape.rectangle', {x: 0, y: 0, width: 10, height: 10}); \
if (vc.run('select.similar', {by: 'opacity'}).by !== 'opacity') throw new Error('not run'); throw new Error('x');";
assert!(matches!(run(&mut s, source), Err(e) if !format!("{e:?}").contains("not run")));
assert_eq!(s.select_similar, "all", "the Select Similar Objects attribute went back");
assert!(!s.caps_lock);
assert!(run(&mut s, "vc.run('view.capsLock', {on: true}); throw new Error('x');").is_err());
assert!(!s.caps_lock, "the Caps Lock state went back");
+77 -9
View File
@@ -348,8 +348,11 @@ fn transform_reserve(app: &VectorcraftApp, ui: &Ui, selected: bool) -> f32 {
/// Align To menu and, for two or more objects, the Distribute buttons (a single object keeps the bar
/// as it was); each shows only while the end of the bar keeps its room. Groups the panel menu has
/// off are left out, and take no room. Without `room` (the Character group had none), all of these
/// groups are left out, so the bar trims them from its right end before the Character group.
fn groups_before_transform(app: &mut VectorcraftApp, ui: &mut Ui, objects: usize, room: bool) -> bool {
/// groups are left out, so the bar trims them from its right end before the Character group. Also
/// returns whether the Character group and the Align group had their room: the Align buttons, the
/// Align To menu and the Distribute buttons the selection calls for all showed, or the group is
/// off.
fn groups_before_transform(app: &mut VectorcraftApp, ui: &mut Ui, objects: usize, room: bool) -> (bool, bool) {
let nothing = objects == 0;
let end = end_width(app, ui, nothing);
if room && group_on(app, "paragraph") && crate::panels::paragraph::control_bar(app, ui, BUTTON, |ui| group_end_width(ui) + end) {
@@ -374,24 +377,30 @@ fn groups_before_transform(app: &mut VectorcraftApp, ui: &mut Ui, objects: usize
app.run("edit.preferences", json!({})).ok();
}
}
return false;
return (false, false);
}
let transform = group_on(app, "transform");
let end = if transform { separator_width(ui) + end } else { 0.0 };
if room && group_on(app, "alignDistribute") && ui.available_width() >= buttons_width(ui, 7) + end {
let align_on = group_on(app, "alignDistribute");
// Whether the whole group showed: the Align buttons, the Align To menu and the Distribute
// buttons the selection calls for.
let mut whole = !align_on;
if room && align_on && ui.available_width() >= buttons_width(ui, 7) + end {
crate::panels::align::align_buttons(app, ui, BUTTON);
if ui.available_width() >= crate::panels::align::align_to_menu_width(BUTTON) + ui.spacing().item_spacing.x + end {
crate::panels::align::align_to_menu(app, ui, BUTTON);
whole = objects < 2;
if objects > 1 && ui.available_width() >= GROUP_GAP + buttons_width(ui, 6) + end {
ui.add_space(GROUP_GAP);
crate::panels::align::distribute_buttons(app, ui, BUTTON);
whole = true;
}
}
if transform {
boundary(ui, separator);
}
}
transform
(transform, room && whole)
}
/// Draws the Control bar's Align group after the artboard controls, with the Artboard tool in use:
@@ -670,6 +679,59 @@ fn opacity_and_style(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, opacity:
}
}
/// The width of the chevron that opens the Select Similar Options menu.
const SELECT_SIMILAR_MENU_WIDTH: f32 = 16.0;
/// The label of each attribute of [`vectorcraft_engine::cmd::SIMILAR`] in the Select Similar Options
/// menu, translated.
fn similar_label(by: &str) -> &'static str {
match by {
"all" => tl!("All"),
"fillColor" => tl!("Fill Color"),
"fillAndStroke" => tl!("Fill & Stroke"),
"strokeColor" => tl!("Stroke Color"),
"strokeWeight" => tl!("Stroke Weight"),
"opacity" => tl!("Opacity"),
"blendingMode" => tl!("Blending Mode"),
"appearance" => tl!("Appearance"),
"graphicStyle" => tl!("Graphic Style"),
"shapeType" => tl!("Shape"),
_ => tl!("Symbol Instance"),
}
}
/// The Control bar's Select Similar Objects button and, beside it, the Select Similar Options menu
/// (<https://help.adobe.com/archive/en_US/illustrator/cs5/illustrator_cs5_help.pdf#page=165>). The
/// button runs `select.similar`, which selects the objects like the first selected one by the
/// attribute kept for the session; a menu item runs it by that item's attribute, which is kept from
/// then on, and the kept one is checked. Both are off while `select.similar` cannot run. They show
/// together at the right end of the bar, after a separator, while something is selected and the
/// panel menu has Object Options on, and only in the room every control ahead of them left
/// ([`contents`]): the bar leaves them out before any other control, and while `ahead_shown` is
/// false (a control ahead of them was left out for room). They show only while the room left is
/// also at least `keep`.
fn trailing_select_similar(app: &mut VectorcraftApp, ui: &mut Ui, ahead_shown: bool, keep: f32) {
let selected = app.session.active().is_some_and(|st| !st.selection.objects.is_empty());
let width = separator_width(ui) + row_width(ui, &[BUTTON, SELECT_SIMILAR_MENU_WIDTH]);
if !ahead_shown || !selected || !group_on(app, "objectOptions") || ui.available_width() < width.max(keep) {
return;
}
boundary(ui, separator);
let enabled = crate::menus::enabled(app, "select.similar");
if widgets::icon_button_enabled(ui, "copy", tl!("Select Similar Objects"), false, enabled, BUTTON).clicked() {
crate::menus::invoke(app, "select.similar", json!({}));
}
let options = widgets::icon_button_enabled(ui, "chevron-down", tl!("Select Similar Options"), false, enabled, SELECT_SIMILAR_MENU_WIDTH);
egui::Popup::menu(&options).show(|ui| {
ui.set_min_width(140.0);
for (by, _) in vectorcraft_engine::cmd::SIMILAR {
if widgets::menu_item(ui, similar_label(by), true, app.session.select_similar == by) {
crate::menus::invoke(app, "select.similar", json!({ "by": by }));
}
}
});
}
/// The Control bar's gripper, at the left end of the bar `bar`. Dragging it shows where a release
/// puts the bar ([`release_at`] in `dock_room`, the window below the menu bar): a band along the
/// edge it docks to, or the outline of the bar floating under the pointer. The bar stays where it is
@@ -861,7 +923,11 @@ fn contents(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, bar: egui::Rect,
// The Character group shows whole, with the Transform link's room left after it, or not at
// all; left out, it leaves out the groups between it and the link too.
let room = !group_on(app, "character") || crate::panels::character::control_bar(app, ui, reserve);
if !groups_before_transform(app, ui, sel.len(), room) {
let (transform, groups_fit) = groups_before_transform(app, ui, sel.len(), room);
if !transform {
// With the Transform group off, the room its link and X/Y/W/H fields would take stays
// free too, as a group left out ahead takes less than that.
trailing_select_similar(app, ui, groups_fit, crate::panels::transform::link_width(ui) + inline_transform_width(ui, t));
erase_trailing(ui);
return;
}
@@ -875,9 +941,9 @@ fn contents(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, bar: egui::Rect,
}
crate::panels::transform::link(app, ui);
// The bounding box, rotated with rotated objects: its centre and its own sides.
if let Some(b) = app.selection_box()
&& ui.available_width() >= inline_transform_width(ui, t)
{
let selection_box = app.selection_box();
let fields_left_out = selection_box.is_some() && ui.available_width() < inline_transform_width(ui, t);
if let Some(b) = selection_box.as_ref().filter(|_| !fields_left_out) {
let c = b.center();
let origin = app.ruler_origin();
let link = crate::panels::transform::proportional(app, ui);
@@ -906,6 +972,8 @@ fn contents(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, bar: egui::Rect,
crate::panels::transform::constrain_link_at(app, ui, ui.interact(room, id, Sense::click()));
}
}
// Select Similar Objects comes last: it takes only the room every control ahead of it left.
trailing_select_similar(app, ui, groups_fit && !fields_left_out, 0.0);
erase_trailing(ui);
});
// Controls that keep their size can still run into the button's room in a narrow window;
+133
View File
@@ -3502,6 +3502,139 @@ fn the_control_bar_shows_the_global_edit_options() {
assert!(seen, "a wide bar has room for it");
}
/// The Control bar's Select Similar Objects button selects the objects like the first selected one
/// by the attribute kept for the session (All at first), and the Select Similar Options menu beside
/// it selects by the item chosen, which is kept and checked from then on. Both show with something
/// selected while the Object Options group is on, and fit inside the bar or are left out.
#[test]
fn the_control_bar_selects_similar_objects() {
crate::i18n::set_current(crate::i18n::Lang::EN);
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
let rect = |app: &mut crate::VectorcraftApp, x: f64, color: &str| {
let id = app.run("shape.rectangle", json!({"x": x, "y": 20, "width": 40, "height": 40})).unwrap()["id"].as_u64().unwrap();
app.run("paint.setFill", json!({"ids": [id], "color": color})).unwrap();
id
};
let a = rect(&mut app, 20.0, "#ff0000");
let b = rect(&mut app, 100.0, "#ff0000");
let c = rect(&mut app, 200.0, "#0000ff");
let ctx = egui::Context::default();
ctx.enable_accesskit();
crate::theme::install_fonts(&ctx);
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(2400.0, 300.0));
let frame = |app: &mut crate::VectorcraftApp, events: Vec<egui::Event>| {
let mut out = ctx.run_ui(egui::RawInput { events, screen_rect: Some(screen), ..Default::default() }, |ui| super::control_bar(app, ui));
out.textures_delta.clear();
out.platform_output.accesskit_update.map(|u| u.nodes).unwrap_or_default()
};
type Nodes = Vec<(egui::accesskit::NodeId, egui::accesskit::Node)>;
let find = |nodes: &Nodes, name: &str| nodes.iter().find(|(_, n)| n.label() == Some(name) || n.value() == Some(name)).map(|(_, n)| n.clone());
// A menu item's label carries its check mark or the room for it.
let item = |nodes: &Nodes, name: &str| {
nodes.iter().find(|(_, n)| n.label().or(n.value()).is_some_and(|t| t.trim_start_matches(['✓', ' ']) == name)).map(|(_, n)| n.clone())
};
let center = |n: &egui::accesskit::Node| {
let b = n.bounds().unwrap_or_else(|| panic!("{n:?}"));
egui::pos2(((b.x0 + b.x1) / 2.0) as f32, ((b.y0 + b.y1) / 2.0) as f32)
};
let click = |app: &mut crate::VectorcraftApp, at: egui::Pos2| {
let press = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
frame(app, vec![egui::Event::PointerMoved(at), press(true)]);
frame(app, vec![press(false)]);
frame(app, vec![]);
frame(app, vec![])
};
let selected = |app: &crate::VectorcraftApp| {
let mut ids: Vec<u64> = app.session.active().unwrap().selection.objects.iter().map(|i| i.0).collect();
ids.sort();
ids
};
app.run("select.set", json!({"ids": []})).unwrap();
frame(&mut app, vec![]);
let nodes = frame(&mut app, vec![]);
assert!(find(&nodes, "Select Similar Objects").is_none() && find(&nodes, "Select Similar Options").is_none(), "nothing selected");
app.run("select.set", json!({"ids": [a]})).unwrap();
frame(&mut app, vec![]);
let nodes = frame(&mut app, vec![]);
let button = find(&nodes, "Select Similar Objects").unwrap_or_else(|| panic!("one rectangle: the button shows"));
click(&mut app, center(&button));
assert_eq!(selected(&app), vec![a, b], "All: the red rectangles");
// The menu selects by the item chosen and keeps it.
app.run("select.set", json!({"ids": [a]})).unwrap();
frame(&mut app, vec![]);
let options = find(&frame(&mut app, vec![]), "Select Similar Options").unwrap_or_else(|| panic!("the options menu shows"));
let nodes = click(&mut app, center(&options));
for label in [
"All",
"Fill Color",
"Fill & Stroke",
"Stroke Color",
"Stroke Weight",
"Opacity",
"Blending Mode",
"Appearance",
"Graphic Style",
"Shape",
"Symbol Instance",
] {
assert!(item(&nodes, label).is_some(), "{label} in the menu");
}
assert!(find(&nodes, "✓ All").is_some(), "All is checked at first");
let stroke = item(&nodes, "Stroke Color").unwrap_or_else(|| panic!("Stroke Color"));
click(&mut app, center(&stroke));
assert_eq!(app.session.select_similar, "strokeColor");
assert_eq!(selected(&app), vec![a, b, c], "Stroke Color: all three share the default stroke");
app.run("select.set", json!({"ids": [c]})).unwrap();
frame(&mut app, vec![]);
let nodes = frame(&mut app, vec![]);
click(&mut app, center(&find(&nodes, "Select Similar Objects").unwrap_or_else(|| panic!("the button"))));
assert_eq!(selected(&app), vec![a, b, c], "the button keeps Stroke Color");
let options = find(&frame(&mut app, vec![]), "Select Similar Options").unwrap_or_else(|| panic!("the options menu"));
let nodes = click(&mut app, center(&options));
assert!(find(&nodes, "✓ Stroke Color").is_some(), "the kept item is checked");
// The panel menu's Object Options group turns them off.
app.run("window.control.options", json!({"group": "objectOptions"})).unwrap();
frame(&mut app, vec![]);
assert!(find(&frame(&mut app, vec![]), "Select Similar Objects").is_none(), "Object Options off");
app.run("window.control.options", json!({"group": "objectOptions"})).unwrap();
// At any width the button and its menu fit inside the bar together or are left out. They take
// only the room left at the end of the bar, so they show only while the Align buttons and the
// inline X field do too. A control ahead of them that appears in a wider bar (the Distribute
// buttons) can take that room again, so the widest bar is the one that must show them.
let mut seen = false;
for width in (600..=2400).step_by(10) {
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(width as f32, 300.0));
let mut nodes = Vec::new();
for _ in 0..2 {
let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), ..Default::default() }, |ui| super::control_bar(&mut app, ui));
out.textures_delta.clear();
nodes = out.platform_output.accesskit_update.map(|u| u.nodes).unwrap_or_default();
}
let shown = find(&nodes, "Select Similar Objects").and_then(|n| n.bounds());
let menu = find(&nodes, "Select Similar Options").and_then(|n| n.bounds());
assert_eq!(shown.is_some(), menu.is_some(), "{width}: the button and its menu show together");
if let Some(b) = menu {
assert!(b.x1 <= f64::from(width), "{width}: {b:?}");
assert!(find(&nodes, "Horizontal Align Left").is_some(), "{width}: shown without the Align buttons");
let x = nodes.iter().find(|(_, n)| n.label() == Some("X:") || n.value() == Some("X:")).and_then(|(_, n)| n.bounds());
assert!(x.is_some_and(|x| x.x1 <= b.x0), "{width}: shown without the X field ahead of it: {x:?}");
}
seen = shown.is_some();
}
assert!(seen, "the widest bar has room for it");
// With the Transform group off they still show at the end of a wide bar, after the Align
// buttons.
app.run("window.control.options", json!({"group": "transform", "visible": false})).unwrap();
frame(&mut app, vec![]);
let nodes = frame(&mut app, vec![]);
let (button, align) = (find(&nodes, "Select Similar Objects"), find(&nodes, "Horizontal Align Left"));
let (Some(b), Some(a)) = (button.and_then(|n| n.bounds()), align.and_then(|n| n.bounds())) else {
panic!("Transform off: the button and the Align buttons show")
};
assert!(a.x1 <= b.x0, "after the Align buttons: {a:?} {b:?}");
}
/// At any width the Control bar shows the inline X, Y, W and H fields whole inside its right margin,
/// or leaves all four out and keeps the Transform link; a wider bar never leaves them out once a
/// narrower one showed them. The Align buttons, and for two objects the Distribute buttons, are
+2
View File
@@ -6138,6 +6138,8 @@ axis Y Axis Y ос
Create New Set Създаване на нов набор
Stop Global Edit Край на глобалното редактиране
Global Edit Options Опции за глобално редактиране
Select Similar Objects Избор на подобни обекти
Select Similar Options Опции за избор на подобни
Leave Out of Global Edit Изключване от глобалното редактиране
@msg Global Edit works on paths, compound paths, symbol instances and groups of them Глобалното редактиране работи с пътища, съставни пътища, екземпляри на символи и групи от тях
Match Съвпадение
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6180,6 +6180,8 @@ axis Y Axis Y osa
Create New Set Napravi novi skup
Stop Global Edit Zaustavi globalno uređivanje
Global Edit Options Opcije globalnog uređivanja
Select Similar Objects Odaberi slične objekte
Select Similar Options Opcije odabira sličnih
Leave Out of Global Edit Isključi iz globalnog uređivanja
Match Podudaranje
On Artboards Na radnim pločama
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1853,6 +1853,8 @@ digits Malayalam Malaiàlam
Global Adjust Ajust global
Global Brush Dimensions Dimensions globals del pinzell
Global Edit Options Opcions globals d’edició
Select Similar Objects Selecciona objectes similars
Select Similar Options Opcions de selecció de similars
Leave Out of Global Edit Exclou de l’edició global
Global Process Color Color de procés global
Global Process Color, CMYK Color de procés global, CMYK
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6684,6 +6684,8 @@ axis Y Axis osa Y
Create New Set Vytvořit novou sadu
Stop Global Edit Ukončit globální úpravy
Global Edit Options Možnosti globálních úprav
Select Similar Objects Vybrat podobné objekty
Select Similar Options Možnosti výběru podobných
Leave Out of Global Edit Vyřadit z globálních úprav
Match Shoda
On Artboards Na pracovních plochách
Can't render this file because it is too large.
+2
View File
@@ -6764,6 +6764,8 @@ 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
Select Similar Objects Vælg lignende objekter
Select Similar Options Indstillinger for valg af lignende
Leave Out of Global Edit Udelad fra 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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6276,6 +6276,8 @@ axis Y Axis Y-Achse
Create New Set Neuen Satz erstellen
Stop Global Edit Globale Bearbeitung beenden
Global Edit Options Optionen für globale Bearbeitung
Select Similar Objects Ähnliche Objekte auswählen
Select Similar Options Optionen für ähnliche Objekte
Leave Out of Global Edit Von globaler Bearbeitung ausschließen
Match Übereinstimmung
On Artboards Auf Zeichenflächen
Can't render this file because it is too large.
+2
View File
@@ -6637,6 +6637,8 @@ axis Y Axis Άξονας Y
Create New Set Δημιουργία νέου συνόλου
Stop Global Edit Τερματισμός καθολικής επεξεργασίας
Global Edit Options Επιλογές καθολικής επεξεργασίας
Select Similar Objects Επιλογή παρόμοιων αντικειμένων
Select Similar Options Επιλογές επιλογής παρόμοιων
Leave Out of Global Edit Εξαίρεση από την καθολική επεξεργασία
Match Αντιστοίχιση
On Artboards Σε πίνακες σχεδίασης
Can't render this file because it is too large.
+2
View File
@@ -6628,6 +6628,8 @@ axis Y Axis Eje Y
Create New Set Crear nuevo conjunto
Stop Global Edit Detener edición global
Global Edit Options Opciones de edición global
Select Similar Objects Seleccionar objetos similares
Select Similar Options Opciones de selección de similares
Leave Out of Global Edit Excluir de la edición global
Match Coincidir
On Artboards En mesas de trabajo
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6715,6 +6715,8 @@ axis Y Axis Y-akseli
Create New Set Luo uusi sarja
Stop Global Edit Lopeta yleismuokkaus
Global Edit Options Yleismuokkauksen asetukset
Select Similar Objects Valitse samankaltaiset objektit
Select Similar Options Samankaltaisten valinnan asetukset
Leave Out of Global Edit Jätä pois yleismuokkauksesta
Match Vastaavuus
On Artboards Piirtoalustoilla
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -6679,6 +6679,8 @@ axis Y Axis Axe Y
Create New Set Créer un jeu
Stop Global Edit Arrêter la modification globale
Global Edit Options Options de modification globale
Select Similar Objects Sélectionner les objets similaires
Select Similar Options Options de sélection des objets similaires
Leave Out of Global Edit Exclure de la modification globale
Match Correspondance
On Artboards Sur les plans de travail
Can't render this file because it is too large.
+2
View File
@@ -1508,6 +1508,8 @@
Global ગ્લોબલ
Global Brush Dimensions ગ્લોબલ બ્રશ પરિમાણો
Global Edit Options ગ્લોબલ એડિટ વિકલ્પો
Select Similar Objects સમાન ઑબ્જેક્ટ પસંદ કરો
Select Similar Options સમાન પસંદગીના વિકલ્પો
Global Process Color ગ્લોબલ પ્રોસેસ રંગ
Glowing Edges… ગ્લોઇંગ એજિસ…
Glyph Bounds ગ્લિફ બાઉન્ડ્સ
Can't render this file because it contains an unexpected character in line 72 and column 5.
+2
View File
@@ -2062,6 +2062,8 @@ antiAlias Sharp शार्प
Global Adjust वैश्विक समायोजन
Global Brush Dimensions ग्लोबल ब्रश आयाम
Global Edit Options ग्लोबल एडिट ऑप्शन्स
Select Similar Objects समान ऑब्जेक्ट चुनें
Select Similar Options समान चयन विकल्प
Global Process Color ग्लोबल प्रोसेस कलर
Global Process Color, CMYK ग्लोबल प्रोसेस कलर, CMYK
Glow Amount ग्लो मात्रा
Can't render this file because it is too large.
+2
View File
@@ -6667,6 +6667,8 @@ axis Y Axis Y tengely
Create New Set Új készlet létrehozása
Stop Global Edit Globális szerkesztés befejezése
Global Edit Options Globális szerkesztés beállításai
Select Similar Objects Hasonló objektumok kijelölése
Select Similar Options Hasonlók kijelölésének beállításai
Leave Out of Global Edit Kizárás a globális szerkesztésből
Match Egyezés
On Artboards Rajztáblákon
Can't render this file because it is too large.
+2
View File
@@ -6154,6 +6154,8 @@ axis Y Axis Sumbu Y
Create New Set Buat set baru
Stop Global Edit Hentikan edit global
Global Edit Options Opsi edit global
Select Similar Objects Pilih objek serupa
Select Similar Options Opsi pilih serupa
Leave Out of Global Edit Keluarkan dari 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
Can't render this file because it contains an unexpected character in line 2451 and column 5.
+2
View File
@@ -6310,6 +6310,8 @@ axis Y Axis Asse Y
Create New Set Crea nuovo set
Stop Global Edit Interrompi modifica globale
Global Edit Options Opzioni modifica globale
Select Similar Objects Seleziona oggetti simili
Select Similar Options Opzioni selezione oggetti simili
Leave Out of Global Edit Escludi dalla modifica globale
Match Corrispondenza
On Artboards Sulle tavole da disegno
Can't render this file because it is too large.
+2
View File
@@ -8181,6 +8181,8 @@ axis Y Axis Y 軸
Create New Set 新規セットを作成
Stop Global Edit グローバル編集を終了
Global Edit Options グローバル編集オプション
Select Similar Objects 類似オブジェクトを選択
Select Similar Options 類似オブジェクトの選択オプション
Leave Out of Global Edit グローバル編集から除外
Match 一致
On Artboards アートボード上
Can't render this file because it is too large.
+2
View File
@@ -6636,6 +6636,8 @@ axis Y Axis Y ღერძი
Create New Set ახალი ნაკრების შექმნა
Stop Global Edit გლობალური რედაქტირების დასრულება
Global Edit Options გლობალური რედაქტირების პარამეტრები
Select Similar Objects მსგავსი ობიექტების მონიშვნა
Select Similar Options მსგავსის მონიშვნის პარამეტრები
Leave Out of Global Edit გლობალური რედაქტირებიდან გამორიცხვა
Match დამთხვევა
On Artboards სამუშაო დაფებზე
Can't render this file because it is too large.
+2
View File
@@ -6271,6 +6271,8 @@ axis Y Axis Y축
Create New Set 새 세트 만들기
Stop Global Edit 전체 편집 중지
Global Edit Options 전체 편집 옵션
Select Similar Objects 유사한 개체 선택
Select Similar Options 유사 개체 선택 옵션
Leave Out of Global Edit 전체 편집에서 제외
Match 일치
On Artboards 아트보드에서
Can't render this file because it is too large.
+2
View File
@@ -8598,6 +8598,8 @@ axis Y Axis Y-as
Create New Set Nieuwe set maken
Stop Global Edit Globale bewerking stoppen
Global Edit Options Opties voor globale bewerking
Select Similar Objects Vergelijkbare objecten selecteren
Select Similar Options Opties voor vergelijkbare objecten selecteren
Leave Out of Global Edit Uitsluiten van 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
Can't render this file because it is too large.
+2
View File
@@ -6539,6 +6539,8 @@ axis Y Axis Oś Y
Create New Set Utwórz nowy zestaw
Stop Global Edit Zakończ edycję globalną
Global Edit Options Opcje edycji globalnej
Select Similar Objects Zaznacz podobne obiekty
Select Similar Options Opcje zaznaczania podobnych
Leave Out of Global Edit Wyklucz z edycji globalnej
Match Dopasowanie
On Artboards Na obszarach kompozycji
Can't render this file because it is too large.
+2
View File
@@ -6599,6 +6599,8 @@ axis Y Axis Eixo Y
Create New Set Criar Novo Conjunto
Stop Global Edit Parar Edição Global
Global Edit Options Opções de Edição Global
Select Similar Objects Selecionar Objetos Semelhantes
Select Similar Options Opções de Seleção de Semelhantes
Leave Out of Global Edit Deixar Fora da Edição Global
Match Corresponder
On Artboards Nas Mesas de Trabalho
Can't render this file because it is too large.
+2
View File
@@ -6296,6 +6296,8 @@ axis Y Axis Ось Y
Create New Set Создать новый набор
Stop Global Edit Завершить глобальное редактирование
Global Edit Options Параметры глобального редактирования
Select Similar Objects Выделить похожие объекты
Select Similar Options Параметры выделения похожих
Leave Out of Global Edit Исключить из глобального редактирования
Match Совпадение
On Artboards На монтажных областях
Can't render this file because it is too large.
+2
View File
@@ -6664,6 +6664,8 @@ axis Y Axis Y-axel
Create New Set Skapa ny uppsättning
Stop Global Edit Avsluta global redigering
Global Edit Options Alternativ för global redigering
Select Similar Objects Markera liknande objekt
Select Similar Options Alternativ för att markera liknande
Leave Out of Global Edit Uteslut från global redigering
Match Matcha
On Artboards På ritytor
Can't render this file because it is too large.
+2
View File
@@ -8561,6 +8561,8 @@ axis Y Axis Y ekseni
Create New Set Yeni Set Oluştur
Stop Global Edit Genel Düzenlemeyi Durdur
Global Edit Options Genel Düzenleme Seçenekleri
Select Similar Objects Benzer Nesneleri Seç
Select Similar Options Benzer Seçimi Seçenekleri
Leave Out of Global Edit Genel Düzenlemenin Dışında Bırak
Match Eşleştir
On Artboards Çalışma Yüzeylerinde
Can't render this file because it is too large.
+2
View File
@@ -6290,6 +6290,8 @@ axis Y Axis Вісь Y
Create New Set Створити новий набір
Stop Global Edit Завершити глобальне редагування
Global Edit Options Параметри глобального редагування
Select Similar Objects Виділити схожі об’єкти
Select Similar Options Параметри виділення схожих
Leave Out of Global Edit Виключити з глобального редагування
Match Збіг
On Artboards На монтажних областях
Can't render this file because it is too large.
+2
View File
@@ -6292,6 +6292,8 @@ axis Y Axis Trục Y
Create New Set Tạo bộ mới
Stop Global Edit Dừng chỉnh sửa toàn cục
Global Edit Options Tùy chọn chỉnh sửa toàn cục
Select Similar Objects Chọn các đối tượng tương tự
Select Similar Options Tùy chọn chọn đối tượng tương tự
Leave Out of Global Edit Loại khỏi chỉnh sửa toàn cục
Match Khớp
On Artboards Trên các bảng vẽ
Can't render this file because it contains an unexpected character in line 204 and column 5.
+2
View File
@@ -6523,6 +6523,8 @@ axis Y Axis Y 轴
Create New Set 创建新组
Stop Global Edit 停止全局编辑
Global Edit Options 全局编辑选项
Select Similar Objects 选择相似对象
Select Similar Options 选择相似对象选项
Leave Out of Global Edit 从全局编辑中排除
Match 匹配
On Artboards 在画板上
Can't render this file because it is too large.
+2
View File
@@ -6330,6 +6330,8 @@ axis Y Axis Y 軸
Create New Set 建立新組
Stop Global Edit 停止整體編輯
Global Edit Options 整體編輯選項
Select Similar Objects 選取相似物件
Select Similar Options 選取相似物件選項
Leave Out of Global Edit 從整體編輯中排除
Match 符合
On Artboards 在工作區域上
Can't render this file because it is too large.
+5 -2
View File
@@ -792,9 +792,12 @@ fn control_bar_nodes(ctx: &egui::Context, app: &mut VectorcraftApp, width: f32,
nodes
}
/// The control named `name` (a button's or link's label, a label's text).
/// The control named `name`: a button's or link's label, else a label's text. A link's text run
/// carries the same text as its value, and the update lists the nodes in no set order, so the
/// label is looked for first.
fn control_bar_node(nodes: &Nodes, name: &str) -> Option<egui::accesskit::Node> {
nodes.iter().find(|(_, n)| n.label() == Some(name) || n.value() == Some(name)).map(|(_, n)| n.clone())
let labeled = nodes.iter().find(|(_, n)| n.label() == Some(name));
labeled.or_else(|| nodes.iter().find(|(_, n)| n.value() == Some(name))).map(|(_, n)| n.clone())
}
/// Where the control named `name` ends, if it shows.
+18 -1
View File
@@ -2776,6 +2776,22 @@ template layer, on a locked artboard while nothing is isolated, or outside the i
such as a symbol instance just placed, is not passed over for being on a locked artboard while it stays selected. When
the selection holds no other object, they return the error "nothing selected".
`select.similar {by?}` is the Control bar's Select Similar Objects button and the Select Similar Options menu beside it
([help](https://help.adobe.com/archive/en_US/illustrator/cs5/illustrator_cs5_help.pdf#page=165)). It selects the
objects like the first selected object by `by` and returns `{count, by}`. `by` is `fillColor`, `fillAndStroke`,
`strokeColor`, `strokeWeight`, `opacity`, `blendingMode`, `appearance`, `graphicStyle`, `shapeType` or
`symbolInstance`, which runs the `select.same.*` command of that name, or `all`, which selects the objects whose fill
and stroke colors, stroke weight, opacity and blending mode all match (tints, compound paths and Live Paint faces as
`select.same.fillColor`); Select › Reselect repeats `all` as `all`. The menu's items, their order and what `all`
compares are VectorCraft's own. A `by` given is kept for the session once the selection is made, and the menu checks
it; without `by` the command uses the kept one, `all` until one is given. Any other `by` is an error that keeps the
selection and the kept attribute, and an error of the `select.same.*` command it runs (for example "the selection has
no graphic style") keeps the kept attribute too. It needs a selection and records no undo step; the journal records the
attribute used, so a recorded action repeats it, and a script run that fails puts the kept attribute back. The button
and the menu show while something is selected and the
Object Options group is on, at the right end of the bar after the X, Y, W and H fields. They take only the room the
other controls leave, so the bar leaves them out, together, before any other control.
`select.object.allOnSameLayers` selects the objects on the layers and sublayers of the selected objects that can be
selected. 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 adds no layer. When the selection holds no
@@ -6698,7 +6714,8 @@ where it is.
`window.control.options {group, visible?}` (desktop app) turns a group of the Control bar's options on (`visible:
true`), off (`false`) or toggles it (omitted), and returns `{group, visible}`. The groups are `snapping` (the Snapping
menu), `anchors` (Convert and Anchors), `toolOptions` (the active tool's options), `objectOptions` (Isolate Selected
Object or Exit Isolation Mode, the envelope controls and the symbol instance controls), `imageOptions` (a selected
Object or Exit Isolation Mode, the envelope controls, the symbol instance controls and Select Similar Objects),
`imageOptions` (a selected
image's or Image Trace object's controls), `fillStroke` (the fill and stroke chips, Stroke and its weight and width
profile), `opacity`, `graphicStyle` (Style, Recolor Artwork and Global Edit Options), `character`, `paragraph`,
`verticalAlignment` (area type's vertical alignment), `alignDistribute`, `transform` (the Transform link and the X/Y/W/H
+1
View File
@@ -648,6 +648,7 @@ since 2026-10-01.
- **Global Edit: Shift-click leaves a match out:** while Global Edit is on, a Shift-click with the Selection tool on a highlighted match, or on an object inside a matched group, leaves that match out (`select.globalEditExclude`): it is no longer highlighted and later edits leave it as it is; the object the edit started from stays, and Global Edit ends when fewer than two matches are left ([Update assets quickly with Global Edit](https://helpx.adobe.com/ph_fil/illustrator/how-to/global-editing-sensei.html)). Regression tests: `a_shift_click_*`, `an_object_left_out_*`, `a_group_left_out_*` and `leaving_out_all_but_one_*` in `crates/engine/src/tests/globaledit.rs`.
- **Global Edit leaves out groups holding type or images:** Select › Start Global Edit on a group that holds type or an image, at any depth, refuses to start, and such a group is not taken as a match, as Global Edit does not take images or text ([Edit similar objects globally](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-similar-objects.html)). Regression test for a group holding type: `global_edit_needs_one_supported_object` in `crates/engine/src/tests/globaledit.rs`.
- **Global Edit Options in the Control bar:** while Select › Start Global Edit can run, the Control bar shows a Global Edit Options dropdown that opens the same options as the Properties panel's (Match: Appearance, Size; On Artboards; Include Objects On Canvas; Result(s)); both set the same options. The dropdown is off while Global Edit is on; it sits after Recolor Artwork, ahead of the X/Y/W/H fields ([Edit similar objects globally](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-similar-objects.html)). Regression test: `the_control_bar_shows_the_global_edit_options` in `crates/ui-egui/src/chrome/tests.rs`.
- **Select Similar Objects in the Control bar:** with something selected, the Control bar's Select Similar Objects button selects the objects like the first selected one, and the Select Similar Options menu beside it chooses what that is based on: All, Fill Color, Fill & Stroke, Stroke Color, Stroke Weight, Opacity, Blending Mode, Appearance, Graphic Style, Shape or Symbol Instance (`select.similar {by}`). The choice is kept for the session and checked in the menu ([archived help guide, select objects with the same attributes](https://help.adobe.com/archive/en_US/illustrator/cs5/illustrator_cs5_help.pdf#page=165)). The list and what All compares are VectorCraft's own. Regression tests: `the_control_bar_selects_similar_objects` in `crates/ui-egui/src/chrome/tests.rs`, `select_similar_selects_by_the_kept_attribute` in `crates/engine/src/tests/menucmds.rs`.
- **Global Edit's enabled check on large paths:** the check that decides whether Select › Start Global Edit can run on the selected path, compound path or symbol instance (which the Control bar makes every frame) measures the box of its anchors and handles instead of building and normalizing a copy of every anchor, with the same answer. On a 200,000-anchor path the check takes 0.7 ms instead of 2.2 to 3.0 ms. Regression tests: `support_*` in `crates/engine/src/cmd/globaledit.rs`.
- **Start Global Edit is offered only for a group it can start on:** with a group selected, Select › Start Global Edit is disabled, and the Properties panel's Quick Actions and the Control bar do not offer it or its options, when the group holds type, an image or nothing, at any depth; the check walks the group's objects the same way, stopping at the first one Global Edit does not take. This is VectorCraft's own rule. Regression test: `start_global_edit_is_offered_only_for_a_group_of_supported_objects` in `crates/engine/src/tests/globaledit.rs`.
- **Global Edit takes rotated copies:** Select › Start Global Edit on an object or group also takes its rotated copies, with the highlights before the start and the options' Results count, and an edit reaches each copy turned by its angle, as copies of a group rotated around a point are edited at once ([Design a modern geometric logo](https://helpx.adobe.com/vn_vi/illustrator/how-to/modern-logo-design.html)). Under VectorCraft's own rules, a copy's angle is the one that lays the selected object's anchors and handles on the copy's in order, each within a millionth of the larger side of the copy's box; a reflected copy is not taken; and with Size, a rotated copy has the same size when it is the object rotated without scaling. Regression tests: `start_global_edit_takes_rotated_copies_*`, `a_rotated_copy_leads_*`, `size_takes_a_rotated_copy_*`, `a_square_whose_anchors_were_turned_*`, `rotated_groups_are_like_the_group_*` and `a_rotated_symbol_instance_*` in `crates/engine/src/tests/globaledit.rs`.