From 77070d29a7368863cae7ff556530a20a7cfbf08c Mon Sep 17 00:00:00 2001 From: ncic <1848265+ncic@users.noreply.github.com> Date: Sat, 14 Nov 2026 09:22:18 +0100 Subject: [PATCH] Pencil: the continuation cursor follows the snapped release; Precise Cursors tests keep the badges --- crates/engine/src/tests/prefs.rs | 12 +++++---- crates/tools/src/draw2/gesture.rs | 14 ++++++++-- crates/tools/src/draw2/gesture/tests.rs | 34 +++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/crates/engine/src/tests/prefs.rs b/crates/engine/src/tests/prefs.rs index d88b8250a2..67a740d4a3 100644 --- a/crates/engine/src/tests/prefs.rs +++ b/crates/engine/src/tests/prefs.rs @@ -723,8 +723,10 @@ fn use_precise_cursors_swaps_the_tools_own_pointers_for_a_crosshair() { assert_eq!(cursor(&mut s, tool), Cursor::Crosshair, "{tool} off"); } s.execute("prefs.set", &json!({"key": "usePreciseCursors", "value": true})).unwrap(); - for (tool, _) in own { - assert_eq!(cursor(&mut s, tool), Cursor::Crosshair, "{tool} on"); + for (tool, c) in own { + // The Pencil's star is drawn on the crosshair and keeps its badge. + let want = if tool == "pencil" { c } else { Cursor::Crosshair }; + assert_eq!(cursor(&mut s, tool), want, "{tool} on"); } for tool in always { assert_eq!(cursor(&mut s, tool), Cursor::Crosshair, "{tool} on"); @@ -758,8 +760,8 @@ fn use_precise_cursors_keeps_the_smooth_tools_not_allowed_pointer() { } } -/// With Alt held while drawing, the Pencil and the Paintbrush show their closing cursors, and Use Precise Cursors -/// turns those into a crosshair too. +/// With Alt held while drawing, the Pencil and the Paintbrush show their closing cursors. Use Precise Cursors keeps +/// them: they are drawn on the crosshair, and the ring says the release closes the path. #[test] fn alt_shows_the_pencil_and_paintbrush_closing_cursors() { use vectorcraft_tools::{Cursor, Mods, PointerEvent, PointerKind}; @@ -773,7 +775,7 @@ fn alt_shows_the_pencil_and_paintbrush_closing_cursors() { s.select_tool(tool, ViewInfo::default()).unwrap(); s.pointer(&PointerEvent::new(PointerKind::Down, 250.0, 250.0), ViewInfo::default()).unwrap(); s.pointer(&PointerEvent::new(PointerKind::Drag, 300.0, 300.0), ViewInfo::default()).unwrap(); - assert_eq!(s.cursor(at, alt, ViewInfo::default()), if precise { Cursor::Crosshair } else { c }, "{tool}, precise {precise}"); + assert_eq!(s.cursor(at, alt, ViewInfo::default()), c, "{tool}, precise {precise}"); s.pointer(&PointerEvent::new(PointerKind::Up, 300.0, 300.0), ViewInfo::default()).unwrap(); } } diff --git a/crates/tools/src/draw2/gesture.rs b/crates/tools/src/draw2/gesture.rs index fc57b4d953..c3191c4363 100644 --- a/crates/tools/src/draw2/gesture.rs +++ b/crates/tools/src/draw2/gesture.rs @@ -98,6 +98,9 @@ pub struct GestureTool { /// A release where the pointer last dragged to would close the stroke because it ends near /// its start (Close Paths When Ends Are Within): the cursor shows the closing ring. near_start: bool, + /// A release where the pointer last dragged to would connect the continued path to another + /// selected path's open end (`finish`): the cursor shows the continuation cursor. + connects: bool, /// Pencil/Paintbrush continuing a selected open path: (path, continue from its start). extend: Option<(NodeId, bool)>, /// The other end of the path `extend` continues: a release near it closes the path. @@ -819,6 +822,7 @@ impl GestureTool { active: false, began: false, near_start: false, + connects: false, extend: None, close_to: None, connect_to: vec![], @@ -1303,6 +1307,7 @@ impl Tool for GestureTool { self.active = true; self.began = false; self.near_start = false; + self.connects = false; // Alt switches the Pencil when its toggle is on, the Blob Brush always, and the // Paintbrush when the press has a selected path. self.smoothing = ev.mods.alt @@ -1380,10 +1385,12 @@ impl Tool for GestureTool { let kept = self.points.last().copied().filter(|l| l.distance(p) < cx.tol(1.0) && !self.snaps()); let release_end = kept.unwrap_or(end); self.near_start = self.release_closes_near_start(cx, release_end); + self.connects = self.connects_at(cx, release_end).is_some(); return vec![]; } self.push(p, ev); self.near_start = self.release_closes_near_start(cx, end); + self.connects = self.connects_at(cx, end).is_some(); if !self.draws_path() || self.smoothing { return vec![]; } @@ -1444,6 +1451,7 @@ impl Tool for GestureTool { self.active = false; self.began = false; self.near_start = false; + self.connects = false; self.smoothing = false; self.straight = false; self.unconstrained = false; @@ -1479,6 +1487,7 @@ impl Tool for GestureTool { self.active = false; self.began = false; self.near_start = false; + self.connects = false; self.smoothing = false; self.straight = false; self.unconstrained = false; @@ -1509,6 +1518,7 @@ impl Tool for GestureTool { self.snap.clear(); self.active = false; self.near_start = false; + self.connects = false; self.began = false; self.smoothing = false; self.straight = false; @@ -1597,8 +1607,8 @@ impl Tool for GestureTool { return if self.find_extend(cx, p).is_some() { Cursor::PencilContinue } else { Cursor::PencilStraight }; } // A continuing Pencil stroke that a release would connect to another selected path's open - // end (`finish`) shows that it continues on. - if self.id == "pencil" && self.active && self.points.last().is_some_and(|&end| self.connects_at(cx, end).is_some()) { + // end (`finish`) shows that it continues on; `connects` tests the point the release keeps. + if self.id == "pencil" && self.active && self.connects { return Cursor::PencilContinue; } // Alt held at the release closes the path (`finish`). diff --git a/crates/tools/src/draw2/gesture/tests.rs b/crates/tools/src/draw2/gesture/tests.rs index 3bee270237..2159b8af2c 100644 --- a/crates/tools/src/draw2/gesture/tests.rs +++ b/crates/tools/src/draw2/gesture/tests.rs @@ -959,6 +959,40 @@ fn a_continued_stroke_shows_where_it_connects_or_closes() { assert_eq!((shown, v["closed"].clone()), (Cursor::Pencil, json!(false)), "at 0 px"); } +/// The continuation cursor tests where the release ends, after snapping, not the pointer: here +/// Snap to Grid moves a stroke's end from 13 pt away from another selected path's end (outside Edit +/// Selected Paths' 12 px) to 9 pt away, so the release connects, and the cursor says so. +#[test] +fn the_continuation_cursor_follows_the_snapped_release() { + let (mut d, _) = doc_with_rect(); + let l = d.layers[0].id; + let add = |d: &mut vectorcraft_doc::Document, a: (f64, f64), b: (f64, f64)| { + let id = d.alloc_id(); + let els = vec![vectorcraft_geom::PathEl::MoveTo(Point::new(a.0, a.1)), vectorcraft_geom::PathEl::LineTo(Point::new(b.0, b.1))]; + let pd = vectorcraft_geom::PathData::from_bezpath(&vectorcraft_geom::BezPath::from_vec(els)); + d.insert(Some(l), 1, vectorcraft_doc::Node::path(id, pd, vectorcraft_doc::Appearance::default_art())).unwrap(); + id + }; + // On the 9 pt grid. + let a = add(&mut d, (306.0, 306.0), (405.0, 405.0)); + let b = add(&mut d, (504.0, 504.0), (603.0, 504.0)); + let p = paint(); + let mut both = Selection::default(); + both.set([a, b]); + let mut cx = cx(&d, &both, &p); + cx.smart_guides = false; + cx.snap_to_point = false; + cx.snap_to_grid = true; + let mut t = GestureTool::new("pencil"); + t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 405.0, 405.0)); + t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 460.0, 450.0)); + t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 517.0, 504.0)); + assert_eq!(t.cursor(&cx, Point::new(517.0, 504.0), Mods::default()), Cursor::PencilContinue); + let acts = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 517.0, 504.0)); + let Some(Action::Preview(_, v)) = acts.first() else { panic!("{acts:?}") }; + assert_eq!(v["connect"], json!({"id": b.0, "end": "start"}), "{v}"); +} + /// #3846: Alt during a Shift line doesn't promise a close the release can't do, and with Alt /// Toggles to Smooth, Alt pressed after the press doesn't turn the line into a smoothing drag. #[test]