Snap to Point: drag a selection by the anchor pressed near
With Smart Guides off, the Selection tool grabbed the selection by the pressed spot, so Snap to Point brought the pointer onto another anchor while the object's own anchor stayed a few pixels off. A press within reach of an anchor now grabs the object by that anchor (Smart Guides on or off, also without a hover event first), and the arrow turns hollow over such an anchor while Snap to Point is on. Fixes #1274 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5cbb7989f9
commit
8b839dd50f
|
|
@ -825,3 +825,51 @@ fn hyphenation_exceptions_preference_reaches_the_hyphenator() {
|
|||
set_pref(&mut s, "hyphenationExceptions", json!(""));
|
||||
assert!(vectorcraft_text::hyphenation_exceptions().is_empty());
|
||||
}
|
||||
|
||||
/// #1274: View › Snap to Point lands the anchor a selection is grabbed by (a press within a few
|
||||
/// pixels of it) on another object's anchor, with Smart Guides on or off, not the pressed spot
|
||||
/// beside it.
|
||||
#[test]
|
||||
fn snap_to_point_lands_the_grabbed_anchor_on_another_anchor() {
|
||||
use vectorcraft_tools::PointerKind::{Down, Drag, Up};
|
||||
let mut s = new_doc();
|
||||
let a = square(&mut s, 100.0, 100.0);
|
||||
square(&mut s, 300.0, 100.0);
|
||||
for v in [ViewInfo { smart_guides: false, ..ViewInfo::default() }, ViewInfo::default()] {
|
||||
s.select_tool("selection", v).unwrap();
|
||||
// Nothing selected, so no bounding box handle sits on the corner.
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
// Pressed 2 px left of and below the top-right anchor (150, 100), released with the
|
||||
// pointer 1.4 px from the other square's top-left anchor (300, 100).
|
||||
gesture(&mut s, v, &[(Down, 148.0, 102.0), (Drag, 200.0, 110.0), (Drag, 299.0, 101.0), (Up, 299.0, 101.0)]);
|
||||
assert_eq!(top_left(&s, a), (250.0, 100.0), "the grabbed anchor lands on the anchor ({v:?})");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// #1274: with Smart Guides off, View › Snap to Point turns the selection arrow hollow over an
|
||||
/// anchor a press would grab, and lands a Pen click within 2 px of an anchor on it.
|
||||
#[test]
|
||||
fn snap_to_point_cues_anchors_and_lands_pen_clicks_on_them() {
|
||||
use vectorcraft_tools::PointerKind::{Down, Up};
|
||||
use vectorcraft_tools::{Cursor, Mods};
|
||||
let mut s = new_doc();
|
||||
square(&mut s, 300.0, 100.0);
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
let on = ViewInfo { smart_guides: false, ..ViewInfo::default() };
|
||||
let off = ViewInfo { snap_to_point: false, ..on };
|
||||
s.select_tool("selection", on).unwrap();
|
||||
let near = vectorcraft_geom::Point::new(302.0, 102.0);
|
||||
assert_eq!(s.cursor(near, Mods::default(), on), Cursor::ArrowHollow);
|
||||
assert_eq!(s.cursor(vectorcraft_geom::Point::new(325.0, 125.0), Mods::default(), on), Cursor::Arrow, "away from the anchors");
|
||||
assert_eq!(s.cursor(near, Mods::default(), off), Cursor::Arrow, "Snap to Point off");
|
||||
for (v, first) in [(on, (300.0, 100.0)), (off, (301.0, 101.0))] {
|
||||
s.select_tool("pen", v).unwrap();
|
||||
gesture(&mut s, v, &[(Down, 301.0, 101.0), (Up, 301.0, 101.0), (Down, 200.0, 250.0), (Up, 200.0, 250.0)]);
|
||||
let id = *s.doc().unwrap().selection.objects.first().unwrap();
|
||||
let p = s.doc().unwrap().doc.node(id).unwrap().path_data().unwrap().subpaths[0].anchors[0].p;
|
||||
assert_eq!((p.x, p.y), first, "{v:?}");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,9 +79,9 @@ pub struct SelectionTool {
|
|||
/// Snapping for the selection moved as a whole, its targets gathered when the move begins: View →
|
||||
/// Snap to Grid lands its bounds on the grid (#740), taking over from the rest; otherwise Smart
|
||||
/// Guides line its bounds up with the other art, and with them off, View → Snap to Point lands the
|
||||
/// point it was grabbed by on an anchor or a ruler guide. Snap to Pixel puts its top-left on whole
|
||||
/// pixels. A copy (Alt, which can go down or up at any time during the drag) leaves the original
|
||||
/// behind, so it snaps to the original too.
|
||||
/// point it was grabbed by (an anchor pressed near, else the pressed point) on an anchor or a ruler
|
||||
/// guide. Snap to Pixel puts its top-left on whole pixels. A copy (Alt, which can go down or up at
|
||||
/// any time during the drag) leaves the original behind, so it snaps to the original too.
|
||||
pub(crate) struct MoveSnap {
|
||||
bounds: Option<Rect>,
|
||||
targets: Option<Targets>,
|
||||
|
|
@ -194,11 +194,13 @@ impl SelectionTool {
|
|||
cx.tol(3.0)
|
||||
}
|
||||
|
||||
/// With Smart Guides on, the anchor or the centre of an object near `p` (the one under the
|
||||
/// pointer, else a selected one) and its label, "anchor" or "center", as Illustrator shows
|
||||
/// them while hovering (#812). A press there grabs the object by that point.
|
||||
/// The anchor of an object near `p` (the one under the pointer, else a selected one when a
|
||||
/// press would keep the selection), or with Smart Guides on its centre, and its label,
|
||||
/// "anchor" or "center", as Illustrator shows them while hovering with Smart Guides (#812). A
|
||||
/// press there grabs the object by that point, so with Smart Guides or View → Snap to Point
|
||||
/// that point is the one that snaps (#1274).
|
||||
fn hover_point(cx: &ToolContext, p: Point) -> Option<(Point, &'static str)> {
|
||||
if !cx.smart_guides {
|
||||
if !cx.smart_guides && !cx.snap_to_point {
|
||||
return None;
|
||||
}
|
||||
let reach = cx.tol(HOVER_REACH);
|
||||
|
|
@ -206,10 +208,15 @@ impl SelectionTool {
|
|||
let nearest = |points: &mut dyn Iterator<Item = Point>| {
|
||||
points.filter(|q| q.distance(p) <= reach).min_by(|a, b| a.distance(p).total_cmp(&b.distance(p)))
|
||||
};
|
||||
under.into_iter().chain(cx.selection.objects.iter().copied()).find_map(|id| {
|
||||
// A press on an unselected object selects it alone: the selected ones stay behind.
|
||||
let selected = if under.is_none_or(|u| cx.selection.contains(u)) { &cx.selection.objects[..] } else { &[] };
|
||||
under.into_iter().chain(selected.iter().copied()).find_map(|id| {
|
||||
let n = cx.doc.node(id)?;
|
||||
let anchor = n.path_data().and_then(|pd| nearest(&mut pd.subpaths.iter().flat_map(|sp| sp.anchors.iter().map(|a| a.p))));
|
||||
anchor.map(|q| (q, "anchor")).or_else(|| {
|
||||
if !cx.smart_guides {
|
||||
return None;
|
||||
}
|
||||
let c = cx.doc.bounds_of(&[id], cx.preview_bounds)?.center();
|
||||
(c.distance(p) <= reach).then_some((c, "center"))
|
||||
})
|
||||
|
|
@ -236,8 +243,9 @@ impl Tool for SelectionTool {
|
|||
}
|
||||
let p = ev.pos;
|
||||
let m = ev.mods;
|
||||
// A press near the hovered centre or anchor grabs the object by it.
|
||||
let grab = self.hover.take().map(|(q, _)| q).filter(|q| q.distance(p) <= cx.tol(HOVER_REACH)).unwrap_or(p);
|
||||
// A press near a centre or an anchor grabs the object by it.
|
||||
self.hover = None;
|
||||
let grab = if matches!(ev.kind, PointerKind::Down) { Self::hover_point(cx, p).map_or(p, |(q, _)| q) } else { p };
|
||||
if matches!(ev.kind, PointerKind::Move) && matches!(self.state, State::Idle) {
|
||||
self.hover = Self::hover_point(cx, p);
|
||||
}
|
||||
|
|
@ -450,7 +458,7 @@ impl Tool for SelectionTool {
|
|||
}
|
||||
o.extend(self.guides.iter().cloned());
|
||||
o.extend(self.guide.overlays(cx));
|
||||
if let (Some((q, label)), State::Idle) = (self.hover, &self.state) {
|
||||
if let (Some((q, label)), State::Idle, true) = (self.hover, &self.state, cx.smart_guides) {
|
||||
let style = crate::guides::GuideStyle::of(cx);
|
||||
if style.labels {
|
||||
o.push(Overlay::Label { p: q, text: label.into(), color: style.color });
|
||||
|
|
@ -495,6 +503,11 @@ impl Tool for SelectionTool {
|
|||
if let Some(c) = self.guide.cursor(cx, p) {
|
||||
return c;
|
||||
}
|
||||
// View → Snap to Point: over an anchor a press would grab the object by, the arrow turns
|
||||
// hollow.
|
||||
if cx.snap_to_point && matches!(Self::hover_point(cx, p), Some((_, "anchor"))) {
|
||||
return Cursor::ArrowHollow;
|
||||
}
|
||||
if let Some(h) = hit_test(cx.doc, p, cx.hit_options()) {
|
||||
if cx.selection.contains(h.top_object(cx.isolation)) || m.alt {
|
||||
return Cursor::Move;
|
||||
|
|
|
|||
Loading…
Reference in New Issue