mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 21:56:24 +00:00
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:
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user