Merge pull request #6900 from dotCooCoo/feat/global-edit-highlight-similar

Global Edit: selecting one object highlights the similar objects before the start
This commit is contained in:
BFlat
2026-11-12 23:08:32 +07:00
committed by GitHub
6 changed files with 105 additions and 13 deletions
+45 -7
View File
@@ -20,6 +20,9 @@
//! and they lie the same way relative to each other: the group as a whole is moved or scaled.
//! With Appearance on, each of those objects must have the same fill and stroke too; with Size,
//! the groups the same width and height.
//! - Selecting one of the repeated objects with the Selection tool highlights the similar objects
//! at once, before Global Edit starts. Here they are the objects Start Global Edit would take
//! with the current Global Edit Options ([`similar`]).
//! - While Global Edit is on, an edit made to one of the found objects is made to all of them,
//! each in proportion to its own size: halving the height of a 100 pt object halves a 20 pt
//! match to 10 pt. Stop Global Edit (or Escape) ends it.
@@ -780,18 +783,55 @@ pub(crate) fn propagate(before: &Document, d: &mut Document, ge: &GlobalEdit, se
/// The outlines of the members of the active document's Global Edit that are not selected, hold
/// no selected object and sit inside no selected object, in their layer's colour: the canvas
/// highlights them while Global Edit is on.
/// highlights them while Global Edit is on. While it is off, with one supported object selected,
/// they are the outlines of the objects Start Global Edit would take with the Global Edit Options
/// ([`similar`]).
pub fn highlights(s: &Session) -> Outlines {
let Some(st) = s.active() else { return vec![] };
let Some(ge) = &st.global_edit else { return vec![] };
let Some(ge) = &st.global_edit else { return similar(s) };
if let Some((rev, out)) = &*ge.outlines.borrow()
&& *rev == st.revision
{
return out.clone();
}
let out = outlines(st, &ge.members);
*ge.outlines.borrow_mut() = Some((st.revision, out.clone()));
out
}
/// [`similar`]'s outlines as of a document revision and the options they were found with.
pub type SimilarCache = std::cell::RefCell<Option<(u64, Options, Outlines)>>;
/// Before Global Edit starts: with one supported object selected, the outlines of the other
/// objects Start Global Edit would take now with the session's Global Edit Options, so the canvas
/// shows them as soon as the object is selected. None while an interaction (a drag) is in
/// progress, or when fewer than two objects would be taken. Kept per document revision and
/// options, as finding them walks the whole document.
pub fn similar(s: &Session) -> Outlines {
let Some(st) = s.active() else { return vec![] };
if st.global_edit.is_some() || st.interaction.is_some() || st.selection.objects.len() != 1 {
return vec![];
}
let o = &s.global_edit_options;
if let Some((rev, opts, out)) = &*st.similar_outlines.borrow()
&& *rev == st.revision
&& opts == o
{
return out.clone();
}
let out = match matches(s, o) {
Ok(m) if m.len() >= 2 => outlines(st, &m),
_ => vec![],
};
*st.similar_outlines.borrow_mut() = Some((st.revision, o.clone(), out.clone()));
out
}
/// The outlines of `members` that are not selected, hold no selected object and sit inside no
/// selected object, in their layer's color.
fn outlines(st: &crate::DocState, members: &[(NodeId, Affine)]) -> Outlines {
let symbols = st.doc.symbol_boxes();
let out: Vec<_> = ge
.members
members
.iter()
.filter_map(|(id, _)| {
let n = st.doc.node(*id).filter(|n| !holds(n, &st.selection.objects))?;
@@ -804,9 +844,7 @@ pub fn highlights(s: &Session) -> Outlines {
};
Some((path, st.doc.layer_color(*id)))
})
.collect();
*ge.outlines.borrow_mut() = Some((st.revision, out.clone()));
out
.collect()
}
#[cfg(test)]
+4
View File
@@ -34,6 +34,9 @@ pub struct DocState {
/// Select › Start Global Edit: the objects edited together until Stop Global Edit or Escape
/// ([`cmd::globaledit`]). Editing state: not saved, not undoable.
pub global_edit: Option<cmd::globaledit::GlobalEdit>,
/// The objects Start Global Edit would take, highlighted before it starts
/// ([`cmd::globaledit::similar`]), as of a revision. Not saved, not undoable.
pub(crate) similar_outlines: cmd::globaledit::SimilarCache,
/// What leaving symbol editing mode goes back to ([`cmd::symboledit`]).
pub(crate) symbol_edit_return: cmd::symboledit::Return,
pub interaction: Option<Interaction>,
@@ -302,6 +305,7 @@ impl DocState {
variables_highlight: None,
isolation: None,
global_edit: None,
similar_outlines: Default::default(),
symbol_edit_return: Default::default(),
interaction: None,
undo_group: None,
+45 -1
View File
@@ -92,7 +92,50 @@ fn start_global_edit_takes_the_objects_of_the_same_shape_at_any_place_and_size()
assert_eq!(crate::cmd::globaledit::highlights(&s).len(), 2);
let r = s.execute("select.globalEdit", &json!({})).unwrap();
assert_eq!(r["active"], false, "the second run is Stop Global Edit");
assert!(s.doc().unwrap().global_edit.is_none() && crate::cmd::globaledit::highlights(&s).is_empty());
assert!(s.doc().unwrap().global_edit.is_none());
assert_eq!(crate::cmd::globaledit::highlights(&s).len(), 2, "with the square still selected, the objects it would take are highlighted again");
}
#[test]
fn selecting_one_object_highlights_the_similar_objects_before_global_edit_starts() {
use crate::cmd::globaledit::highlights;
let mut s = session();
let [a, b, _, wide, _] = squares(&mut s);
assert!(highlights(&s).is_empty(), "nothing selected");
sel(&mut s, &[a]);
assert_eq!(members(&s), None, "Global Edit has not started");
assert_eq!(highlights(&s).len(), 2, "the copy and the smaller square");
s.execute("select.globalEditOptions", &json!({"size": true})).unwrap();
assert_eq!(highlights(&s).len(), 1, "the options narrow them: the copy only");
s.execute("select.globalEditOptions", &json!({"size": false})).unwrap();
assert_eq!(highlights(&s).len(), 2);
sel(&mut s, &[b]);
s.execute("edit.clear", &json!({})).unwrap();
sel(&mut s, &[a]);
assert_eq!(highlights(&s).len(), 1, "a deleted copy is no longer highlighted");
sel(&mut s, &[wide]);
assert!(highlights(&s).is_empty(), "the rectangle has no similar object");
sel(&mut s, &[a, wide]);
assert!(highlights(&s).is_empty(), "two objects selected");
}
#[test]
fn the_canvas_highlights_similar_objects_before_the_start_with_the_selection_tool_only() {
let mut s = session();
let [a, b, ..] = squares(&mut s);
let outline = node(&s, b).stroke_path().unwrap();
let shown = |s: &mut Session| {
s.overlays(ViewInfo::default())
.iter()
.any(|o| matches!(o, vectorcraft_tools::Overlay::Path { path, width, .. } if *path == outline && *width == 1.0))
};
sel(&mut s, &[a]);
s.select_tool("selection", ViewInfo::default()).unwrap();
assert!(shown(&mut s), "the copy is highlighted with the Selection tool");
s.select_tool("directSelection", ViewInfo::default()).unwrap();
assert!(!shown(&mut s), "not with another tool before Global Edit starts");
s.execute("select.globalEdit", &json!({})).unwrap();
assert!(shown(&mut s), "while Global Edit is on, with any tool");
}
#[test]
@@ -506,6 +549,7 @@ fn leaving_out_all_but_one_ends_global_edit_and_the_first_object_stays() {
let r = s.execute("select.globalEditExclude", &json!({"id": b.0})).unwrap();
assert_eq!(r, json!({"active": false, "count": 0, "ids": []}));
assert_eq!(members(&s), None, "one object left: Global Edit ends");
sel(&mut s, &[wide]);
assert!(crate::cmd::globaledit::highlights(&s).is_empty());
}
+5 -2
View File
@@ -1142,8 +1142,11 @@ impl Session {
let place = w.show.then_some(WidgetPlace { screen: view.screen.as_ref(), corner: w.position });
v.splice(0..0, vectorcraft_tools::distort::perspective::grid_overlays_in(&d.doc, 1.0 / view.zoom.max(1e-9), place));
}
// Global Edit: the objects edited with the selected one are highlighted.
for (path, color) in crate::cmd::globaledit::highlights(self) {
// Global Edit: the objects edited with the selected one are highlighted; before it starts,
// with the Selection tool, the objects it would take.
let ge_on = self.active().is_some_and(|d| d.global_edit.is_some());
let shown = if ge_on || self.tool.id() == "selection" { crate::cmd::globaledit::highlights(self) } else { vec![] };
for (path, color) in shown {
v.push(Overlay::Path { path, color, width: 1.0, dashed: false });
}
if let Some((cmd, p)) = deferred_preview {
+3 -2
View File
@@ -610,6 +610,8 @@ macOS cursor delivery (#2292) is narrowed to a native host adapter using the exi
them in proportion, as one undo step ([public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-similar-objects.html));
on a group (#4939), the groups like it (the same objects at the same places, moved or scaled) take an edit
made to an object inside it at the object in the same place ([public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-groups-with-similar-objects.html));
with one such object or group selected with the Selection tool, the objects Start Global Edit would take are
highlighted on the canvas before it starts (`selecting_one_object_highlights_the_similar_objects_before_global_edit_starts`);
dynamic symbols (#4938): Symbol Options › Symbol Type (Dynamic Symbol / Static Symbol; `type` on
`symbol.new` / `symbol.options` / `symbol.list`), an instance's own fill, stroke, opacity and blend for an
object of a dynamic symbol's art (`symbol.instance.setAppearance`, by child-index path), drawn and exported
@@ -654,8 +656,7 @@ macOS cursor delivery (#2292) is narrowed to a native host adapter using the exi
`graphicStyle.addFromLibrary`).
- **Missing:** more built-in brush libraries beyond the eight shipped (and further graphic-style libraries beyond the ten shipped, and further symbol libraries beyond Basic
Shapes, UI Frames, Map Markers, Inventory Icons and Skill Icons; original, generated in code, never Adobe's); dynamic symbols' `.ai` read and write;
Global Edit of groups holding type or images, rotated copies as matches and highlighting matches before the
start; 9-slice guides outside the symbol's art (a dragged guide stops at the art's edge).
Global Edit of groups holding type or images, and rotated copies as matches; 9-slice guides outside the symbol's art (a dragged guide stops at the art's edge).
- **Estimate:** 5–15 h.
## G3 3D and Materials
+3 -1
View File
@@ -2415,7 +2415,9 @@ each of the others, whose hidden or locked objects are left as they are; when th
other group takes its objects at that level, keeping its own ids in order. Whenever the document is put back (Undo,
Redo, a cancelled drag or scrub), the matches it no longer has leave the Global Edit, which ends when fewer than two
are left. An edit that would take any match past the canvas fails with the coordinates-out-of-range error and changes
nothing. Nothing of it is saved.
nothing. Nothing of it is saved. Before Global Edit starts, while one object that `select.globalEdit` accepts is
selected with the Selection tool, the canvas highlights the other objects a start would take with the current Global
Edit Options (none while a drag is in progress, or when there are none).
`select.globalEditExclude {id}` leaves the match `id` out of the Global Edit, as a Shift-click on a highlighted match
(or on an object inside it) with the Selection tool does: it is no longer highlighted, and later edits leave it as it