Direct Selection: handle snapping ignores the path's moving bounds, Shift follows the Constrain Angle (review of #332)
- A dragged handle no longer snaps to its own path's bounds and centre, which move with the handle; the path's anchors are point and alignment targets instead. - Shift steps 45° from the Constrain Angle preference, like the cut and gradient tools. - snap_draw and snap_handle share one pixel/grid/smart-guides switch. - A test that a smooth anchor's opposite handle turns with a Shift-dragged handle and that Alt still breaks it, and the tools' module docs (both from #338). Co-authored-by: theironchef <6278036+theironchef@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
86de124cc8
commit
a1f2f23b72
|
|
@ -449,3 +449,34 @@ fn a_dragged_handle_snaps_to_smart_guides() {
|
|||
assert_eq!(out_handle(&s, id), Point::new(298.0, 170.0), "{tool}: smart guides off");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_drag_keeps_a_smooth_anchor_smooth_and_alt_breaks_it() {
|
||||
// #322: the opposite handle of a smooth anchor turns with the constrained one; Alt still
|
||||
// moves the dragged handle alone.
|
||||
let shift = Mods { shift: true, ..Mods::default() };
|
||||
let shift_alt = Mods { shift: true, alt: true, ..Mods::default() };
|
||||
for (mods, opposite_follows) in [(shift, true), (shift_alt, false)] {
|
||||
let mut s = session();
|
||||
let anchors = json!([{"x": 100, "y": 200}, {"x": 200, "y": 100, "in": [150, 100], "out": [250, 100]}, {"x": 300, "y": 200}]);
|
||||
let id = NodeId(s.execute("path.create", &json!({"anchors": anchors})).unwrap()["id"].as_u64().unwrap());
|
||||
s.select_tool("directSelection", view()).unwrap();
|
||||
for (kind, x, y, m) in [
|
||||
(PointerKind::Down, 200.0, 100.0, Mods::default()),
|
||||
(PointerKind::Up, 200.0, 100.0, Mods::default()),
|
||||
(PointerKind::Down, 250.0, 100.0, mods),
|
||||
(PointerKind::Drag, 275.0, 165.0, mods),
|
||||
(PointerKind::Up, 275.0, 165.0, mods),
|
||||
] {
|
||||
s.pointer(&PointerEvent::new(kind, x, y).with_mods(m), view()).unwrap();
|
||||
}
|
||||
let a = path(&s, id).subpaths[0].anchors[1];
|
||||
let (o, i) = (a.h_out - a.p, a.h_in - a.p);
|
||||
assert!((o.x - o.y).abs() < 1e-6 && o.x > 0.0, "{mods:?}: 45°: {o:?}");
|
||||
if opposite_follows {
|
||||
assert!(near(a.h_in, a.p - o.normalize() * 50.0), "{mods:?}: {i:?}");
|
||||
} else {
|
||||
assert_eq!(a.h_in, Point::new(150.0, 100.0), "{mods:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
//!
|
||||
//! Direct Selection: click an anchor to select it (Shift toggles), click a segment to select the
|
||||
//! path's anchors on that segment, drag to move selected anchors, drag a direction handle to
|
||||
//! reshape, marquee to select anchors, drag a live rectangle's corner widget to round its corners.
|
||||
//! reshape (Shift keeps it at 45° steps round its anchor, Alt moves it alone; smart guides snap
|
||||
//! it), marquee to select anchors, drag a live rectangle's corner widget to round its corners.
|
||||
//! Group Selection: click selects the leaf; each further click on it adds the next enclosing group.
|
||||
//! Both pick the key objects of a blend. Direct Selection also edits a blend's spine: drag its
|
||||
//! points (a key object on a point moves with it) and, once a point is clicked, its handles; and
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
//! Add: click a segment to insert an anchor without changing the shape (Alt = delete).
|
||||
//! Delete: click an anchor to remove it, re-fitting the neighbouring curve (Alt = add).
|
||||
//! Anchor Point: click a smooth anchor → corner; drag an anchor → pull out smooth handles; drag a
|
||||
//! handle → move it independently; drag a segment → reshape the curve.
|
||||
//! handle → move it independently (Shift: at 45° steps round its anchor); drag a segment → reshape
|
||||
//! the curve.
|
||||
//! Scissors: click a path to split it there.
|
||||
|
||||
use serde_json::json;
|
||||
|
|
|
|||
|
|
@ -158,6 +158,11 @@ impl Targets {
|
|||
|
||||
/// Snap `p` for a drawing tool when smart guides (or grid snapping) are on.
|
||||
pub fn snap_draw(cx: &ToolContext, p: Point, exclude: &[NodeId]) -> (Point, Vec<Overlay>) {
|
||||
snap_with(cx, p, || Targets::collect(cx.doc, exclude, None))
|
||||
}
|
||||
|
||||
/// Snap `p` to pixels or the grid when they are on, else to the smart guide `targets`.
|
||||
fn snap_with(cx: &ToolContext, p: Point, targets: impl FnOnce() -> Targets) -> (Point, Vec<Overlay>) {
|
||||
if cx.snap_to_pixel {
|
||||
return (Point::new(p.x.round(), p.y.round()), vec![]);
|
||||
}
|
||||
|
|
@ -168,20 +173,28 @@ pub fn snap_draw(cx: &ToolContext, p: Point, exclude: &[NodeId]) -> (Point, Vec<
|
|||
if !cx.smart_guides {
|
||||
return (p, vec![]);
|
||||
}
|
||||
Targets::collect(cx.doc, exclude, None).snap_point(p, cx.tol(5.0))
|
||||
targets().snap_point(p, cx.tol(5.0))
|
||||
}
|
||||
|
||||
/// Where a dragged direction handle of anchor `ai` of subpath `si` of path `id` goes for the
|
||||
/// pointer at `p`: with Shift at a multiple of 45° from its anchor (as the Pen draws them), else
|
||||
/// snapped as a drawn point is (smart guides, the grid, pixels).
|
||||
/// pointer at `p`: with Shift at a multiple of 45° (from the Constrain Angle) round its anchor,
|
||||
/// else snapped as a drawn point is (smart guides, the grid, pixels).
|
||||
pub fn snap_handle(cx: &ToolContext, (id, si, ai): (NodeId, usize, usize), p: Point, shift: bool) -> (Point, Vec<Overlay>) {
|
||||
if shift {
|
||||
let anchor = cx.doc.node(id).and_then(|n| n.path_data()).and_then(|pd| pd.subpaths.get(si)?.anchors.get(ai).map(|a| a.p));
|
||||
if let Some(a) = anchor {
|
||||
return (a + vectorcraft_geom::constrain_angle(p - a, 45.0), vec![]);
|
||||
}
|
||||
let path = cx.doc.node(id).and_then(|n| n.path_data());
|
||||
if shift && let Some(a) = path.and_then(|pd| pd.subpaths.get(si)?.anchors.get(ai)) {
|
||||
return (a.p + vectorcraft_geom::constrain_angle_from(p - a.p, 45.0, cx.constrain_angle), vec![]);
|
||||
}
|
||||
snap_draw(cx, p, &[])
|
||||
snap_with(cx, p, || {
|
||||
// The path's bounds move with the handle, so they would chase it: its anchors (which stay
|
||||
// put) are the targets instead, the handle lining up with them too.
|
||||
let mut t = Targets::collect(cx.doc, &[id], None);
|
||||
for (_, _, a) in path.into_iter().flat_map(|pd| pd.anchors()).take(20_000) {
|
||||
t.points.push((a.p, Kind::Anchor));
|
||||
t.xs.push((a.p.x, a.p, Kind::Anchor));
|
||||
t.ys.push((a.p.y, a.p, Kind::Anchor));
|
||||
}
|
||||
t
|
||||
})
|
||||
}
|
||||
|
||||
/// Snap a picked point (a transform tool's reference point) to the nearest anchor or centre of the
|
||||
|
|
|
|||
Loading…
Reference in New Issue