diff --git a/crates/engine/src/cmd.rs b/crates/engine/src/cmd.rs index 99b8e1ecdd..313531bb99 100644 --- a/crates/engine/src/cmd.rs +++ b/crates/engine/src/cmd.rs @@ -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 diff --git a/crates/engine/src/cmd/plugin_call.rs b/crates/engine/src/cmd/plugin_call.rs index 6d1ddbee43..6c23161a9d 100644 --- a/crates/engine/src/cmd/plugin_call.rs +++ b/crates/engine/src/cmd/plugin_call.rs @@ -540,6 +540,7 @@ struct Saved { recent_colors: Vec, recent_urls: Vec, 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; } diff --git a/crates/engine/src/cmd/plugin_call/reviewed.rs b/crates/engine/src/cmd/plugin_call/reviewed.rs index 03a931ba5d..2b1826328e 100644 --- a/crates/engine/src/cmd/plugin_call/reviewed.rs +++ b/crates/engine/src/cmd/plugin_call/reviewed.rs @@ -536,6 +536,7 @@ pub(super) const ALLOWED: &[&str] = &[ "select.same.textStrokeColor", "select.segments", "select.set", + "select.similar", "select.storedPath", "select.toggle", "select.update", diff --git a/crates/engine/src/cmd/select.rs b/crates/engine/src/cmd/select.rs index ac9ca365f4..03bbd11ab6 100644 --- a/crates/engine/src/cmd/select.rs +++ b/crates/engine/src/cmd/select.rs @@ -225,9 +225,69 @@ pub fn specs() -> Vec { 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 { + 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 diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 4b47966463..890f8622f8 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -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, diff --git a/crates/engine/src/script/mod.rs b/crates/engine/src/script/mod.rs index 1b95b5fe2a..295c9ac795 100644 --- a/crates/engine/src/script/mod.rs +++ b/crates/engine/src/script/mod.rs @@ -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>, global_edit_options: cmd::globaledit::Options, + select_similar: &'static str, recent_urls: Vec, 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); diff --git a/crates/engine/src/session/documents.rs b/crates/engine/src/session/documents.rs index 1244b77432..b54333206f 100644 --- a/crates/engine/src/session/documents.rs +++ b/crates/engine/src/session/documents.rs @@ -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(), diff --git a/crates/engine/src/tests/menucmds.rs b/crates/engine/src/tests/menucmds.rs index 1151f83910..4afab29a91 100644 --- a/crates/engine/src/tests/menucmds.rs +++ b/crates/engine/src/tests/menucmds.rs @@ -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] diff --git a/crates/engine/src/tests/script_run.rs b/crates/engine/src/tests/script_run.rs index d272fea167..94daa358bd 100644 --- a/crates/engine/src/tests/script_run.rs +++ b/crates/engine/src/tests/script_run.rs @@ -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"); diff --git a/crates/ui-egui/src/chrome/control_bar.rs b/crates/ui-egui/src/chrome/control_bar.rs index fc06926247..8b6c2400e6 100644 --- a/crates/ui-egui/src/chrome/control_bar.rs +++ b/crates/ui-egui/src/chrome/control_bar.rs @@ -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 +/// (). 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; diff --git a/crates/ui-egui/src/chrome/tests.rs b/crates/ui-egui/src/chrome/tests.rs index be37f10a29..fc0099a291 100644 --- a/crates/ui-egui/src/chrome/tests.rs +++ b/crates/ui-egui/src/chrome/tests.rs @@ -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| { + 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 = 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 diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index fb3080c8c2..0605145651 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -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 Съвпадение diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 118609f801..06f7d13084 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index ab50e14a58..8e43b2b181 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 3b9c300980..dc099defb7 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 59ce36d15f..616e6a2e3f 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index ddc01a8016..373b47b25b 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index a89d3c72e4..7c476955bd 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -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 Σε πίνακες σχεδίασης diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 4a912f8005..2de3b42bf1 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 06499d13ea..4c60f1aee1 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 90882cf105..687baadeef 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/gu.tsv b/crates/ui-egui/src/i18n/gu.tsv index 0e9ed1997e..9998610049 100644 --- a/crates/ui-egui/src/i18n/gu.tsv +++ b/crates/ui-egui/src/i18n/gu.tsv @@ -1508,6 +1508,8 @@ Global ગ્લોબલ Global Brush Dimensions ગ્લોબલ બ્રશ પરિમાણો Global Edit Options ગ્લોબલ એડિટ વિકલ્પો + Select Similar Objects સમાન ઑબ્જેક્ટ પસંદ કરો + Select Similar Options સમાન પસંદગીના વિકલ્પો Global Process Color ગ્લોબલ પ્રોસેસ રંગ Glowing Edges… ગ્લોઇંગ એજિસ… Glyph Bounds ગ્લિફ બાઉન્ડ્સ diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 671052e82b..d053ca01af 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -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 ग्लो मात्रा diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index a218a2ce15..e540f5a3fb 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index fdf06cc9ee..040f2bfd5a 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 2b5a6bf3da..99e24d180d 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 87ec6c8b05..952d811ccb 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -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 アートボード上 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 8c42eafaf1..d5ea058337 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -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 სამუშაო დაფებზე diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 4fd8001ea7..6dbd010e41 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -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 아트보드에서 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 31872f060e..469712b7b4 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 77b1425e5f..3b7c804fbe 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index caed79af2a..5e0c001d25 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 206b209ee2..9d49ebd6a7 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -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 На монтажных областях diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 82e63d1667..92ce3e0cef 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 237694bff5..51df63e4f9 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -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 diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index a74a03529d..45e65cd644 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -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 На монтажних областях diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 16c613874c..db04b6ad0a 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -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ẽ diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index b83bee650f..65cb744c57 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -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 在画板上 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 4637ecb811..14d1972d62 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -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 在工作區域上 diff --git a/crates/ui-egui/src/panels/paragraph/tests.rs b/crates/ui-egui/src/panels/paragraph/tests.rs index f570beaeca..25c05ce3fc 100644 --- a/crates/ui-egui/src/panels/paragraph/tests.rs +++ b/crates/ui-egui/src/panels/paragraph/tests.rs @@ -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 { - 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. diff --git a/docs/mcp.md b/docs/mcp.md index 99b0bfef38..dee1116bfa 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -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 diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index c6972468cc..9b66eaafec 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -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`.