Merge pull request #7574 from dotCooCoo/fix/issue-7411

Fix #7411: a selection grabbed by an anchor snaps it onto other paths
This commit is contained in:
BFlat
2026-11-14 20:37:33 +07:00
committed by GitHub
4 changed files with 176 additions and 20 deletions
+35 -3
View File
@@ -36,7 +36,8 @@ pub struct Targets {
pub(super) xs: Vec<(f64, Point, Kind)>,
pub(super) ys: Vec<(f64, Point, Kind)>,
/// Path segments and their control bounds, which a point lands on ("path"): gathered only for
/// dragged anchors ([`Self::for_anchor_drag`]).
/// dragged anchors ([`Self::for_anchor_drag`]) and selections moved by an anchor
/// ([`Self::for_move_by_anchor`]).
pub(super) segments: Vec<(Rect, PathSeg)>,
/// Ruler guides, which pull a point into line where they run (Snap to Point).
pub(super) rulers: Vec<Ruler>,
@@ -170,7 +171,18 @@ impl Targets {
/// Guides › Snap to Last Location, a move (not a copy, which has nothing in `exclude`) goes back
/// to where it began when it comes near ([`Self::snap_back`]), unless Snap to Pixel snaps moves.
pub fn for_move(cx: &ToolContext, exclude: &[NodeId]) -> Self {
let mut t = Self::collect(cx.doc, exclude, None).styled(cx);
Self::moving(cx, exclude, Gather::Bounds)
}
/// [`Self::for_move`]'s targets for a selection moved by one of its anchors (#7411): the path
/// segments of the art but `exclude` too, which that anchor lands on ([`Self::snap_onto_path`]),
/// as a dragged anchor does with Direct Selection.
pub fn for_move_by_anchor(cx: &ToolContext, exclude: &[NodeId]) -> Self {
Self::moving(cx, exclude, Gather::Segments)
}
fn moving(cx: &ToolContext, exclude: &[NodeId], gather: Gather) -> Self {
let mut t = Self::collect_inner(cx.doc, exclude, None, None, gather).styled(cx);
if cx.spacing_guides {
t.boxes = object_boxes(cx.doc, &cx.selection.objects);
}
@@ -858,6 +870,21 @@ impl Targets {
self.line(from, q).into_iter().chain(self.label(q, "align")).collect()
}
/// A point `p` near no target anchor or center landed where two target paths cross
/// ("intersect"), else on the nearest target path ("path"), within `tol`: the point and its
/// label, as [`Self::snap_point`] lands it. The ruler guides of `rulers` count as paths that
/// cross ([`Self::rulers`]). None when no path is in reach, or the targets hold no segments.
pub(crate) fn snap_onto_path(&self, p: Point, tol: f64, rulers: Option<&Targets>) -> Option<(Point, Vec<Overlay>)> {
if self.segments.is_empty() {
return None;
}
if let Some(q) = self.crossing_with(p, tol, rulers.map_or(&self.rulers, |r| &r.rulers)) {
return Some((q, self.label(q, "intersect").into_iter().collect()));
}
let q = self.on_segment(p, tol)?;
Some((q, self.label(q, Kind::Edge.label()).into_iter().collect()))
}
/// The nearest point within `tol` of `p` on a target segment.
fn on_segment(&self, p: Point, tol: f64) -> Option<Point> {
self.segments
@@ -872,6 +899,11 @@ impl Targets {
/// Where two target paths, a path and a ruler guide, or two ruler guides cross, the crossing
/// nearest `p` within `tol` ("intersect", #1602).
fn crossing(&self, p: Point, tol: f64) -> Option<Point> {
self.crossing_with(p, tol, &self.rulers)
}
/// [`Self::crossing`] with the ruler guides `rulers`.
fn crossing_with(&self, p: Point, tol: f64, rulers: &[Ruler]) -> Option<Point> {
// A ruler guide running past, as a short line through the reach of `p`.
let ruler = |r: &Ruler| {
let (pos, ..) = r.line_at(p, tol)?;
@@ -885,7 +917,7 @@ impl Targets {
.iter()
.filter(|(b, c)| b.inflate(tol, tol).contains(p) && c.nearest(p, 1e-6).distance_sq <= tol * tol)
.map(|(_, c)| *c)
.chain(self.rulers.iter().filter_map(ruler))
.chain(rulers.iter().filter_map(ruler))
// shortcut: the first 32 lines in reach are paired, raise it if busier crossings matter
.take(32)
.collect();
+52 -16
View File
@@ -94,6 +94,9 @@ enum State {
/// The point that snaps, moving with the pointer: `start`, or where a press on a path's
/// stroke or beside its edge took hold of the path ([`hold_point`]).
hold: Point,
/// The press grabbed the selection by an anchor (`start`): it snaps as
/// [`MoveSnap::by_anchor`] says.
by_anchor: bool,
began: bool,
/// Shift-pressed on this selected object: released without a drag, it leaves the
/// selection.
@@ -247,13 +250,24 @@ pub(crate) struct MoveSnap {
impl MoveSnap {
pub(crate) fn new(cx: &ToolContext) -> Self {
Self::gather(cx, Targets::for_move)
}
/// [`Self::new`] for a selection grabbed by one of its anchors: with Smart Guides, that anchor
/// also lands on the other art's paths and where they cross (#7411), as Direct Selection lands
/// an anchor it drags.
pub(crate) fn by_anchor(cx: &ToolContext) -> Self {
Self::gather(cx, Targets::for_move_by_anchor)
}
fn gather(cx: &ToolContext, for_move: fn(&ToolContext, &[NodeId]) -> Targets) -> Self {
let guides = cx.smart_guides && !cx.snap_to_grid;
let points = |exclude: &[NodeId]| if cx.snap_to_grid { None } else { Targets::snap_to_point(cx, exclude) };
Self {
bounds: selection_bounds(cx),
targets: guides.then(|| Targets::for_move(cx, &cx.selection.objects)),
targets: guides.then(|| for_move(cx, &cx.selection.objects)),
points: points(&cx.selection.objects),
copy_targets: guides.then(|| Targets::for_move(cx, &[])),
copy_targets: guides.then(|| for_move(cx, &[])),
copy_points: points(&[]),
rulers: (guides && cx.snap_to_point).then(|| Targets::rulers(cx)),
}
@@ -286,6 +300,9 @@ impl MoveSnap {
}
// The axes (x, y) Smart Guides already put in line with other art.
let mut taken = [false; 2];
// The grabbed anchor landed on a path or a crossing, which a ruler guide does not pull
// it off.
let mut on_path = false;
if let (Some(t), Some(b)) = (targets, self.bounds) {
// The point the selection was grabbed by, near another object's anchor or centre,
// lands on it, rather than the bounds lining up a little beside it (#955).
@@ -294,6 +311,13 @@ impl MoveSnap {
d = q - start;
guides = ov;
taken = [true; 2];
} else if let Some((q, ov)) = t.snap_onto_path(start + d, cx.snap_tol(), self.rulers.as_ref()) {
// Grabbed by an anchor ([`Self::by_anchor`]), it lands on a path near it, or where
// the path crosses another or a ruler guide, instead of lining the bounds up.
d = q - start;
guides = ov;
taken = [true; 2];
on_path = true;
} else {
let (adj, ov, axes) = t.snap_rects_axes(&[b + d], cx.snap_tol());
d += adj;
@@ -304,7 +328,7 @@ impl MoveSnap {
// A ruler guide within reach of the grabbed point wins over lining the bounds up; on an
// axis without one that Smart Guides left free, the bounds line up with the ruler guides
// (#5746), alongside the Smart Guides.
if let Some(t) = &self.rulers {
if let Some(t) = self.rulers.as_ref().filter(|_| !on_path) {
let (adj, ov, held) = t.snap_moved_to_rulers(start + d, self.bounds.map(|b| b + d), cx.snap_tol(), taken);
if adj != Vec2::ZERO {
d += adj;
@@ -681,6 +705,15 @@ impl SelectionTool {
})
}
/// The point a press `ev` grabs the selection by ([`Self::hover_point`]), and whether it is an
/// anchor. Nothing for other pointer events.
fn grabbed_at(cx: &ToolContext, ev: &PointerEvent) -> (Option<Point>, bool) {
match ev.kind {
PointerKind::Down => Self::hover_point(cx, ev.pos).map_or((None, false), |(q, label)| (Some(q), label == "anchor")),
_ => (None, false),
}
}
/// On a press, notes the text the pointer is loaded with ([`Self::loaded_at_press`]); a
/// release clears it.
fn note_loaded_at_press(&mut self, kind: PointerKind) {
@@ -913,7 +946,7 @@ impl Tool for SelectionTool {
let m = ev.mods;
// A press near a centre or an anchor grabs the object by it.
self.hover = None;
let grabbed = if matches!(ev.kind, PointerKind::Down) { Self::hover_point(cx, p).map(|(q, _)| q) } else { None };
let (grabbed, by_anchor) = Self::grabbed_at(cx, ev);
let grab = grabbed.unwrap_or(p);
if matches!(ev.kind, PointerKind::Move) && matches!(self.state, State::Idle) {
self.hover = Self::hover_point(cx, p);
@@ -939,7 +972,7 @@ impl Tool for SelectionTool {
{
let several = cx.selection.objects.len() > 1 || !cx.selection.guides.is_empty();
let (deselect, key) = if m.shift { (Some(id), None) } else { (None, (several && !m.alt).then_some(id)) };
self.state = State::Moving { start: p, press: p, hold: center, began: false, deselect, key, exclude: None };
self.state = State::Moving { start: p, press: p, hold: center, by_anchor: false, began: false, deselect, key, exclude: None };
return vec![];
}
// 3. Objects: Cmd/Ctrl-click selects the one under the selected one (cycling).
@@ -947,7 +980,8 @@ impl Tool for SelectionTool {
&& cx.select_behind
&& let Some(behind) = object_behind(cx, p)
{
self.state = State::Moving { start: p, press: p, hold: p, began: false, deselect: None, key: None, exclude: None };
self.state =
State::Moving { start: p, press: p, hold: p, by_anchor: false, began: false, deselect: None, key: None, exclude: None };
return vec![Action::Exec("select.set".into(), json!({ "ids": [behind.0] }))];
}
let (hit, scope) = match selected_source_hit(cx, p) {
@@ -969,17 +1003,17 @@ impl Tool for SelectionTool {
// Grabbed by no anchor or centre, a press on a path's stroke or beside its
// edge holds the path there, and that point of it snaps (#3701).
let hold = if grabbed.is_some() { grab } else { hold_point(cx, &h, p) };
self.state = State::Moving { start: grab, press: p, hold, began: false, deselect, key, exclude };
self.state = State::Moving { start: grab, press: p, hold, by_anchor, began: false, deselect, key, exclude };
out
}
None => {
self.press_nothing(cx, p, grab, m);
self.press_nothing(cx, p, grab, by_anchor, m);
vec![]
}
}
}
(PointerKind::Drag, State::Moving { start, press, hold, began, exclude, .. }) => {
self.drag_moving(cx, p, m, ev.backquote, (start, press, hold, began), exclude)
(PointerKind::Drag, State::Moving { start, press, hold, by_anchor, began, exclude, .. }) => {
self.drag_moving(cx, p, m, ev.backquote, (start, press, hold, by_anchor, began), exclude)
}
(PointerKind::Drag, State::Scaling { handle, bx, areas }) => {
self.last_backquote = ev.backquote;
@@ -1679,9 +1713,10 @@ impl SelectionTool {
/// A press at `p` that hits no object starts a marquee, except where a clipping mask hides
/// the art of a selected object (picked in the Layers panel, say): there, without Shift, it
/// grabs the selection by that art (at `grab`), as a press on its visible part does.
fn press_nothing(&mut self, cx: &ToolContext, p: Point, grab: Point, m: Mods) {
/// `by_anchor`: `grab` is an anchor of it.
fn press_nothing(&mut self, cx: &ToolContext, p: Point, grab: Point, by_anchor: bool, m: Mods) {
self.state = if !m.shift && selected_hidden_at(cx.doc, p, cx.selection_hit_options(), &cx.selection.objects).is_some() {
State::Moving { start: grab, press: p, hold: grab, began: false, deselect: None, key: None, exclude: None }
State::Moving { start: grab, press: p, hold: grab, by_anchor, began: false, deselect: None, key: None, exclude: None }
} else {
State::Marquee { start: p, cur: p, toggle: m.shift, enclosed: false, directional_inside: false }
};
@@ -1693,7 +1728,7 @@ impl SelectionTool {
p: Point,
m: Mods,
backquote: bool,
(start, press, hold, began): (Point, Point, Point, bool),
(start, press, hold, by_anchor, began): (Point, Point, Point, bool, bool),
exclude: Option<(NodeId, NodeId)>,
) -> Vec<Action> {
let mut out = vec![];
@@ -1701,6 +1736,7 @@ impl SelectionTool {
if p.distance(press) < Self::drag_threshold(cx) {
return out;
}
let gather = if by_anchor { MoveSnap::by_anchor } else { MoveSnap::new };
// A Shift-drag from a highlighted object of the Global Edit selects it and moves it
// with the selection: its snap targets are gathered for the selection with it.
self.moving = Some(match exclude {
@@ -1708,9 +1744,9 @@ impl SelectionTool {
out.push(Action::Exec("select.toggle".into(), json!({ "id": top.0 })));
let mut selection = cx.selection.clone();
selection.add(top);
MoveSnap::new(&ToolContext { selection: &selection, ..*cx })
gather(&ToolContext { selection: &selection, ..*cx })
}
None => MoveSnap::new(cx),
None => gather(cx),
});
out.push(Action::Begin(if m.alt { "Copy".into() } else { "Move".into() }));
}
@@ -1721,7 +1757,7 @@ impl SelectionTool {
}
// Shift has already constrained `d`: any change is a snap.
self.snapped = (d - unsnapped).hypot() > 1e-9;
self.state = State::Moving { start, press, hold, began: true, deselect: None, key: None, exclude: None };
self.state = State::Moving { start, press, hold, by_anchor, began: true, deselect: None, key: None, exclude: None };
self.measure = cx.measurement_labels.then(|| (p, cx.offset_label(d.x, d.y)));
let mut params = json!({ "matrix": matrix_json(Affine::translate(d)), "copy": m.alt });
if backquote {
+88
View File
@@ -171,6 +171,94 @@ fn a_move_grabbed_by_an_anchor_lands_it_on_another_anchor() {
assert!((200.0 + x - 350.3).abs() < 1e-9 && (200.0 + y - 250.0).abs() < 1e-9, "the corner lands at ({}, {})", 200.0 + x, 200.0 + y);
}
/// A document with the rectangle of [`doc_with_rect`] (100..200 square) and a straight path from
/// `a` to `b` for each pair, and the rectangle's id.
fn doc_with_rect_and_lines(lines: &[(Point, Point)]) -> (vectorcraft_doc::Document, NodeId) {
let (mut d, id) = doc_with_rect();
let l = d.layers[0].id;
for &(a, b) in lines {
let n = d.alloc_id();
let path = vectorcraft_geom::PathData::single(vectorcraft_geom::SubPath::polyline(&[a, b], false));
d.insert(Some(l), 1, vectorcraft_doc::Node::path(n, path, vectorcraft_doc::Appearance::default_art())).unwrap();
}
(d, id)
}
/// The rectangle of `d` grabbed at `(gx, gy)` (hovered first, so a press near an anchor grabs it)
/// and dragged to `to`, with Smart Guides on or off (`smart`): the point it was grabbed at after the
/// move, and the label shown.
fn grabbed_and_dragged(d: &vectorcraft_doc::Document, id: NodeId, smart: bool, (gx, gy): (f64, f64), to: (f64, f64)) -> (Point, Option<String>) {
let (none, mut picked, p) = (Selection::default(), Selection::default(), paint());
picked.add(id);
let (before, after) = (ToolContext { smart_guides: smart, ..cx(d, &none, &p) }, ToolContext { smart_guides: smart, ..cx(d, &picked, &p) });
let mut t = SelectionTool::default();
t.pointer(&before, &ev(PointerKind::Move, gx, gy));
t.pointer(&before, &ev(PointerKind::Down, gx, gy));
let a = t.pointer(&after, &ev(PointerKind::Drag, to.0, to.1));
let Some(Action::Preview(_, v)) = a.iter().find(|a| matches!(a, Action::Preview(..))) else { panic!("{a:?}") };
let start = SelectionTool::hover_point(&before, Point::new(gx, gy)).map_or(Point::new(gx, gy), |(q, _)| q);
let label = t.overlays(&after).iter().find_map(|o| match o {
Overlay::Label { text, .. } => Some(text.clone()),
_ => None,
});
(start + Vec2::new(v["matrix"][4].as_f64().unwrap(), v["matrix"][5].as_f64().unwrap()), label)
}
/// #7411: grabbed by an anchor and dropped near a point along another path, away from its anchors
/// and center, the anchor lands on the path ("path") with Smart Guides. A selection grabbed
/// elsewhere, or with Smart Guides off, does not land on the path.
#[test]
fn a_move_grabbed_by_an_anchor_lands_it_on_another_path() {
let (a, b) = (Point::new(300.0, 400.0), Point::new(500.0, 300.0));
let (d, id) = doc_with_rect_and_lines(&[(a, b)]);
let off_line = |q: Point| ((b - a).cross(q - a) / (b - a).hypot()).abs();
// Grab the rectangle by its bottom-right corner (200, 200) and drop it half a point beside
// the line, at (430, 335) on it.
let (corner, label) = grabbed_and_dragged(&d, id, true, (201.0, 201.0), (430.0, 335.5));
assert!(off_line(corner) < 1e-9, "the corner lands at {corner:?}, {} off the line", off_line(corner));
assert!((corner.x - 430.0).abs() < 0.5, "{corner:?}");
assert_eq!(label.as_deref(), Some("path"));
// Grabbed by its fill instead, the pointer stays half a point beside the line.
let (grip, _) = grabbed_and_dragged(&d, id, true, (150.0, 170.0), (430.0, 335.5));
assert!(off_line(grip) > 0.4, "the grip lands at {grip:?}");
// With Smart Guides off, the corner stays where it was dragged.
let (corner, _) = grabbed_and_dragged(&d, id, false, (201.0, 201.0), (430.0, 335.5));
assert!(off_line(corner) > 0.4, "the corner lands at {corner:?}");
}
/// #7411: a selection grabbed by an anchor lands it where two other paths cross ("intersect"), and
/// on another object's anchor near the path rather than on the path.
#[test]
fn a_move_grabbed_by_an_anchor_lands_it_where_paths_cross() {
let (d, id) =
doc_with_rect_and_lines(&[(Point::new(300.0, 400.0), Point::new(500.0, 300.0)), (Point::new(460.0, 250.0), Point::new(460.0, 420.0))]);
let (corner, label) = grabbed_and_dragged(&d, id, true, (201.0, 201.0), (460.4, 320.5));
assert!(corner.distance(Point::new(460.0, 320.0)) < 1e-9, "the corner lands at {corner:?}");
assert_eq!(label.as_deref(), Some("intersect"));
// The end of the upright path (460, 250) is an anchor: the corner lands on it.
let (corner, label) = grabbed_and_dragged(&d, id, true, (201.0, 201.0), (460.5, 251.0));
assert!(corner.distance(Point::new(460.0, 250.0)) < 1e-9, "the corner lands at {corner:?}");
assert_eq!(label.as_deref(), Some("anchor"));
}
/// #7411: a selection grabbed by an anchor lands it where another path crosses a ruler guide
/// ("intersect"), and a ruler guide running past does not pull it off a path it landed on.
#[test]
fn a_move_grabbed_by_an_anchor_lands_it_where_a_path_crosses_a_ruler_guide() {
let (a, b) = (Point::new(300.0, 400.0), Point::new(500.0, 300.0));
let (mut d, id) = doc_with_rect_and_lines(&[(a, b)]);
d.add_ruler_guide(&vectorcraft_doc::Guide::new(true, 440.0));
let (corner, label) = grabbed_and_dragged(&d, id, true, (201.0, 201.0), (440.5, 330.5));
assert!(corner.distance(Point::new(440.0, 330.0)) < 1e-9, "the corner lands at {corner:?}");
assert_eq!(label.as_deref(), Some("intersect"));
// Beyond reach of the crossing, the corner stays on the path with the guide 3.7 points beside
// it.
let off_line = |q: Point| ((b - a).cross(q - a) / (b - a).hypot()).abs();
let (corner, label) = grabbed_and_dragged(&d, id, true, (201.0, 201.0), (436.43, 332.12));
assert!(off_line(corner) < 1e-9, "the corner lands at {corner:?}");
assert_eq!(label.as_deref(), Some("path"));
}
/// #3716: a press a few pixels from an anchor grabs the object by that anchor, but the drag
/// threshold counts from where the pointer went down. A click that wobbles by a pixel selects
/// the object without moving it, instead of jumping the anchor onto the pointer.
+1 -1
View File
@@ -1205,7 +1205,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
Layer Options for several highlighted rows shows the first row's settings, and OK changes only the settings set in the dialog: each row keeps its own color, Template, Lock, Show, Print, Preview and Dim Images otherwise (`crates/ui-egui/src/dialogs/layer_options.rs` tests).
As VectorCraft's own rule, a layer color (`layer.setProps`, `layer.new`, `layer.newSublayer`) or a Live Paint Gap Preview Color (`livePaint.gapOptions`) given as a hex color with an alpha part (`#rrggbbaa` or `#rgba`) is an error and changes nothing, instead of being set without its alpha. Regression tests: `a_layer_color_with_an_alpha_part_is_refused` in `crates/engine/src/tests/layers.rs` and `a_gap_color_with_an_alpha_part_is_refused` in `crates/engine/src/tests/build.rs`.
Panel Options › Locate Selected Objects (beyond Illustrator, off by default): when the last selected object changes on the canvas or by a command, the panel opens the layers and groups around it and scrolls to its row, as Locate Object does, without highlighting the row; when Show Layers Only, a search or the filter leaves the object without a row, the panel opens no rows for it and scrolls as with the option off.
- **Drawing aids:** Smart Guides and snapping (moving, with the Selection, Group Selection or Direct Selection tool, and resizing by the bounding-box handles with the Selection or Free Transform tool, Shift and Alt too: the dragged corner or side lands on the other objects' anchors and centres ("anchor", "center") or lines up with their edges and centres (a line and "align"); with Smart Guides › Spacing Guides, a moved selection that lines up with nothing along an axis lands as far from its neighbour as two other objects of its row or column are apart, or halfway between its two neighbours, the equal gaps marked; with View › Snap to Grid on, a moved selection lands on the grid instead, by whichever of its edges is nearest a gridline on each axis (the center does not snap), #740, #1816; the Rotate, Scale, Shear and Reflect tools drag from the anchor they grab and snap the pointer the same way, so a corner dragged onto another object's anchor scales exactly onto it, #482; anchors dragged with Direct Selection: the one pressed on lands on other anchors, its own path's that stay put among them, on path segments or in line with them, the others following, Shift keeps 45° steps, and with Smart Guides off Snap to Point pulls it onto anchors; the Artboard tool snaps artboards, and their bleed, to other artboards, bleeds and art, the grid or whole pixels while moving, copying and resizing them, and the Transform panel shows and edits the active artboard's X/Y/W/H; drawn points, #506: hovering shows where the press goes, the point lands on the other objects' anchors, centres and paths ("anchor", "center", "path") or lines up with their anchors, edges and centres ("align"), the Pen's on the path being drawn too, near a construction guide through the anchor before it on that guide (Preferences › Smart Guides › Construction Guides and Angles), and with Shift kept to its 45° step or a square's diagonal it slides along that way into line; the targets are gathered once per document state, from the art in the window, and the anchors, centres and lines near the pointer are found by bisection, so hovering never rescans the document), guides dragged out of the rulers (#451: they snap as moved guides do, Shift to the ruler's ticks; with the Artboard tool they are artboard guides, which run across their artboard only and move, copy and delete with it, `guide.add {artboard}`) that the Selection, Direct Selection and Group Selection tools then pick and drag (#414: the selected guides highlight and move together, Alt copies them, Shift snaps the dragged one to the ticks, otherwise it snaps to pixels, the grid or the edges, centres (so the sides' midpoints) and anchors of the art and the artboards, or with Smart Guides off Snap to Point pulls it onto anchors; Shift-clicked they join the selected art, and Shift-added art keeps them, so guides and art are selected together in either order, Delete then deleting both in one step; the arrow keys nudge them, Delete deletes them, dragging one back onto its ruler deletes it, Lock Guides and Hide Guides leave them alone; `guide.list`, `guide.select`, `guide.move` and `guide.remove` reach agents), a right-click on either ruler or the origin box to pick the document's units (the current one checked; `document.setUnits`, one undo step, #528), and Draw Normal / Behind / Inside modes. Smart Guides: a point drawn with the Pen or a shape tool, and an anchor dragged with Direct Selection, snaps to where paths and ruler guides cross, labelled "intersect" (#1658, part of #1602; a moved selection's own anchors don't). With Smart Guides › Measurement Labels on, the Line Segment tool labels the segment being drawn with its distance, angle and X and Y offsets (`D`, `Angle`, `dX`, `dY`), and the Pen, Curvature and Smart Pen tools label their rubber band the same way when Enable Rubber Band is on (#2983).
- **Drawing aids:** Smart Guides and snapping (moving, with the Selection, Group Selection or Direct Selection tool, and resizing by the bounding-box handles with the Selection or Free Transform tool, Shift and Alt too: the dragged corner or side lands on the other objects' anchors and centres ("anchor", "center") or lines up with their edges and centres (a line and "align"); with Smart Guides › Spacing Guides, a moved selection that lines up with nothing along an axis lands as far from its neighbour as two other objects of its row or column are apart, or halfway between its two neighbours, the equal gaps marked; with View › Snap to Grid on, a moved selection lands on the grid instead, by whichever of its edges is nearest a gridline on each axis (the center does not snap), #740, #1816; the Rotate, Scale, Shear and Reflect tools drag from the anchor they grab and snap the pointer the same way, so a corner dragged onto another object's anchor scales exactly onto it, #482; anchors dragged with Direct Selection: the one pressed on lands on other anchors, its own path's that stay put among them, on path segments or in line with them, the others following, Shift keeps 45° steps, and with Smart Guides off Snap to Point pulls it onto anchors; the Artboard tool snaps artboards, and their bleed, to other artboards, bleeds and art, the grid or whole pixels while moving, copying and resizing them, and the Transform panel shows and edits the active artboard's X/Y/W/H; drawn points, #506: hovering shows where the press goes, the point lands on the other objects' anchors, centres and paths ("anchor", "center", "path") or lines up with their anchors, edges and centres ("align"), the Pen's on the path being drawn too, near a construction guide through the anchor before it on that guide (Preferences › Smart Guides › Construction Guides and Angles), and with Shift kept to its 45° step or a square's diagonal it slides along that way into line; the targets are gathered once per document state, from the art in the window, and the anchors, centres and lines near the pointer are found by bisection, so hovering never rescans the document), guides dragged out of the rulers (#451: they snap as moved guides do, Shift to the ruler's ticks; with the Artboard tool they are artboard guides, which run across their artboard only and move, copy and delete with it, `guide.add {artboard}`) that the Selection, Direct Selection and Group Selection tools then pick and drag (#414: the selected guides highlight and move together, Alt copies them, Shift snaps the dragged one to the ticks, otherwise it snaps to pixels, the grid or the edges, centres (so the sides' midpoints) and anchors of the art and the artboards, or with Smart Guides off Snap to Point pulls it onto anchors; Shift-clicked they join the selected art, and Shift-added art keeps them, so guides and art are selected together in either order, Delete then deleting both in one step; the arrow keys nudge them, Delete deletes them, dragging one back onto its ruler deletes it, Lock Guides and Hide Guides leave them alone; `guide.list`, `guide.select`, `guide.move` and `guide.remove` reach agents), a right-click on either ruler or the origin box to pick the document's units (the current one checked; `document.setUnits`, one undo step, #528), and Draw Normal / Behind / Inside modes. Smart Guides: a point drawn with the Pen or a shape tool, and an anchor dragged with Direct Selection, snaps to where paths and ruler guides cross, labelled "intersect" (#1658, part of #1602; a moved selection's own anchors don't, except the anchor the Selection tool grabbed it by). A selection the Selection tool grabs by one of its anchors snaps that anchor onto the other objects' paths ("path"), and where a path crosses another path or a ruler guide ("intersect"), in the order Direct Selection uses for a dragged anchor; when the anchor lands on a path, the selection's bounds do not also line up with other objects (#7411). The archived 2007 [user guide](https://help.adobe.com/archive/en_US/illustrator/cs3/illustrator_cs3_help.pdf#page=55) (page 49) says Smart Guides align the pointer of a moved object to existing paths. Snapping onto paths only when the selection was grabbed by an anchor is VectorCraft's own rule; a selection grabbed anywhere else still lines its bounds up. Tests: `select::tests::a_move_grabbed_by_an_anchor_lands_it_on_another_path`, `a_move_grabbed_by_an_anchor_lands_it_where_paths_cross` and `a_move_grabbed_by_an_anchor_lands_it_where_a_path_crosses_a_ruler_guide`. With Smart Guides › Measurement Labels on, the Line Segment tool labels the segment being drawn with its distance, angle and X and Y offsets (`D`, `Angle`, `dX`, `dY`), and the Pen, Curvature and Smart Pen tools label their rubber band the same way when Enable Rubber Band is on (#2983).
A move that Shift keeps level, upright or diagonal stays on that line when it snaps, with the Selection, Group Selection or Direct Selection tool. It slides along the line until the selection lands on the grid or on whole pixels, lines up with the other objects or is spaced evenly with them, or until the line passes an anchor or crosses a ruler guide near the point the selection was grabbed by. A level or upright move snaps along its own axis only.
Snap to Last Location: with Smart Guides on and Preferences › Smart Guides › Snap to Last Location on (off by default, VectorCraft's own choice), a selection moved by dragging with the Selection, Group Selection or Direct Selection tool, Shift too, goes back to where the move began while it is within Snapping Tolerance of it, labeled "last location"; an Alt copy is not pulled back onto its original, and with View › Snap to Grid or Snap to Pixel on the move snaps to the grid or to whole pixels instead (`prefs.set` key `snapToLastLocation`). The public [Smart Guides options](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/smart-guides-options.html) page lists the option as snapping objects back to their previous position; the distance, the label, the copy rule and the grid and pixel rule are VectorCraft's own. Tests: `select::tests::snap_to_last_location_puts_a_move_back_where_it_began`, `select::tests::snap_to_pixel_wins_over_snap_to_last_location`, `direct::tests::snap_to_last_location_puts_a_path_moved_by_its_anchor_back`, `direct::tests::snap_to_pixel_wins_over_snap_to_last_location_for_a_path_moved_by_its_anchor`, `tests_prefs::snap_to_last_location_puts_a_dragged_square_back`.
Pixel Preview: with View › Pixel Preview on, the tools use no Smart Guides. Nothing snaps to Smart Guides or shows their guides, and Snap to Point acts as it does with Smart Guides off. View › Smart Guides keeps its setting and applies again when Pixel Preview is turned off. [Work with Smart Guides](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/work-with-smart-guides.html) says Smart Guides cannot be used while Pixel Preview is on. Tests: `tests::smartguides::pixel_preview_turns_smart_guides_off`, `canvas::tests::pixel_preview_stops_smart_guides_snapping_a_move`.