From bef928bf173ba1e8f10ebc7a42229ab5f8d2cea2 Mon Sep 17 00:00:00 2001 From: theironchef <6278036+theironchef@users.noreply.github.com> Date: Wed, 7 Oct 2026 03:38:33 -0700 Subject: [PATCH 1/2] Fix #309: Pen: Alt pressed mid-drag keeps the curve into the new anchor Pressing Alt while dragging out a smooth point set its incoming handle to the anchor itself, so the segment just shaped turned straight. The Pen now remembers the incoming handle it last previewed and keeps it while Alt is held, so only the outgoing handle follows the pointer: a corner with curves on both sides, as in the reference app. Alt held from the start of the drag still gives an outgoing handle only. Co-Authored-By: Claude Opus 5.5 --- crates/tools/src/pen.rs | 45 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/crates/tools/src/pen.rs b/crates/tools/src/pen.rs index da8371142..51daa7bbf 100644 --- a/crates/tools/src/pen.rs +++ b/crates/tools/src/pen.rs @@ -1,7 +1,8 @@ //! The Pen tool (P). //! -//! Click adds a corner anchor; click-drag adds a smooth anchor with symmetric handles (Alt-drag -//! breaks the handles); Shift constrains to 45°. Clicking the first anchor closes the path. +//! 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); Shift constrains to 45°. Clicking the first anchor closes the path. //! Enter/Esc (or switching tools) ends the path. Clicking the end of a selected open path continues //! it. The rubber-band preview shows the next segment. On a selected blend's spine a click adds a //! point (on a point no key object sits on: deletes it). @@ -18,6 +19,8 @@ pub struct PenTool { drawing: bool, drag: Option<(Point, bool)>, hover: Option, + /// The incoming handle of the anchor being dragged out, as last previewed. + in_h: Point, } /// The open path the pen is extending: the single selected open path. @@ -67,6 +70,7 @@ impl Tool for PenTool { return vec![Action::Begin("Close Path".into()), Action::Preview("path.close".into(), json!({"id": id.0}))]; } self.drag = Some((p, false)); + self.in_h = p; return vec![Action::Begin("Pen".into()), Action::Preview("path.appendAnchor".into(), json!({"id": id.0, "x": p.x, "y": p.y}))]; } if let Some(acts) = spine_click(cx, p, tol) { @@ -107,7 +111,10 @@ impl Tool for PenTool { json!({"id": id.0, "in": [2.0 * a.x - out_h.x, 2.0 * a.y - out_h.y], "independent": alt}), )]; } - let in_h = if alt { a } else { a - (out_h - a) }; + // 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) }; + self.in_h = in_h; vec![Action::Preview( "path.appendAnchor".into(), json!({"id": id.0, "x": a.x, "y": a.y, "in": [in_h.x, in_h.y], "out": [out_h.x, out_h.y]}), @@ -210,6 +217,38 @@ mod tests { assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 10.0, 10.0)), vec![Action::Commit]); } + #[test] + fn alt_pressed_mid_drag_keeps_the_incoming_handle() { + 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 alt = Mods { alt: true, ..Mods::default() }; + let handles = |acts: Vec| 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]))); + // Alt goes down: the curve shaped so far stays, and only the outgoing handle moves on. + let drag = PointerEvent::new(PointerKind::Drag, 450.0, 470.0).with_mods(alt); + assert_eq!(handles(t.pointer(&cx, &drag)), (json!([350.0, 420.0]), json!([450.0, 470.0]))); + let drag = PointerEvent::new(PointerKind::Drag, 400.0, 480.0).with_mods(alt); + assert_eq!(handles(t.pointer(&cx, &drag)), (json!([350.0, 420.0]), json!([400.0, 480.0]))); + assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 400.0, 480.0).with_mods(alt)), vec![Action::Commit]); + // Alt held from the start: the new anchor gets an outgoing handle only. + t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 300.0, 440.0).with_mods(alt)); + let drag = PointerEvent::new(PointerKind::Drag, 340.0, 440.0).with_mods(alt); + assert_eq!(handles(t.pointer(&cx, &drag)), (json!([300.0, 440.0]), json!([340.0, 440.0]))); + } + #[test] fn clicking_a_selected_blend_spine_adds_a_point() { let (mut d, _) = doc_with_rect(); From 464a93c7f9415f7bdc9695810e9401bbdf213a87 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Thu, 8 Oct 2026 01:40:52 +0700 Subject: [PATCH 2/2] Pen: ROADMAP entry for Alt pressed mid-drag (review of #310) Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index b9517f82c..e4f5a7d4c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,7 +4,7 @@ VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adob This file tracks **how far we are and what's left**. Time estimates are wall-clock hours of continuous Claude Opus 5.5 agent work (including builds and the CI gate), given both for **one agent** and for **4–6 parallel agents** on disjoint crates. They are counted from the remaining work (see [Parity estimate](#parity-estimate)), calibrated against measured throughput, and updated as work lands. -_Last updated: 2026-10-07 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes and the Layers panel rework; see [Honest assessment](#honest-assessment-2026-10-05))._ +_Last updated: 2026-10-08 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the Pen and shape-tool modifier fixes; see [Honest assessment](#honest-assessment-2026-10-05))._ ## Where we are @@ -109,6 +109,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist - **Cutting:** Eraser, Scissors, Knife, Mirror & Cut, Line Cut and Rectangle Cut (real geometry, compound paths keep their holes). - **Transform:** Rotate, Reflect, Scale, Shear (click or Alt-click snaps the reference point to anchors and centres), Reshape, Free Transform (free and perspective distort and shear, switched by the keys held during a handle drag). - **Live Corners:** drag a live rectangle's corner widgets (Selection or Direct Selection) to round all corners, with a radius readout. Corners stay circular when the rectangle is scaled unevenly or along its own sides when rotated (the radius keeps its size, or scales by the mean scale with Scale Corners on); only a shear distorts them. + - **Pen:** Alt pressed in the middle of dragging out an anchor splits its handles and keeps the curve already shaped into it (only the outgoing handle follows the pointer). - **Graphs:** Column, Stacked Column, Bar, Stacked Bar, Line, Area, Scatter, Pie and Radar graph tools with Graph Data and Graph Type. - **Other:** Eyedropper, Gradient annotator, Artboard, Measure, Type, Hand, Zoom, Rotate View. - **Editing type:** double-clicking type with a selection tool switches to the Type tool with the caret where it was clicked.