mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 03:06:57 -06:00
Fix #322: dragged direction handles take Shift and smart guides
Dragging a direction handle with Direct Selection or the Anchor Point tool sent the pointer position straight to path.setHandle: Shift did nothing and the handle never snapped, while the Pen already constrains the handles it pulls out. guides::snap_handle places a dragged handle: with Shift at a multiple of 45° from its anchor (the Pen's constraint, keeping the dragged length), otherwise snapped as a drawn point is (smart guides, the grid, pixels). Both tools use it and show the smart guides while the handle moves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
820f405c61
commit
86de124cc8
@@ -77,11 +77,13 @@ pub struct DirectSelectionTool {
|
||||
/// The spine point last clicked (blend, anchor): its handles show and can be dragged.
|
||||
spine: Option<(NodeId, usize)>,
|
||||
mesh: MeshEdit,
|
||||
/// Smart guides of the handle being dragged.
|
||||
guides: Vec<Overlay>,
|
||||
}
|
||||
|
||||
impl DirectSelectionTool {
|
||||
pub fn new(group: bool) -> Self {
|
||||
Self { group, state: State::Idle, spine: None, mesh: MeshEdit::default() }
|
||||
Self { group, state: State::Idle, spine: None, mesh: MeshEdit::default(), guides: vec![] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,9 +322,11 @@ impl Tool for DirectSelectionTool {
|
||||
out
|
||||
}
|
||||
(PointerKind::Drag, State::Handle { id, si, ai, out }) => {
|
||||
let (q, guides) = crate::guides::snap_handle(cx, (id, si, ai), p, ev.mods.shift);
|
||||
self.guides = guides;
|
||||
vec![Action::Preview(
|
||||
"path.setHandle".into(),
|
||||
json!({"id": id.0, "subpath": si, "anchor": ai, "which": if out {"out"} else {"in"}, "x": p.x, "y": p.y, "independent": ev.mods.alt}),
|
||||
json!({"id": id.0, "subpath": si, "anchor": ai, "which": if out {"out"} else {"in"}, "x": q.x, "y": q.y, "independent": ev.mods.alt}),
|
||||
)]
|
||||
}
|
||||
(PointerKind::Drag, State::SpinePoint { id, anchor, from, start, began }) => {
|
||||
@@ -369,6 +373,7 @@ impl Tool for DirectSelectionTool {
|
||||
}
|
||||
(PointerKind::Up, State::Handle { .. } | State::SpineHandle { .. }) => {
|
||||
self.state = State::Idle;
|
||||
self.guides.clear();
|
||||
vec![Action::Commit]
|
||||
}
|
||||
(PointerKind::Up, State::Marquee { start, add, .. }) => {
|
||||
@@ -398,6 +403,7 @@ impl Tool for DirectSelectionTool {
|
||||
match &self.state {
|
||||
State::Marquee { start, cur, .. } => vec![Overlay::Marquee(Rect::from_points(*start, *cur))],
|
||||
State::Corner(c) => c.overlays(cx),
|
||||
State::Handle { .. } => self.guides.clone(),
|
||||
_ => {
|
||||
let mut out = self.spine_overlays(cx);
|
||||
out.extend(self.mesh.overlays(cx));
|
||||
|
||||
@@ -11,7 +11,7 @@ use vectorcraft_doc::NodeId;
|
||||
use vectorcraft_geom::Point;
|
||||
|
||||
use super::{hit_anchor, hit_segment, insert_anchor, remove_anchor};
|
||||
use crate::{Action, Cursor, Mods, PointerEvent, PointerKind, Tool, ToolContext};
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum State {
|
||||
@@ -24,6 +24,8 @@ enum State {
|
||||
pub struct AnchorTool {
|
||||
id: &'static str,
|
||||
state: State,
|
||||
/// Smart guides of the handle being dragged.
|
||||
guides: Vec<Overlay>,
|
||||
}
|
||||
|
||||
impl AnchorTool {
|
||||
@@ -34,7 +36,7 @@ impl AnchorTool {
|
||||
"scissors" => "scissors",
|
||||
_ => "addAnchor",
|
||||
};
|
||||
Self { id, state: State::Idle }
|
||||
Self { id, state: State::Idle, guides: vec![] }
|
||||
}
|
||||
|
||||
fn add(cx: &ToolContext, p: Point) -> Vec<Action> {
|
||||
@@ -128,10 +130,14 @@ impl Tool for AnchorTool {
|
||||
"path.convertAnchor".into(),
|
||||
json!({"id": id.0, "subpath": si, "anchor": ai, "to": "smooth", "x": p.x, "y": p.y}),
|
||||
),
|
||||
State::Handle { id, si, ai, out, .. } => Action::Preview(
|
||||
"path.setHandle".into(),
|
||||
json!({"id": id.0, "subpath": si, "anchor": ai, "which": if out { "out" } else { "in" }, "x": p.x, "y": p.y, "independent": true}),
|
||||
),
|
||||
State::Handle { id, si, ai, out, .. } => {
|
||||
let (q, guides) = crate::guides::snap_handle(cx, (id, si, ai), p, ev.mods.shift);
|
||||
self.guides = guides;
|
||||
Action::Preview(
|
||||
"path.setHandle".into(),
|
||||
json!({"id": id.0, "subpath": si, "anchor": ai, "which": if out { "out" } else { "in" }, "x": q.x, "y": q.y, "independent": true}),
|
||||
)
|
||||
}
|
||||
State::Reshape { id, si, seg, t, start, .. } => Action::Preview(
|
||||
"path.reshapeSegment".into(),
|
||||
json!({"id": id.0, "subpath": si, "segment": seg, "t": t, "dx": p.x - start.x, "dy": p.y - start.y}),
|
||||
@@ -141,6 +147,7 @@ impl Tool for AnchorTool {
|
||||
out
|
||||
}
|
||||
("anchorPoint", PointerKind::Up) => {
|
||||
self.guides.clear();
|
||||
let st = std::mem::replace(&mut self.state, State::Idle);
|
||||
match st {
|
||||
State::Convert { began: true, .. } | State::Handle { began: true, .. } | State::Reshape { began: true, .. } => {
|
||||
@@ -172,6 +179,9 @@ impl Tool for AnchorTool {
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
|
||||
if matches!(self.state, State::Handle { .. }) { self.guides.clone() } else { vec![] }
|
||||
}
|
||||
fn cursor(&self, cx: &ToolContext, p: Point, m: Mods) -> Cursor {
|
||||
match self.id {
|
||||
"addAnchor" if !m.alt => Cursor::PenAdd,
|
||||
|
||||
@@ -171,6 +171,19 @@ pub fn snap_draw(cx: &ToolContext, p: Point, exclude: &[NodeId]) -> (Point, Vec<
|
||||
Targets::collect(cx.doc, exclude, None).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).
|
||||
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![]);
|
||||
}
|
||||
}
|
||||
snap_draw(cx, p, &[])
|
||||
}
|
||||
|
||||
/// Snap a picked point (a transform tool's reference point) to the nearest anchor or centre of the
|
||||
/// selection or of the object under the pointer, when Snap to Point or Smart Guides is on.
|
||||
pub fn snap_pick(cx: &ToolContext, p: Point) -> (Point, Vec<Overlay>) {
|
||||
|
||||
Reference in New Issue
Block a user