Browse / github.com/storytold/vectorcraft / patch
G11: Pen Ctrl/Cmd keeps collinear unequal handles while dragging
Pen: Ctrl-drag new anchor should keep handles collinear while adjusting outgoing length ## Report While drawing with the Pen tool, a user reports that Ctrl-dragging a newly created anchor does not preserve collinearity of the two direction handles while changing only the outgoing handle length. They can achieve the result only after ending the drawing gesture and using Direct Selection. ## Expected During the in-progress Pen anchor drag, Ctrl should retain the opposing handle direction and permit adjustment of the newest/outgoing handle length. ## Current The requested Ctrl-modified drawing behavior is not available during the initial Pen drag. Existing behavior documents Al
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- nrq6993-a11y (imported from pull request #4259, open)
- Base
1a84e180979eada1c7bedbc0728b566298bf2f6a- Size
- +60 −3 in 1 file
- Review
- pending
- Shared
- 2026-10-28
- Id
04bc8db35872e9cec2b66d03
How it went
No other operator has reported using it yet.
Diff
raw · crates/tools/src/pen.rs
@@ -2,7 +2,8 @@ //! //! Click adds a corner anchor; click-drag adds a smooth anchor with symmetric handles (Alt breaks //! them: the incoming handle stays where it was when Alt went down and only the outgoing one-//! follows the pointer; Space held moves the anchor, handles and all); Shift constrains to 45°.+//! follows the pointer; Ctrl/Cmd keeps the handles collinear but unequal in length; Space held+//! moves the anchor, handles and all); Shift constrains to 45°. //! A press on the first anchor closes the path, the drag shaping the closing curve. Clicking the //! last one retracts its outgoing handle, so the next segment leaves it as a corner; dragging from //! it pulls a new one out on its own.@@ -142,6 +143,33 @@ fn pick_endpoint(first: Option<Point>, last: Point, p: Point, tol: f64, tie: End (None, None) => None, } }++/// Ctrl/Cmd while dragging a smooth anchor: collinear handles, unequal length (#3851).+fn pen_unequal_handles(m: Mods) -> bool {+ (m.cmd || m.ctrl) && !m.alt+}++fn smooth_handle_pair(a: Point, out_h: Point, stored_in: Point, alt: bool, unequal: bool) -> (Point, Point) {+ if alt {+ return (stored_in, out_h);+ }+ if unequal {+ let in_h = if stored_in.distance(a) > 1e-12 { stored_in } else { a - (out_h - a) };+ let dx = in_h.x - a.x;+ let dy = in_h.y - a.y;+ let len = (dx * dx + dy * dy).sqrt();+ if len > 1e-12 {+ let ux = dx / len;+ let uy = dy / len;+ let ox = out_h.x - a.x;+ let oy = out_h.y - a.y;+ let out_len = -(ox * ux + oy * uy);+ return (in_h, Point::new(a.x - ux * out_len, a.y - uy * out_len));+ }+ }+ (a - (out_h - a), out_h)+}+ /// Alt held over a handle end, an anchor or a segment of a selected path: the Anchor Point tool's /// gesture, within its tolerance. fn alt_converts(cx: &ToolContext, p: Point, m: Mods) -> bool {@@ -323,9 +351,11 @@ impl Tool for PenTool { out_h = a + vectorcraft_geom::constrain_angle_from(ev.pos - a, 45.0, cx.constrain_angle); } let alt = ev.mods.alt;+ let unequal = pen_unequal_handles(ev.mods); let Some(id) = path else { // First anchor of a new path: re-issue create with handles.- let in_h = a - (out_h - a);+ let (in_h, out_h) = smooth_handle_pair(a, out_h, self.in_h, alt, unequal);+ self.in_h = in_h; return vec![Action::Preview( "path.create".into(), json!({"anchors": [{"x": a.x, "y": a.y, "out": [out_h.x, out_h.y], "in": [in_h.x, in_h.y]}]}),@@ -339,7 +369,7 @@ impl Tool for PenTool { } // Alt pressed mid-drag keeps the curve already shaped into the anchor: only the // outgoing handle follows. Held from the start, the incoming handle stays retracted.- let in_h = if alt { self.in_h } else { a - (out_h - a) };+ let (in_h, out_h) = smooth_handle_pair(a, out_h, self.in_h, alt, unequal); self.in_h = in_h; vec![Action::Preview( "path.appendAnchor".into(),@@ -739,6 +769,33 @@ mod tests { assert_eq!(handles(t.pointer(&cx, &drag)), (json!([300.0, 440.0]), json!([340.0, 440.0]))); } + /// #3851: Ctrl/Cmd while dragging keeps smooth handles collinear but unequal in length.+ #[test]+ fn ctrl_pressed_mid_drag_keeps_collinear_unequal_handles() {+ let (mut d, _) = doc_with_rect();+ let l = d.layers[0].id;+ let id = d.alloc_id();+ let line = vectorcraft_geom::shapes::line(Point::new(10.0, 300.0), Point::new(60.0, 300.0));+ d.insert(Some(l), 1, vectorcraft_doc::Node::path(id, line, vectorcraft_doc::Appearance::default_art())).unwrap();+ let mut s = Selection::default();+ s.set([id]);+ let p = paint();+ let cx = cx(&d, &s, &p);+ let mut t = PenTool { drawing: true, ..PenTool::default() };+ let ctrl = Mods { ctrl: true, ..Mods::default() };+ let handles = |acts: Vec<Action>| match acts.as_slice() {+ [Action::Preview(c, v)] if c == "path.appendAnchor" => (v["in"].clone(), v["out"].clone()),+ other => panic!("not an anchor preview: {other:?}"),+ };+ t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 400.0, 420.0));+ let drag = PointerEvent::new(PointerKind::Drag, 450.0, 420.0);+ assert_eq!(handles(t.pointer(&cx, &drag)), (json!([350.0, 420.0]), json!([450.0, 420.0])));+ let drag = PointerEvent::new(PointerKind::Drag, 480.0, 420.0).with_mods(ctrl);+ assert_eq!(handles(t.pointer(&cx, &drag)), (json!([350.0, 420.0]), json!([480.0, 420.0])));+ let drag = PointerEvent::new(PointerKind::Drag, 480.0, 500.0).with_mods(ctrl);+ assert_eq!(handles(t.pointer(&cx, &drag)), (json!([350.0, 420.0]), json!([480.0, 420.0])));+ }+ #[test] fn space_pressed_inside_the_drag_threshold_moves_the_anchor_by_the_next_move_only() { let (d, s) = (vectorcraft_doc::Document::new(500.0, 500.0), Selection::default());