mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 18:12:11 +00:00
Smart Guides: Snap to Last Location puts paths Direct Selection moves by an anchor back
A path selected whole that Direct Selection moves held by one of its anchors snapped only through the anchor-drag targets, so Snap to Last Location never put it back. The move now goes back to where it began within Snapping Tolerance, labeled "last location", as a Selection tool move does; an Alt copy is not pulled back.
This commit is contained in:
@@ -60,7 +60,7 @@ use crate::guides::{HandleSnap, Leave, PointSnap, Targets};
|
||||
use crate::meshedit::{MeshEdit, grid_of};
|
||||
use crate::pathtype::{self, BracketDrag, near_bracket};
|
||||
use crate::rulerguide::GuideEdit;
|
||||
use crate::select::{MoveSnap, hit_center, hold_point, is_area_type, is_path_type, matrix_json, release_guide_at};
|
||||
use crate::select::{MoveSnap, hit_center, hold_point, is_area_type, is_path_type, matrix_json, release_guide_at, selection_bounds};
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -200,6 +200,10 @@ pub struct DirectSelectionTool {
|
||||
/// What the anchor holding paths moved whole snaps to while Alt makes a copy: the original left
|
||||
/// behind too. Gathered with [`Self::anchor_snap`], before the previews move the original.
|
||||
copy_snap: Option<PointSnap>,
|
||||
/// With Smart Guides › Snap to Last Location, for paths selected whole that an anchor holds
|
||||
/// while they move: the targets that put the move back where it began, and the bounds it began
|
||||
/// with. Set when the move begins.
|
||||
last_location: Option<(Targets, Option<Rect>)>,
|
||||
/// What the dragged handle snaps to.
|
||||
handle_snap: HandleSnap,
|
||||
/// What the objects being moved snap to, gathered when the move begins.
|
||||
@@ -249,6 +253,7 @@ impl DirectSelectionTool {
|
||||
anchor_snap: None,
|
||||
line_end: None,
|
||||
copy_snap: None,
|
||||
last_location: None,
|
||||
handle_snap: HandleSnap::default(),
|
||||
move_snap: None,
|
||||
partial_snap: None,
|
||||
@@ -724,6 +729,7 @@ impl Tool for DirectSelectionTool {
|
||||
self.state = State::Idle;
|
||||
self.guides.clear();
|
||||
(self.anchor_snap, self.copy_snap, self.move_snap) = (None, None, None);
|
||||
self.last_location = None;
|
||||
(self.click, self.click_segment, self.click_group, self.frame_click) = (None, None, None, None);
|
||||
if frame { vec![Action::Cancel, Action::Notify(FRAME_PICKED.into())] } else { vec![Action::Cancel] }
|
||||
}
|
||||
@@ -1440,6 +1446,9 @@ impl DirectSelectionTool {
|
||||
// Alt can go down or up at any time: gather both while nothing has moved.
|
||||
self.anchor_snap = Some(PointSnap::new(cx, || Targets::for_anchor_drag(cx.doc, cx.selection)));
|
||||
self.copy_snap = Some(PointSnap::new(cx, || Targets::for_anchor_drag(cx.doc, &Selection::default())));
|
||||
// As a move with the Selection tool does ([`MoveSnap::new`]).
|
||||
let back = cx.smart_guides && !cx.snap_to_grid && cx.snap_to_last_location;
|
||||
self.last_location = back.then(|| (Targets::last_location(cx), selection_bounds(cx)));
|
||||
} else {
|
||||
self.move_snap = Some(MoveSnap::new(cx));
|
||||
}
|
||||
@@ -1463,7 +1472,10 @@ impl DirectSelectionTool {
|
||||
return out;
|
||||
}
|
||||
let snap = if ev.mods.alt { &self.copy_snap } else { &self.anchor_snap };
|
||||
if let (Some(grab), Some(snap)) = (grab, snap) {
|
||||
let back = self.last_location.as_ref().filter(|_| !ev.mods.alt).and_then(|(t, from)| t.snap_back(*from, d, cx.snap_tol()));
|
||||
if let (Some(_), Some(ov)) = (grab, back) {
|
||||
(d, self.guides) = (Vec2::ZERO, ov);
|
||||
} else if let (Some(grab), Some(snap)) = (grab, snap) {
|
||||
let shift = ev.mods.shift.then(|| Leave::segment(cx, grab, true));
|
||||
let (q, guides) = snap.snap_from(cx, grab + (p - start), shift.as_ref());
|
||||
(d, self.guides) = (q - grab, guides);
|
||||
@@ -1487,6 +1499,7 @@ impl DirectSelectionTool {
|
||||
self.state = State::Idle;
|
||||
self.guides.clear();
|
||||
(self.anchor_snap, self.copy_snap, self.move_snap) = (None, None, None);
|
||||
self.last_location = None;
|
||||
let group = self.click_group.take();
|
||||
// A click on one of several selected frame anchors selects it alone.
|
||||
if let (false, Some((id, anchor))) = (began, self.frame_click.take()) {
|
||||
|
||||
@@ -542,6 +542,45 @@ fn pressing_an_anchor_of_a_fully_selected_path_keeps_the_selection() {
|
||||
assert_eq!(a, vec![Action::Exec("select.anchors".into(), json!({"id": id.0, "anchors": [[0, 0]], "mode": "toggle"}))]);
|
||||
}
|
||||
|
||||
/// Smart Guides › Snap to Last Location: a path selected whole that Direct Selection moves held by
|
||||
/// one of its anchors goes back to where it began within Snapping Tolerance, labeled "last
|
||||
/// location", a move Shift keeps to its line too; farther out, or with the option off, it moves as
|
||||
/// dragged.
|
||||
#[test]
|
||||
fn snap_to_last_location_puts_a_path_moved_by_its_anchor_back() {
|
||||
let (mut d, id) = doc_with_rect();
|
||||
// An artboard whose edges and center are out of reach.
|
||||
d.artboards[0].rect = Rect::new(0.0, 0.0, 1000.0, 1000.0);
|
||||
let mut s = Selection::default();
|
||||
s.set([id]);
|
||||
let p = paint();
|
||||
let on = ToolContext { snap_to_last_location: true, snapping_tolerance: 10.0, ..cx(&d, &s, &p) };
|
||||
// A press on the top-left anchor, then a drag to `to` with `m` held.
|
||||
let moved = |c: &ToolContext, to: (f64, f64), m: Mods| {
|
||||
let mut t = DirectSelectionTool::new(false);
|
||||
assert!(t.pointer(c, &PointerEvent::new(PointerKind::Down, 100.0, 100.0)).is_empty());
|
||||
let a = t.pointer(c, &PointerEvent::new(PointerKind::Drag, to.0, to.1).with_mods(m));
|
||||
let Some(Action::Preview(command, v)) = a.last() else { panic!("{a:?}") };
|
||||
assert_eq!(command, "object.transform");
|
||||
let labels: Vec<String> =
|
||||
t.overlays(c).into_iter().filter_map(|o| if let Overlay::Label { text, .. } = o { Some(text) } else { None }).collect();
|
||||
(Vec2::new(v["matrix"][4].as_f64().unwrap_or(f64::NAN), v["matrix"][5].as_f64().unwrap_or(f64::NAN)), labels)
|
||||
};
|
||||
let none = Mods::default();
|
||||
let (back, labels) = moved(&on, (106.0, 105.0), none);
|
||||
assert!(back.hypot() < 1e-9, "{back:?}: 7.8 pt away, within 10");
|
||||
assert_eq!(labels, ["last location"]);
|
||||
let shift = Mods { shift: true, ..none };
|
||||
let (back, _) = moved(&on, (107.0, 100.0), shift);
|
||||
assert!(back.hypot() < 1e-9, "{back:?}: a Shift move goes back too");
|
||||
let (out, labels) = moved(&on, (109.0, 108.0), none);
|
||||
assert!((out - Vec2::new(9.0, 8.0)).hypot() < 1e-9, "{out:?}: 12 pt away, out of reach");
|
||||
assert!(labels.is_empty(), "{labels:?}");
|
||||
let off = ToolContext { snap_to_last_location: false, ..on };
|
||||
let (stays, _) = moved(&off, (106.0, 105.0), none);
|
||||
assert!((stays - Vec2::new(6.0, 5.0)).hypot() < 1e-9, "{stays:?}: the option off");
|
||||
}
|
||||
|
||||
/// A click (no drag) on an anchor that is selected with others, whether the whole path is or
|
||||
/// some of its anchors, selects that anchor alone, as in Illustrator: a press keeps the
|
||||
/// selection so a drag moves it all, the release without a drag narrows it. Shift still
|
||||
|
||||
@@ -186,6 +186,13 @@ impl Targets {
|
||||
(self.last_location && d.hypot() <= tol).then(|| from.and_then(|r| self.label(r.center(), "last location")).into_iter().collect())
|
||||
}
|
||||
|
||||
/// Targets that hold nothing but Smart Guides › Snap to Last Location ([`Self::snap_back`]),
|
||||
/// styled as `cx` asks: for a move of the selection whose other snaps come from other targets,
|
||||
/// such as paths selected whole that Direct Selection moves held by an anchor.
|
||||
pub fn last_location(cx: &ToolContext) -> Self {
|
||||
Self { last_location: cx.snap_to_last_location, ..Self::default() }.styled(cx)
|
||||
}
|
||||
|
||||
/// Targets for scaling the selection: the other art, styled as `cx` asks, and with Smart Guides ›
|
||||
/// Spacing Guides the neighbours whose gaps the moving edge should match ([`Self::snap_scale`]).
|
||||
pub fn for_scale(cx: &ToolContext) -> Self {
|
||||
|
||||
Reference in New Issue
Block a user