From b67a08dc0a41996578672184022777249ef10561 Mon Sep 17 00:00:00 2001 From: ncic <1848265+ncic@users.noreply.github.com> Date: Fri, 13 Nov 2026 22:29:09 +0100 Subject: [PATCH 1/4] Pencil: shows where a stroke continues, connects and closes The continuation cursor (a short rising line) shows over an open end of a selected path before any press, freehand too, and while a continuing stroke nears an open end of another selected path it connects to. The straight-line cursor becomes a short level line. A stroke continuing a path, a straight line too, shows the closing ring within Close Paths When Ends Are Within of the path's other end, and the release closes it; path.freehand takes the tool's closed for extend instead of its own four-Fidelity distance. --- crates/engine/src/cmd/draw2/commands.rs | 2 +- crates/engine/src/cmd/draw2/freehand.rs | 19 ++++-- crates/engine/src/tests/draw2.rs | 25 ++++++++ crates/tools/src/cursor.rs | 12 ++-- crates/tools/src/cursors.toml | 10 +++- crates/tools/src/draw2/gesture.rs | 77 +++++++++++++++++++----- crates/tools/src/draw2/gesture/tests.rs | 80 ++++++++++++++++++++++--- crates/ui-egui/src/cursors.rs | 12 ++-- crates/ui-egui/src/cursors/tests.rs | 17 ++++-- docs/mcp.md | 11 +++- docs/target-app-parity.md | 5 +- docs/ui-parity.md | 5 +- 12 files changed, 217 insertions(+), 58 deletions(-) diff --git a/crates/engine/src/cmd/draw2/commands.rs b/crates/engine/src/cmd/draw2/commands.rs index 400e13baab..e6f88a31e2 100644 --- a/crates/engine/src/cmd/draw2/commands.rs +++ b/crates/engine/src/cmd/draw2/commands.rs @@ -11,7 +11,7 @@ pub fn specs() -> Vec { "Pencil", [], None, - "{points: [[x,y]…] or [[x,y,pressure]…] or [[x,y,pressure,tiltX,tiltY,rotation]…] (pen pressure 0..1: the stroke keeps it, for Calligraphic and Scatter brushes with Pressure variation; a point without one presses fully. Pen tilt in degrees, each −90..90, x positive leaning right and y leaning down the page, and barrel rotation in degrees clockwise: the stroke keeps how far the pen leaned, which way and how far its barrel turned, for brushes with Tilt, Bearing and Rotation variation; null for what a point lacks), fidelity?: pt (1.5; from 0.000015625 (0.01 / 640) to 100 / 0.0313 (about 3194.89), the Pencil's and the Paintbrush's 0.01 to 100 px Fidelity at the highest and the lowest zoom; a value outside that range is an error. The tools send their Fidelity in pixels divided by the zoom, kept within the range), fit?: bool (true; false: the points are joined by straight segments, as the Pencil previews a stroke with Live curve fitting off), closed?, style?: \"pencil\"|\"brush\", fill?: bool, keepSelected?: bool (true: the drawn or continued path is selected; false: a new path leaves nothing selected, and a continued or redrawn one is deselected while the rest of the selection stays), extend?: {id, end: \"start\"|\"end\"}, connect?: {id, end: \"start\"|\"end\"}, redraw?: {id, within?: pt (12)}} fit a freehand stroke → {id}. extend continues the path from that end; with connect, the stroke ends on that end of another path's last open subpath and joins it on, in one path with the extended path's appearance (the other path is removed when nothing of it is left); redraw (Edit Selected Paths) replaces the section of the path between where the stroke starts and ends, each within `within` of one subpath (the shorter way round a closed one), and draws a new path when the stroke doesn't end on it", + "{points: [[x,y]…] or [[x,y,pressure]…] or [[x,y,pressure,tiltX,tiltY,rotation]…] (pen pressure 0..1: the stroke keeps it, for Calligraphic and Scatter brushes with Pressure variation; a point without one presses fully. Pen tilt in degrees, each −90..90, x positive leaning right and y leaning down the page, and barrel rotation in degrees clockwise: the stroke keeps how far the pen leaned, which way and how far its barrel turned, for brushes with Tilt, Bearing and Rotation variation; null for what a point lacks), fidelity?: pt (1.5; from 0.000015625 (0.01 / 640) to 100 / 0.0313 (about 3194.89), the Pencil's and the Paintbrush's 0.01 to 100 px Fidelity at the highest and the lowest zoom; a value outside that range is an error. The tools send their Fidelity in pixels divided by the zoom, kept within the range), fit?: bool (true; false: the points are joined by straight segments, as the Pencil previews a stroke with Live curve fitting off), closed?, style?: \"pencil\"|\"brush\", fill?: bool, keepSelected?: bool (true: the drawn or continued path is selected; false: a new path leaves nothing selected, and a continued or redrawn one is deselected while the rest of the selection stays), extend?: {id, end: \"start\"|\"end\"}, connect?: {id, end: \"start\"|\"end\"}, redraw?: {id, within?: pt (12)}} fit a freehand stroke → {id}. extend continues the path from that end, and closes it when `closed` is true (left out, when the stroke ends within four Fidelities of the path's other end, 6 to 400 pt); with connect, the stroke ends on that end of another path's last open subpath and joins it on, in one path with the extended path's appearance (the other path is removed when nothing of it is left); redraw (Edit Selected Paths) replaces the section of the path between where the stroke starts and ends, each within `within` of one subpath (the shorter way round a closed one), and draws a new path when the stroke doesn't end on it", has_doc, freehand ), diff --git a/crates/engine/src/cmd/draw2/freehand.rs b/crates/engine/src/cmd/draw2/freehand.rs index 31e184e195..1ec591f2ff 100644 --- a/crates/engine/src/cmd/draw2/freehand.rs +++ b/crates/engine/src/cmd/draw2/freehand.rs @@ -204,11 +204,18 @@ pub(super) fn freehand(s: &mut Session, p: &Value) -> Result { } None => None, }; - // An extending stroke that ends within four Fidelities of the path's other end closes it: - // at least 6 pt and at most 400 pt, whatever the Fidelity it is fitted at. + // `closed`, when given, says whether the stroke closes the path: the Pencil and the + // Paintbrush send it, by their Close Paths When Ends Are Within and Alt at the release. + // Left out, an extending stroke that ends within four Fidelities of the path's other end + // closes it: at least 6 pt and at most 400 pt, whatever the Fidelity it is fitted at. let close_tol = (tol * 4.0).clamp(6.0, 400.0); - let closing = connect.is_none() && sp.anchors.len() >= 2 && pts.last().is_some_and(|p| p.distance(first) <= close_tol); - if closing && let Some(l) = pts.last_mut() { + let near_first = pts.last().is_some_and(|p| p.distance(first) <= close_tol); + let wants_close = if p.get("closed").is_some_and(|v| !v.is_null()) { closed } else { near_first }; + let closing = connect.is_none() && sp.anchors.len() >= 2 && wants_close; + // A stroke ending near the other end ends on it; one ending farther away closes with a + // straight segment back to it. + let ends_on_first = closing && near_first; + if ends_on_first && let Some(l) = pts.last_mut() { *l = first; } if let Some((_, _, y, _)) = &connect @@ -225,13 +232,15 @@ pub(super) fn freehand(s: &mut Session, p: &Value) -> Result { add.remove(0); } sp.anchors.extend(add); - if closing + if ends_on_first && sp.anchors.len() > 2 && let Some(l) = sp.anchors.pop() && let Some(a0) = sp.anchors.first_mut() { a0.h_in = l.h_in; sp.closed = true; + } else if closing && !ends_on_first && sp.anchors.len() > 2 { + sp.closed = true; } for a in &mut sp.anchors { *a = Anchor::with_handles(a.p, a.h_in, a.h_out); diff --git a/crates/engine/src/tests/draw2.rs b/crates/engine/src/tests/draw2.rs index f29df7ff23..89393c0f4c 100644 --- a/crates/engine/src/tests/draw2.rs +++ b/crates/engine/src/tests/draw2.rs @@ -4852,6 +4852,31 @@ fn pencil_extend_closes_within_400_pt_at_any_fidelity() { } } +/// `closed`, when given, decides whether a stroke continuing an open path closes it (the tools +/// send it by Close Paths When Ends Are Within): false leaves it open even ending on the other end; +/// true closes one ending 50 pt away with a straight segment back, the stroke's end kept, and one +/// ending 2 pt away on the other end itself. +#[test] +fn pencil_extend_closes_as_closed_says() { + let mut s = session(); + for (end, closed, ends_on_start) in [([0i32, 0], false, false), ([0, 50], true, false), ([0, 2], true, true)] { + let l = line(&mut s, 0.0, 0.0, 200.0, 0.0); + let points = json!([[200, 0], [200, 100], [100, 150], end]); + let p = json!({"points": points, "fidelity": 1.5, "closed": closed, "extend": {"id": l.0}, "keepSelected": false}); + s.execute("path.freehand", &p).unwrap(); + let pd = path(&s, l); + let sp = &pd.subpaths[0]; + assert_eq!(sp.closed, closed, "ending at {end:?}"); + if closed { + let stroke_end = Point::new(f64::from(end[0]), f64::from(end[1])); + let kept = sp.anchors.iter().any(|a| a.p.distance(stroke_end) < 1e-6); + assert_eq!(kept, !ends_on_start, "ending at {end:?}: {:?}", sp.anchors); + let on_start = sp.anchors.iter().filter(|a| a.p.distance(Point::new(0.0, 0.0)) < 1e-6).count(); + assert_eq!((sp.anchors[0].p, on_start), (Point::new(0.0, 0.0), 1), "ending at {end:?}"); + } + } +} + /// At Fidelity 0.01 short, one-point and repeated-point strokes still draw or are refused, never /// crash, and what they draw has finite anchors. #[test] diff --git a/crates/tools/src/cursor.rs b/crates/tools/src/cursor.rs index 584149da8f..85b9cf258e 100644 --- a/crates/tools/src/cursor.rs +++ b/crates/tools/src/cursor.rs @@ -110,14 +110,16 @@ pub enum Cursor { /// The Pencil tool with Alt pressed after the drag starts and Alt key toggles to Smooth Tool off: /// releasing the mouse button with Alt still held closes the path. A drag started with Shift or /// Alt held draws an open straight line instead. Also while a freehand stroke is drawn back near - /// its start, where releasing the mouse button closes the path. A crosshair with a ring. + /// its start, or a stroke continuing a selected path near that path's other end, where + /// releasing the mouse button closes the path. A crosshair with a ring. PencilClose, /// The Pencil tool before a press that draws a straight line (Shift or Alt held). A crosshair with a - /// short straight line. + /// short level line. PencilStraight, - /// The Pencil tool before a press that draws a straight line (Shift or Alt held), over an end of - /// a selected open path: the line continues the path from that end. A crosshair with a short - /// straight line leaving a square end point. + /// The Pencil tool before a press over an end of a selected open path: the stroke, freehand or a + /// straight line, continues the path from that end. Also while a continuing stroke ends near an + /// open end of another path selected at the press, where releasing the mouse button connects the + /// two. A crosshair with a short line rising to the right. PencilContinue, /// The Paintbrush tool. Paintbrush, diff --git a/crates/tools/src/cursors.toml b/crates/tools/src/cursors.toml index 6f5b32e56a..1c15b37d71 100644 --- a/crates/tools/src/cursors.toml +++ b/crates/tools/src/cursors.toml @@ -638,6 +638,7 @@ group = "Drawing and editing tools" shown_when = [ "The Pencil tool with Alt pressed after the drag starts, when Alt key toggles to Smooth Tool is off: releasing the mouse button with Alt still held closes the path. A drag started with Shift or Alt held draws an open straight line and does not show this cursor.", "The Pencil tool while a freehand stroke is drawn back to within Close Paths When Ends Are Within of its start: releasing the mouse button there closes the path.", + "The Pencil tool while a stroke continuing a selected open path, freehand or a straight line, is drawn to within Close Paths When Ends Are Within of that path's other end: releasing the mouse button there closes the path.", ] hotspot = "The lead tip." look = "The Pencil cursor with a ring badge (closing the loop), like PenClose." @@ -647,14 +648,17 @@ id = "PencilStraight" group = "Drawing and editing tools" shown_when = ["The Pencil tool with Shift or Alt held before a press, except with Alt held when Alt key toggles to Smooth Tool is on: the press draws a straight line, at 45° steps from the Constrain Angle when Shift is held and at any angle with Alt alone."] hotspot = "The lead tip." -look = "The Pencil cursor with a short straight-line badge, where PencilClose has its ring." +look = "The Pencil cursor with a short level-line badge, where PencilClose has its ring." [[cursor]] id = "PencilContinue" group = "Drawing and editing tools" -shown_when = ["The Pencil tool over an end of a selected open path, within Edit Selected Paths, with Shift or Alt held before a press, except with Alt held when Alt key toggles to Smooth Tool is on: the press draws a straight line that continues the path from that end."] +shown_when = [ + "The Pencil tool over an end of a selected open path, within Edit Selected Paths, before a press, except with Alt held when Alt key toggles to Smooth Tool is on: the press continues the path from that end, freehand, or with a straight line when Shift or Alt is held.", + "The Pencil tool while a stroke continuing a selected path ends within Edit Selected Paths of an open end of another path selected at the press: releasing the mouse button connects the two.", +] hotspot = "The lead tip." -look = "PencilStraight's badge with a small filled square at the line's lower end (the end point the line continues from)." +look = "The Pencil cursor with a short badge line rising to the right, where PencilClose has its ring (the stroke carries on from the path)." [[cursor]] id = "Paintbrush" diff --git a/crates/tools/src/draw2/gesture.rs b/crates/tools/src/draw2/gesture.rs index 2de44f97ba..67da5e3499 100644 --- a/crates/tools/src/draw2/gesture.rs +++ b/crates/tools/src/draw2/gesture.rs @@ -97,6 +97,8 @@ pub struct GestureTool { near_start: 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. + close_to: Option, /// A Pencil stroke continuing a selected open path: the other paths selected at the press, /// whose open ends it connects to. connect_to: Vec, @@ -796,6 +798,7 @@ impl GestureTool { began: false, near_start: false, extend: None, + close_to: None, connect_to: vec![], redraw: None, fidelity, @@ -1066,6 +1069,21 @@ impl GestureTool { self.find_end(cx, p, &cx.selection.objects) } + /// The other end of the open end `find_end` found: the opposite end of `id`'s last open + /// subpath. + fn other_end(cx: &ToolContext, id: NodeId, at_start: bool) -> Option { + let pd = cx.doc.node(id)?.path_data()?; + let sp = pd.subpaths.iter().rev().find(|s| !s.closed && !s.anchors.is_empty())?; + let a = if at_start { sp.anchors.last() } else { sp.anchors.first() }; + a.map(|a| a.p) + } + + /// Continues the path at the open end `find_extend` found, if any, noting its other end. + fn set_extend(&mut self, cx: &ToolContext, found: Option<(NodeId, bool, Point)>) { + self.extend = found.map(|(id, at_start, _)| (id, at_start)); + self.close_to = found.and_then(|(id, at_start, _)| Self::other_end(cx, id, at_start)); + } + /// [`Self::find_extend`] among `ids`. fn find_end(&self, cx: &ToolContext, p: Point, ids: &[NodeId]) -> Option<(NodeId, bool, Point)> { if self.edit_within <= 0.0 { @@ -1110,16 +1128,32 @@ impl GestureTool { } /// Does a stroke that ends at `end` end near its start (Close Paths When Ends Are Within, off - /// at 0 pixels)? + /// at 0 pixels)? A stroke continuing a selected path ends near that path's other end, a + /// straight line too; a new straight line never closes, as it would run back over itself. fn ends_near_start(&self, cx: &ToolContext, end: Point) -> bool { - self.close_within > 0.0 && self.points.len() > 2 && self.points[0].distance(end) <= cx.tol(self.close_within) + if self.close_within <= 0.0 { + return false; + } + let tol = cx.tol(self.close_within); + match (self.extend, self.close_to) { + (Some(_), Some(to)) => self.points.len() >= 2 && to.distance(end) <= tol, + (Some(_), None) => false, + (None, _) => !self.straight && self.points.len() > 2 && self.points.first().is_some_and(|s| s.distance(end) <= tol), + } } - /// Does a release now, with the stroke ending at `end`, close a freehand stroke because it ends - /// near its start (`finish`)? A stroke that redraws a selected path closes only when the engine - /// finds it does not end on that path, so it counts as not closing. + /// The open end of another path selected at the press that a continuing Pencil stroke ending + /// at `end` connects to (`finish`). + fn connects_at(&self, cx: &ToolContext, end: Point) -> Option<(NodeId, bool, Point)> { + self.extend.and_then(|_| self.find_end(cx, end, &self.connect_to)) + } + + /// Does a release now, with the stroke ending at `end`, close the stroke because it ends near + /// its start, or near the other end of the path it continues (`finish`)? A stroke that redraws + /// a selected path closes only when the engine finds it does not end on that path, so it counts + /// as not closing, and a stroke that connects to another path doesn't close. fn release_closes_near_start(&self, cx: &ToolContext, end: Point) -> bool { - self.draws_path() && !self.smoothing && !self.straight && self.extend.is_none() && self.redraw.is_none() && self.ends_near_start(cx, end) + self.draws_path() && !self.smoothing && self.redraw.is_none() && self.ends_near_start(cx, end) && self.connects_at(cx, end).is_none() } /// Parameters of `path.freehand` for the current stroke, fitted at Fidelity on screen. Only the @@ -1160,14 +1194,16 @@ impl GestureTool { if !self.began { return vec![]; } - // Close Paths When Ends Are Within is off at 0 pixels. + // Close Paths When Ends Are Within is off at 0 pixels. A stroke continuing a + // selected path closes it near its other end, a straight line too. let near = pts.last().is_some_and(|&end| self.ends_near_start(cx, end)); - // A straight line has two points: closing it would draw it back over itself. - let closed = !self.straight && ((m.alt && (self.id == "paintbrush" || !self.alt_smooth)) || (self.extend.is_none() && near)); - let mut params = self.freehand_params(cx, closed); + // Alt at the release closes a freehand stroke; a new straight line has two points: + // closing it would draw it back over itself. + let closed = (!self.straight && m.alt && (self.id == "paintbrush" || !self.alt_smooth)) || near; // A Pencil stroke that continues a selected path and ends on an open end of another - // path selected at the press connects the two. - let connect = pts.last().and_then(|&end| self.find_end(cx, end, &self.connect_to)); + // path selected at the press connects the two, and doesn't close. + let connect = pts.last().and_then(|&end| self.connects_at(cx, end)); + let mut params = self.freehand_params(cx, closed && connect.is_none()); if let Some((other, at_start, _)) = connect { params["connect"] = json!({"id": other.0, "end": if at_start { "start" } else { "end" }}); } @@ -1241,7 +1277,7 @@ impl Tool for GestureTool { self.straight = (self.id == "pencil" && !self.smoothing && (ev.mods.shift || ev.mods.alt)) || (self.id == "knife" && ev.mods.alt); self.unconstrained = self.straight && !ev.mods.shift; let extend = if self.draws_path() && !self.smoothing { self.find_extend(cx, p) } else { None }; - self.extend = extend.map(|(id, at_start, _)| (id, at_start)); + self.set_extend(cx, extend); // The stroke's previews select the path it continues, so the others are kept now. self.connect_to = match self.extend { Some((id, _)) if self.id == "pencil" => cx.selection.objects.iter().copied().filter(|o| *o != id).collect(), @@ -1299,6 +1335,8 @@ impl Tool for GestureTool { return if std::mem::take(&mut self.began) { vec![Action::Cancel] } else { vec![] }; } self.points.truncate(1); + // The line ends at `p`, where its end snaps. + end = p; } else if self.points.last().is_some_and(|l| l.distance(p) < cx.tol(1.0)) || self.points.len() >= crate::MAX_STROKE_SAMPLES { // A release here keeps the last point unless it snaps (`Up`), so the ring // follows the point `finish` will test. @@ -1374,6 +1412,7 @@ impl Tool for GestureTool { self.unconstrained = false; self.clear(); self.extend = None; + self.close_to = None; self.connect_to.clear(); self.redraw = None; out @@ -1517,6 +1556,11 @@ impl Tool for GestureTool { if !self.active && self.straight_at_press(m) { 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()) { + return Cursor::PencilContinue; + } // Alt held at the release closes the path (`finish`). // A straight line stays open, so its release has nothing to close, and before a press Alt // gives the Pencil a straight line. @@ -1536,9 +1580,12 @@ impl Tool for GestureTool { "smooth" => Cursor::Smooth, "pathEraser" => Cursor::PathEraser, "join" => Cursor::Join, + // Before a press over an open end of a selected path the Pencil shows that the stroke + // continues it (`PointerKind::Down`). + _ if !self.active && self.find_extend(cx, p).is_some() => Cursor::PencilContinue, // Before a press that draws a new path the Pencil shows its star; over a selected path - // that the press continues from an end or redraws (`PointerKind::Down`), it does not. - _ if !self.active && self.find_extend(cx, p).is_none() && self.find_redraw(cx, p).is_none() => Cursor::PencilNew, + // that the press redraws (`PointerKind::Down`), it does not. + _ if !self.active && self.find_redraw(cx, p).is_none() => Cursor::PencilNew, _ => Cursor::Pencil, } } diff --git a/crates/tools/src/draw2/gesture/tests.rs b/crates/tools/src/draw2/gesture/tests.rs index 624adc2e98..65ad5f00e7 100644 --- a/crates/tools/src/draw2/gesture/tests.rs +++ b/crates/tools/src/draw2/gesture/tests.rs @@ -785,7 +785,7 @@ fn pencil_shows_the_star_before_a_press_that_draws_a_new_path() { } } let mut t = GestureTool::new("pencil"); - assert_eq!(t.cursor(&cx, end, Mods::default()), Cursor::Pencil, "over an end the stroke continues"); + assert_eq!(t.cursor(&cx, end, Mods::default()), Cursor::PencilContinue, "over an end the stroke continues"); assert_eq!(t.cursor(&cx, middle, Mods::default()), Cursor::Pencil, "over the path the stroke redraws"); assert_eq!(t.cursor(&cx, away, Mods::default()), Cursor::PencilNew, "away from the path"); // While a stroke is drawn, the Pencil's own cursor. @@ -830,7 +830,7 @@ fn pencil_shift_line_continues_a_selected_path_from_its_end() { } /// Before a press over an end of a selected open path, the Pencil shows its continuation cursor -/// exactly when the press would draw a straight line that continues the path; away from the +/// exactly when the press continues the path, freehand or with a straight line; away from the /// ends, with nothing selected, with Edit Selected Paths off, once the press is down and with /// the Paintbrush, it does not. #[test] @@ -851,12 +851,9 @@ fn pencil_shows_the_continuation_cursor_over_an_end_a_line_continues() { let shift = Mods { shift: true, ..Default::default() }; let alt = Mods { alt: true, ..Default::default() }; let shift_alt = Mods { shift: true, alt: true, ..Default::default() }; - // The release's stroke: a straight line that continues the path. - let continues = |acts: &[Vec]| { - acts.last() - .and_then(|a| a.first()) - .is_some_and(|a| matches!(a, Action::Preview(_, v) if v.get("extend").is_some() && v["points"].as_array().is_some_and(|q| q.len() == 2))) - }; + // The release's stroke continues the path. + let continues = + |acts: &[Vec]| acts.last().and_then(|a| a.first()).is_some_and(|a| matches!(a, Action::Preview(_, v) if v.get("extend").is_some())); for alt_smooth in [false, true] { for m in [Mods::default(), shift, alt, shift_alt] { for (at, other_end) in [(Point::new(405.0, 303.0), (500.0, 296.0)), (Point::new(296.0, 302.0), (200.0, 310.0))] { @@ -872,7 +869,7 @@ fn pencil_shows_the_continuation_cursor_over_an_end_a_line_continues() { let end = Point::new(405.0, 303.0); assert_eq!(t.cursor(&cx, end, shift), Cursor::PencilContinue); assert_eq!(t.cursor(&cx, end, alt), Cursor::PencilContinue, "Alt alone continues at any angle"); - assert_eq!(t.cursor(&cx, end, Mods::default()), Cursor::Pencil, "without Shift or Alt the stroke is freehand"); + assert_eq!(t.cursor(&cx, end, Mods::default()), Cursor::PencilContinue, "a freehand stroke continues it too"); assert_eq!(t.cursor(&cx, Point::new(350.0, 360.0), shift), Cursor::PencilStraight, "away from the ends"); // While the line is drawn, the Pencil's own cursor. t.pointer(&cx, &PointerEvent::new(PointerKind::Down, end.x, end.y).with_mods(shift)); @@ -889,6 +886,71 @@ fn pencil_shows_the_continuation_cursor_over_an_end_a_line_continues() { assert_eq!(Cursor::PencilContinue.precise(), Cursor::Crosshair, "Use Precise Cursors"); } +/// A Pencil stroke continuing a selected path shows the continuation cursor while a release would +/// connect it to an open end of another path selected at the press, and the closing ring while a +/// release would close the path because the stroke ends within Close Paths When Ends Are Within +/// of the path's other end, a straight line too; the release does what the cursor showed. +#[test] +fn a_continued_stroke_shows_where_it_connects_or_closes() { + let (mut d, _) = doc_with_rect(); + let l = d.layers[0].id; + let add = |d: &mut vectorcraft_doc::Document, pts: &[(f64, f64)]| { + let id = d.alloc_id(); + let mut els = vec![vectorcraft_geom::PathEl::MoveTo(Point::new(pts[0].0, pts[0].1))]; + els.extend(pts[1..].iter().map(|&(x, y)| vectorcraft_geom::PathEl::LineTo(Point::new(x, y)))); + 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 + }; + let a = add(&mut d, &[(300.0, 300.0), (400.0, 300.0), (400.0, 400.0)]); + let b = add(&mut d, &[(500.0, 500.0), (600.0, 500.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; + let none = Mods::default(); + let shift = Mods { shift: true, ..Default::default() }; + // Runs a stroke from `pts[0]`, giving the cursor at its last point before the release and the + // release's `path.freehand` parameters. + let run = |t: &mut GestureTool, pts: &[(f64, f64)], m: Mods| { + t.pointer(&cx, &PointerEvent::new(PointerKind::Down, pts[0].0, pts[0].1).with_mods(m)); + for &(x, y) in &pts[1..] { + t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, x, y).with_mods(m)); + } + let (x, y) = pts[pts.len() - 1]; + let shown = t.cursor(&cx, Point::new(x, y), m); + let acts = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, x, y).with_mods(m)); + let Some(Action::Preview(_, v)) = acts.first() else { panic!("{acts:?}") }; + (shown, v.clone()) + }; + let mut t = GestureTool::new("pencil"); + // To the other selected path's start: it connects, and doesn't close. + let (shown, v) = run(&mut t, &[(400.0, 400.0), (450.0, 450.0), (498.0, 502.0)], none); + assert_eq!(shown, Cursor::PencilContinue, "near the other path's end"); + assert_eq!( + (v["extend"].clone(), v["connect"].clone(), v["closed"].clone()), + (json!({"id": a.0, "end": "end"}), json!({"id": b.0, "end": "start"}), json!(false)) + ); + // Back near the path's own start (10 px from it): the ring, and the release closes it. + let (shown, v) = run(&mut t, &[(400.0, 400.0), (350.0, 420.0), (290.0, 380.0), (292.0, 306.0)], none); + assert_eq!(shown, Cursor::PencilClose, "near the path's other end"); + assert_eq!((v["extend"].clone(), v["closed"].clone()), (json!({"id": a.0, "end": "end"}), json!(true))); + assert!(v.get("connect").is_none(), "{v}"); + // Farther than Close Paths When Ends Are Within (15 px): no ring, and it stays open. + let (shown, v) = run(&mut t, &[(400.0, 400.0), (350.0, 420.0), (290.0, 380.0), (290.0, 330.0)], none); + assert_eq!((shown, v["closed"].clone()), (Cursor::Pencil, json!(false)), "away from the other end"); + // A straight line from the end to near the start: the ring, and it closes the path. + let (shown, v) = run(&mut t, &[(400.0, 400.0), (350.0, 350.0), (306.0, 306.0)], shift); + assert_eq!(shown, Cursor::PencilClose, "a straight line near the other end"); + assert_eq!((v["points"].as_array().map(Vec::len), v["closed"].clone()), (Some(2), json!(true))); + // Close Paths When Ends Are Within off: neither ring nor close. + t.set_option("closeWithin", &json!(0.0)); + let (shown, v) = run(&mut t, &[(400.0, 400.0), (350.0, 420.0), (290.0, 380.0), (292.0, 306.0)], none); + assert_eq!((shown, v["closed"].clone()), (Cursor::Pencil, json!(false)), "at 0 px"); +} + /// #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] diff --git a/crates/ui-egui/src/cursors.rs b/crates/ui-egui/src/cursors.rs index a438a71881..5495d3753c 100644 --- a/crates/ui-egui/src/cursors.rs +++ b/crates/ui-egui/src/cursors.rs @@ -677,18 +677,16 @@ fn glyph(c: Cursor, p: Pos2) -> Option> { ink.circle_stroke(p + vec2(8.0, 8.0), 3.0, Stroke::new(3.0, HALO)); ink.circle_stroke(p + vec2(8.0, 8.0), 3.0, Stroke::new(1.2, INK)); } - // The Pencil's crosshair with a short straight line where the closing ring goes. + // The Pencil's crosshair with a short level line where the closing ring goes. Cursor::PencilStraight => { crosshair(ink, p); - line(ink, p + vec2(5.0, 12.0), p + vec2(12.0, 5.0)); + line(ink, p + vec2(3.55, 8.5), p + vec2(13.45, 8.5)); } - // The straight-line cursor's line leaving a filled square: the end point a line continues from. + // The Pencil's crosshair with a short line rising to the right where the closing ring goes: + // the stroke carries on from the path. Cursor::PencilContinue => { crosshair(ink, p); - // The square first, so its halo does not cover the line. - let end = p + vec2(5.0, 12.0); - poly(ink, [(-2.0, -2.0), (2.0, -2.0), (2.0, 2.0), (-2.0, 2.0)].map(|(x, y)| end + vec2(x, y)).to_vec(), INK, INK); - line(ink, end, p + vec2(12.0, 5.0)); + line(ink, p + vec2(5.0, 12.0), p + vec2(12.0, 5.0)); } Cursor::Eyedropper => eyedropper(ink, p), Cursor::ResizeH => double_arrow(ink, p, vec2(1.0, 0.0)), diff --git a/crates/ui-egui/src/cursors/tests.rs b/crates/ui-egui/src/cursors/tests.rs index ae1947616f..49c932175d 100644 --- a/crates/ui-egui/src/cursors/tests.rs +++ b/crates/ui-egui/src/cursors/tests.rs @@ -244,7 +244,7 @@ fn the_pencil_new_path_cursor_is_the_crosshair_with_a_star() { } } -/// The Pencil's straight-line cursor is the crosshair with a short straight line below right, +/// The Pencil's straight-line cursor is the crosshair with a short level line below right, /// through the point where the closing cursor's ring is hollow. #[test] fn the_pencil_straight_line_cursor_is_the_crosshair_with_a_line() { @@ -264,15 +264,18 @@ fn the_pencil_straight_line_cursor_is_the_crosshair_with_a_line() { assert_eq!(at(&cross, 8.5, 8.5)[3], 0, "the crosshair has no badge at {ppp}"); // Along the line, not across it: no ink off its ends. assert!(!inked(at(&img, 5.0, 5.0)) && !inked(at(&img, 12.0, 12.0)), "a line, not a cross, at {ppp}"); + // Level: inked at both ends of it, and not where the continuation cursor's line rises. + assert!(inked(at(&img, 4.5, 8.5)) && inked(at(&img, 12.5, 8.5)), "a level line at {ppp}"); + assert!(!inked(at(&img, 6.0, 11.0)) && !inked(at(&img, 11.0, 6.0)), "not a rising one at {ppp}"); assert_ne!(img, ring, "at {ppp}"); assert_eq!(at(&img, -6.0, 0.0), at(&cross, -6.0, 0.0), "the arms are the crosshair's at {ppp}"); } } -/// The Pencil's continuation cursor is the straight-line cursor with a filled square on the -/// line's lower end, beside the line where the straight-line cursor has no ink. +/// The Pencil's continuation cursor is the crosshair with a short line rising to the right below +/// right, where the straight-line cursor's line is level. #[test] -fn the_pencil_continuation_cursor_is_the_straight_line_cursor_with_an_end_point() { +fn the_pencil_continuation_cursor_is_the_crosshair_with_a_rising_line() { for ppp in [1.0, 2.0] { let cross = rasterize(Cursor::Crosshair, ppp).unwrap(); let img = rasterize(Cursor::PencilContinue, ppp).unwrap(); @@ -285,8 +288,10 @@ fn the_pencil_continuation_cursor_is_the_straight_line_cursor_with_an_end_point( }; let inked = |p: [u8; 4]| p[3] > 200 && p[0] < 64; assert!(inked(at(&img, 8.5, 8.5)), "the line crosses the badge's middle at {ppp}"); - assert!(inked(at(&img, 3.5, 13.5)), "the end point's square at {ppp}"); - assert!(!inked(at(&straight, 3.5, 13.5)), "the straight-line cursor has no end point at {ppp}"); + assert!(inked(at(&img, 5.0, 11.0)) && inked(at(&img, 10.0, 6.0)), "the line rises to the right at {ppp}"); + assert!(!inked(at(&straight, 6.0, 11.0)), "the straight-line cursor's line is level at {ppp}"); + assert!(!inked(at(&img, 4.5, 8.5)) && !inked(at(&img, 12.5, 8.5)), "not a level one at {ppp}"); + assert!(!inked(at(&img, 3.5, 13.5)), "no end point square at {ppp}"); assert!(!inked(at(&img, 12.0, 12.0)), "a line, not a cross, at {ppp}"); assert_eq!(at(&img, -6.0, 0.0), at(&cross, -6.0, 0.0), "the arms are the crosshair's at {ppp}"); } diff --git a/docs/mcp.md b/docs/mcp.md index cb5c40501c..e23a46b0b9 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1,6 +1,6 @@ # VectorCraft MCP server -> **Last updated:** 2026-11-13 · **Change:** Envelopes: a top object's side whose pieces are all one curve split at anchors follows that curve's parameter (#7097); existing response shapes retained. Previously: Save location-refusal error (#6146); existing response shapes retained. +> **Last updated:** 2026-11-13 · **Change:** Pencil continuation, straight-line and closing cursors; scores and estimates unchanged; no response-shape change. Previously: Envelopes: a top object's side whose pieces are all one curve split at anchors follows that curve's parameter (#7097); existing response shapes retained. `vectorcraft-cli mcp` runs a [Model Context Protocol](https://modelcontextprotocol.io) server on stdio (newline-delimited JSON-RPC 2.0; see [Protocol](#protocol) for version negotiation). Agents use it to @@ -3058,8 +3058,12 @@ already walks Image Trace. Short agent entry points: is rejected with an error. The Pencil and the Paintbrush give Fidelity in screen pixels, 0.01 to 100 px, and send it divided by the zoom (0.0313 to 640) and kept within that range, so a stroke is fitted at the same tolerance on screen at every zoom. A stroke - that continues a path (`extend`) closes it when it ends within four times the Fidelity of the path's other end, at - least 6 pt and at most 400 pt. The Blob Brush's `path.blob` Fidelity stays in points, 0.01 to 100 pt. + that continues a path (`extend`) closes it when `closed` is true: the Pencil and the Paintbrush send it when the + stroke ends within Close Paths When Ends Are Within of the path's other end (the cursor shows the closing ring there), + a straight Pencil line too, or with Alt held at the release. The stroke's end then joins the other end when it is + within four times the Fidelity of it (at least 6 pt and at most 400 pt), and a straight segment closes the path from + farther away. Without `closed`, the stroke closes the path when it ends within that four-times-Fidelity distance. The + Blob Brush's `path.blob` Fidelity stays in points, 0.01 to 100 pt. - Variables: `variable.list`, `variable.define {name, kind: text|visibility|image|graph}`, `variable.bind {variable, ids?}`, `variable.makeTextDynamic`, `variable.makeVisibilityDynamic`, `variable.makeImageDynamic`, `variable.makeGraphDynamic` (`variable.bind` and `variable.makeTextDynamic` refuse type that already has another text variable and name it), @@ -11327,5 +11331,6 @@ the error text. The interactive fallback applies only to ordinary File > Save, n | Date | Change | |---|---| +| 2026-11-13 | `path.freehand`: `closed`, when given, decides whether a stroke that continues a path (`extend`) closes it; the Pencil and the Paintbrush send it by Close Paths When Ends Are Within. | | 2026-11-13 | Envelopes: a top object's side whose pieces are all one curve split at anchors follows that curve's parameter (#7097). | | 2026-11-11 | Document READ_ONLY_LOCATION for saves (#6146); no response-shape change. | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index e309f72409..f195cab4b2 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-13 · **Change:** minor (`.ai` and EPS import: in layers that don't draw as the page, art that differs away from an envelope of type no longer costs it its editable type; it keeps its drawn art where, in the pixels it and its drawn art ink, the layers with it differ from the page more than its own check allows, or past the envelopes' share of the page check, #7141; Character panel: the Set Font Height Reference dropdown shows an icon before each reference, #6304; scores and estimates unchanged; previously: Envelopes: a top object's side whose pieces are all one curve split at anchors follows that curve's parameter, so a top object saved with its sides' curves in pieces draws as its `.ai` drawn art, #7097; scores and estimates unchanged; previously: Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: PDF import: glyphs in a span with an empty ActualText come in as outlines, #6971; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously minor: Object › Rasterize with the Grayscale or Bitmap colour model makes a grayscale image, #6969; scores and estimates unchanged; previously minor: `.ai` and EPS import: type in an envelope with Distort Appearance is also tried mapped from the box of what it paints, miter tips and moved strokes included, after its glyphs grown by half its strokes' weight (#6824; #6617: Swatches panel menu: Show Recent Colors; scores unchanged; previously minor: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: explicitly requested oversized editing artboard; macOS: the canvas cursor stays after the pointer moves, #6362; scores and estimates unchanged; previously minor: Type tool: kerning at a caret at the start of a line moves the character after it, #5111; scores and estimates unchanged; previously minor: Rulers: the zero point dragged out of the rulers' corner snaps as a drawn point does; previously minor: Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: Envelopes: with Distort Appearance, type's strokes paint along the bent glyph outlines at their own weight, as users' `.ai` files draw them, which changes how VectorCraft's own envelopes of stroked type draw (#6644; scores and estimates unchanged); previously minor: G5: Energy Beams brush library; bundled content ~44% → ~46%; previously minor: G5: Skill Icons symbol library; bundled content ~42% → ~44%; previously minor: `.ai` and EPS import: an envelope saved in Edit Contents whose mesh lies away from its drawn art by a move opens live on that art (#6570; scores and estimates unchanged); previously minor: ruler double-click adds a guide at the pointer, #5239; G5: Status Effects graphic style library; bundled content ~40% → ~42%; MCP symbol libraries list UI Frames and Map Markers; previously minor: G5: Inventory Icons built-in symbol library; bundled content ~38% → ~40%; previously minor: `.ai` and EPS import: stroked type in an envelope maps from its glyphs grown by half its stroke (#3179; scores and estimates unchanged); previously minor: `.ai` and EPS import: warps of type open as mesh envelopes on their saved grid where VectorCraft's warp doesn't draw as their drawn art (#3179; scores and estimates unchanged); G5: Game Trails brush library; bundled content ~36% → ~38%; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: `.ai` and EPS import: envelopes of type open live where they draw as their drawn art and the layers with them draw as the page (#3179; scores and estimates unchanged); G5: Game HUD graphic style library; bundled content ~34% → ~36%; previously minor: Line Arrow arrowhead (#5421; scores and estimates unchanged); G5: Map Markers built-in symbol library; bundled content ~32% → ~34%; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: Canvas: a 3D effect changed on type or a group redraws where its solid was; Fit to Artwork Bounds and per-artboard SVG export build each solid once for all artboards; previously minor: Effect Gallery: a thumbnail for each filter and a preview area inside the dialog, on an original drawing; Raster effects 52% → 55%, hours 1–2 → none; headline numbers unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-13 · **Change:** minor (Pencil continuation, straight-line and closing cursors; scores and estimates unchanged). > > Oversized artboard import update (2026-11-12): Explicit editing-data import retains a single artboard beyond a capped 14,400 pt PDF page (#6865). > @@ -53,6 +53,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-13 | minor | Pencil cursors: the continuation cursor (a short rising line) shows over an open end of a selected path before any press, freehand too, and while a continuing stroke nears another selected path's open end it connects to; the straight-line cursor is a short level line; a continued path, a straight line too, shows the closing cursor and closes within Close Paths When Ends Are Within of its other end, and with Alt at the release (`a_continued_stroke_shows_where_it_connects_or_closes` in `crates/tools/src/draw2/gesture/tests.rs`, `pencil_extend_closes_as_closed_says` in `crates/engine/src/tests/draw2.rs`). Scores and estimates unchanged. | | 2026-11-13 | minor | Graphic Styles: in a panel made taller the list fills the space above the footer instead of stopping at 220 pt (#2992; `the_list_fills_a_tall_panel_above_its_footer` in `crates/ui-egui/src/panels/graphic_styles.rs`). Scores and estimates unchanged. | | 2026-11-13 | minor | PDF import: glyphs inside a marked-content span with an empty `ActualText` come in as outlines in their fill and stroke instead of being left out, adding no text (#6971; `empty_actual_text_keeps_the_glyphs_art` in `crates/pdf/src/tests/actual_text.rs`). Scores and estimates unchanged. | | 2026-11-13 | minor | Library panels (swatch, Color Book, brush, symbol and graphic style libraries): in a panel made taller the list fills the space above the footer instead of scrolling inside 220 or 280 pt (#2992; `the_list_fills_a_tall_panel_above_its_footer` in `crates/ui-egui/src/panels/library_panel/tests.rs`). Scores and estimates unchanged. | @@ -1044,7 +1045,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur With the Blob Brush, Alt held before the press, or pressed before the drag has moved, switches to the Smooth tool, as the reference app's [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) page lists: the cursor shows the Smooth tool, the drag shows a thin line and, with a path selected that it can smooth, the dashed band it smooths, without the length label, Shift does not constrain it, and the release smooths the selected paths under the drag (`path.smoothRegion`) instead of painting; with nothing selected the release changes no path. A stroke that has moved keeps painting when Alt goes down. Curvature point markers are circular, with no direction handles. While drawing, its own point feedback replaces generic selection markers; existing paths use the selection overlay, including partial selections and Hide Edges. Anchor Size applies, and Direct Selection and Pen retain their handles. The real egui-frame regressions in `crates/ui-egui/src/canvas.rs` verify marker positions, counts and unchanged documents (#1236, in part); the existing-path interpolation request remains open. The curve through the points (`curvature_spline` in `crates/tools/src/draw2.rs`) makes each stretch of three points follow the circle through them: a smooth point's handles lie along that circle's tangent, each `chord / (3 cos²(α/2))` long (α between handle and chord, capped at 90°), and the ends of an open path mirror their neighbour's tangent in their chord, so a J or a U curves evenly, a turn tighter than 30° comes to a point, and uneven spacing doesn't loop; the rubber band, a dragged or inserted point and `path.convertAnchor` to smooth use the same rule, while the Pencil's fitting keeps its own; `tests/draw2.rs` checks that the rubber band is the path the click makes (#1236, in part). The Smooth tool smooths only the stretch of path under the drag (#1547, fixes #1512). A path the Smooth tool changes stays selected whole, and the anchor points and segments selected on it with Direct Selection are deselected. Selected paths the drag does not change keep their selected anchors, and undo restores the earlier selection (`tests/draw2.rs`, `smoothing_a_path_drops_its_direct_selected_anchors`). While the Smooth tool drags with a path selected that it can smooth, a dashed outline shows the band it smooths: everything within its radius of the drag (12 screen pixels, VectorCraft's own value), round at both ends; Alt smoothing with the Pencil, Paintbrush and Blob Brush shows the same band. While Alt is held during a Paintbrush stroke or before a press with no path selected that the Smooth tool can act on, the Paintbrush shows a closing cursor (a crosshair with a ring), and so does the Pencil with Alt key toggles to Smooth Tool off when Alt is pressed after the drag starts; releasing the mouse button with Alt still held closes the path. Before a Paintbrush press with such a path selected, Alt shows the Smooth cursor, and holding Alt at the press smooths that path with the Smooth tool's radius and Smooth Tool Options' Fidelity. A Pencil drag started with Shift or Alt held draws a straight line instead, which stays open. - With Shift held at the press, the Pencil draws a straight line from the press point to the pointer at 45° steps from the Constrain Angle, as [Draw straight lines with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/draw-straight-lines-with-the-pencil-tool.html) describes; pressed on an end of a selected open path, the line continues the path from that end (part of #3799). With Option/Alt held at the press and Alt key toggles to Smooth Tool off, the Pencil draws a straight line at any angle, whose ends snap to Smart Guides, and it stays open when Alt is still held at the release; Shift and Alt together keep the 45° steps (#3799). While Shift or Alt is held before the press, the Pencil shows a straight-line cursor (a crosshair with a short line), and over an end of a selected open path, where the line continues the path, a path-continuation cursor (the same line leaving a small square), as the page describes. + With Shift held at the press, the Pencil draws a straight line from the press point to the pointer at 45° steps from the Constrain Angle, as [Draw straight lines with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/draw-straight-lines-with-the-pencil-tool.html) describes; pressed on an end of a selected open path, the line continues the path from that end (part of #3799). With Option/Alt held at the press and Alt key toggles to Smooth Tool off, the Pencil draws a straight line at any angle, whose ends snap to Smart Guides, and it stays open when Alt is still held at the release; Shift and Alt together keep the 45° steps (#3799). While Shift or Alt is held before the press, the Pencil shows a straight-line cursor (a crosshair with a short level line), as the page describes. Over an end of a selected open path, where the stroke continues the path, freehand or as a straight line, it shows a path-continuation cursor (a crosshair with a short line rising to the right), as [Extend paths with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/extend-paths-with-the-pencil-tool.html) describes, and again while a continuing stroke ends near an open end of another selected path, where the release connects the two ([Connect two paths with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/connect-two-paths-with-the-pencil-tool.html)). A continuing stroke, a straight line too, that ends within Close Paths When Ends Are Within of the path's other end shows the closing cursor, and the release closes the path; Alt held at the release closes it too. - **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools. Space held while dragging out a shape, line, arc, spiral or grid moves it at its current size. While a grid is dragged out, Up/Down change its rows (concentric dividers) and Left/Right its columns (radial dividers). A click with a grid tool asks for the size, the divider counts and the grid options (#1377, in part): a skew for each set of dividers, Use Outside Rectangle As Frame or Create Compound Path From Ellipses, and Fill Grid; they are parameters of `shape.rectangularGrid` and `shape.polarGrid`, stay as the tool's options and apply to drags too. A shape just drawn stays adjustable with its tool: its Live Shape widgets drag, a press on its centre point (Show Center) moves the selection with the Selection tool's move arrow, snapping and Alt copy, and a press in the rotate zone just outside a bounding-box corner turns it about the centre with the Selection tool's rotate cursor (#5477; tests `a_shape_tool_moves_the_selection_by_its_centre_point_and_turns_it_from_a_corner` and, through the engine, `shapetools.rs`). A drag on one of its eight bounding-box handles scales it as the Selection tool's handles do, with their cursors and snapping, instead of drawing a new shape from the handle. A shape tool takes a press as a handle's only on the drawn handle square (VectorCraft's own rule: 3 screen points from its centre along each axis at the default Anchor Size, a half point more or less per step; the Selection tool takes 5), so a press just beside a handle still draws (tests `a_shape_tool_scales_the_selection_by_its_bounding_box_handles`, `a_shape_tool_draws_from_a_press_just_beside_a_handle` and `a_shape_tool_scales_the_shape_just_drawn_by_its_bounding_box_handle`). The curve a skew percentage follows and the direction of the radial skew are our own (the public documentation gives neither), and the dialogs have no reference point locator. An ellipse's Pie Start and End Angle and Invert Pie (Transform and Properties panels' Ellipse Properties, `object.setLiveShape {pieStart, pieEnd, invertPie}`) cut it as a pie (#812). With the bounding box showing, the Selection tool shows a selected ellipse's two pie widgets just outside it; dragging one sets its angle in one undo step and double-clicking one makes the ellipse whole again. A live polygon's Polygon Properties there: side count, angle, corner type and radius, radius, side length and Make Sides Equal once an uneven scale has made its sides differ (`object.setLiveShape {sides, polygonAngle, polygonRadius, sideLength, makeSidesEqual}`, part of #812); a star is a plain path, as in Illustrator. The Flare Tool Options last as the tool's options: the next flare dragged out uses them, a click's dialog shows them and double-clicking the tool opens them (part of #812). - **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools. Space held while dragging out a shape, line, arc, spiral or grid moves it at its current size. The Flare tool's center drag turns the rays by 0.1 radian at each drag update in which the pointer's document y grows, and back by as much at each one in which it falls, Shift held keeps their turn, and Space held during either of its drags moves the flare. While a grid is dragged out, Up/Down change its rows (concentric dividers) and Left/Right its columns (radial dividers). A click with a grid tool asks for the size, the divider counts and the grid options (#1377, in part): a skew for each set of dividers, Use Outside Rectangle As Frame or Create Compound Path From Ellipses, and Fill Grid; they are parameters of `shape.rectangularGrid` and `shape.polarGrid`, stay as the tool's options and apply to drags too. The curve a skew percentage follows and the direction of the radial skew are our own (the public documentation gives neither), and the dialogs have no reference point locator. An ellipse's Pie Start and End Angle and Invert Pie (Transform and Properties panels' Ellipse Properties, `object.setLiveShape {pieStart, pieEnd, invertPie}`) cut it as a pie (#812). With the bounding box showing, the Selection tool shows a selected ellipse's two pie widgets just outside it; dragging one sets its angle in one undo step and double-clicking one makes the ellipse whole again. A live polygon's Polygon Properties there: side count, angle, corner type and radius, radius, side length and Make Sides Equal once an uneven scale has made its sides differ (`object.setLiveShape {sides, polygonAngle, polygonRadius, sideLength, makeSidesEqual}`, part of #812); a star is a plain path, as in Illustrator. The Flare Tool Options last as the tool's options: the next flare dragged out uses them, a click's dialog shows them and double-clicking the tool opens them (part of #812). - **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools. Space held while dragging out a shape, line, arc, spiral or grid moves it at its current size. The Flare tool's center drag turns the rays with the pointer, Shift keeps them in their original orientation (unturned, or turned by a nonzero General › Constrain Angle), and Space held during either of its drags moves the flare. While a grid is dragged out, Up/Down change its rows (concentric dividers) and Left/Right its columns (radial dividers). A click with a grid tool asks for the size, the divider counts and the grid options (#1377, in part): a skew for each set of dividers, Use Outside Rectangle As Frame or Create Compound Path From Ellipses, and Fill Grid; they are parameters of `shape.rectangularGrid` and `shape.polarGrid`, stay as the tool's options and apply to drags too. The curve a skew percentage follows and the direction of the radial skew are our own (the public documentation gives neither), and the dialogs have no reference point locator. An ellipse's Pie Start and End Angle and Invert Pie (Transform and Properties panels' Ellipse Properties, `object.setLiveShape {pieStart, pieEnd, invertPie}`) cut it as a pie (#812). With the bounding box showing, the Selection tool shows a selected ellipse's two pie widgets just outside it; dragging one sets its angle in one undo step and double-clicking one makes the ellipse whole again. A live polygon's Polygon Properties there: side count, angle, corner type and radius, radius, side length and Make Sides Equal once an uneven scale has made its sides differ (`object.setLiveShape {sides, polygonAngle, polygonRadius, sideLength, makeSidesEqual}`, part of #812); a star is a plain path, as in Illustrator. The Flare Tool Options last as the tool's options: the next flare dragged out uses them, a click's dialog shows them and double-clicking the tool opens them (part of #812). diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 974fbb440e..a7adedb22d 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-13 · **Change:** minor (Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: macOS: the canvas cursor stays after the pointer moves, #6362; scores and estimates unchanged; previously minor: Remove Background in the Contextual Task Bar, Properties › Quick Actions and the Control bar; previously minor: Ruler origin: the corner drag snaps as a drawn point does; previously minor: Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: Outline view: images and gradient meshes are picked on what Outline draws, so a click inside them reaches the path behind, #6437; previously minor: a middle-click on a document tab closes it, #6506; previously minor: Blob Brush: a round tip's preview is the outline of the area it adds, round at both ends, as the stroke it commits (#4343); scores and estimates unchanged; previously:; ruler double-click adds a guide at the pointer, #5239; G5: Inventory Icons built-in symbol library (four built-ins); previously minor: files that record no units open in Preferences › Units › General, #5820; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; shape tools: a press on the selection's centre point moves it and one in the rotate zone outside a box corner turns it, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-13 · **Change:** minor (Pencil continuation, straight-line and closing cursors; scores and estimates unchanged; previously: Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: macOS: the canvas cursor stays after the pointer moves, #6362; scores and estimates unchanged; previously minor: Remove Background in the Contextual Task Bar, Properties › Quick Actions and the Control bar; previously minor: Ruler origin: the corner drag snaps as a drawn point does; previously minor: Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: Outline view: images and gradient meshes are picked on what Outline draws, so a click inside them reaches the path behind, #6437; previously minor: a middle-click on a document tab closes it, #6506; previously minor: Blob Brush: a round tip's preview is the outline of the area it adds, round at both ends, as the stroke it commits (#4343); scores and estimates unchanged; previously:; ruler double-click adds a guide at the pointer, #5239; G5: Inventory Icons built-in symbol library (four built-ins); previously minor: files that record no units open in Preferences › Units › General, #5820; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; shape tools: a press on the selection's centre point moves it and one in the rotate zone outside a box corner turns it, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) > > Vector Halftone preview update (2026-11-10): temporary dot selection marks are hidden, and the bounding box stays available (#6369). > @@ -51,6 +51,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-13 | minor | Pencil cursors: the continuation cursor (a short rising line) shows over an open end of a selected path before any press, freehand too, and while a continuing stroke nears another selected path's open end it connects to; the straight-line cursor is a short level line; a continued path, a straight line too, shows the closing cursor and closes within Close Paths When Ends Are Within of its other end, and with Alt at the release. Scores and estimates unchanged. | | 2026-11-13 | minor | Graphic Styles: in a panel made taller the list fills the space above the footer instead of stopping at 220 pt (#2992; `the_list_fills_a_tall_panel_above_its_footer` in `crates/ui-egui/src/panels/graphic_styles.rs`). Scores and estimates unchanged. | | 2026-11-13 | minor | Library panels (swatch, Color Book, brush, symbol and graphic style libraries): in a panel made taller the list fills the space above the footer instead of scrolling inside 220 or 280 pt (#2992; `the_list_fills_a_tall_panel_above_its_footer` in `crates/ui-egui/src/panels/library_panel/tests.rs`). Scores and estimates unchanged. | | 2026-11-13 | minor | Swatches: in a panel made taller the swatch list fills the space above the footer instead of scrolling inside 150 or 200 pt (#1638). Scores unchanged. | @@ -177,7 +178,7 @@ tools, never checked side by side) isn't captured row by row. | Blend spine Anchor Point conversion and handle editing (#2544) | implemented | symmetric handles, independent drag/retraction, visible-handle policy and selection/stacking covered by tool and engine regressions; Windows native canvas checks and screenshots reviewed in PR #2643 | | Several blend spine points picked and dragged together (#5285) | done | with Direct Selection, Shift-click picks more points of a selected blend's spine or takes one away, as on a path; dragging any picked point moves them all, and the key objects on them, in one undo step (`object.blend.spine.moveAnchor {with}`); a marquee doesn't pick spine points yet | | Add/Delete/Anchor Point tools; Anchor Point tool drags segments to reshape | done | #2267: a click on a handle end retracts that handle and leaves the anchor a corner with the other one; Shift held at the press on an anchor moves it, handles and all (Shift kept down: at 45° steps) instead of pulling out handles | -| Pencil: Fidelity, Keep selected, Alt to Smooth, edit selected paths, close by proximity | done | Fidelity is in screen pixels, the same at any zoom, and goes down to 0.01 px in the dialog and the tool, which sends `path.freehand` the value in points at the current zoom (#5729; the Smooth tool keeps 0.05 pt); its slider runs across the Tool Options row on a logarithmic rail from 0.01 to 20 px, the field shows hundredths, and the slider's and the field's arrow keys step hundredths up to 0.1 px, tenths up to 1 px and whole pixels above (Shift ×10) (#3328); Alt key toggles to Smooth Tool (#2001); Shift held at the press draws a straight line at 45° steps from the Constrain Angle, and Alt held at the press (with Alt key toggles to Smooth Tool off) a straight line at any angle (#3799), with a straight-line cursor while Shift or Alt is held before the press, and a path-continuation cursor over an end of a selected open path, where the line continues the path; Edit Selected Paths continues a selected path from an end and, from a stroke that starts and ends on a selected path, redraws the section between (the shorter way round a closed path, #2850); with Keep selected (Pencil and Paintbrush, on by default) on, a stroke selects the path it draws or continues and replaces the earlier selection, and off a new stroke leaves nothing selected, the earlier selection included, while a continued or redrawn path is deselected and the rest of the selection stays; a filled Pencil path keeps a stroke of None (#1804, part of #1679); while a freehand stroke is drawn back to within Close Paths When Ends Are Within of its start, the Pencil and the Paintbrush show the closing cursor, and a release there closes the path; a Pencil stroke from an end of one selected open path to an end of another connects the two into one path | +| Pencil: Fidelity, Keep selected, Alt to Smooth, edit selected paths, close by proximity | done | Fidelity is in screen pixels, the same at any zoom, and goes down to 0.01 px in the dialog and the tool, which sends `path.freehand` the value in points at the current zoom (#5729; the Smooth tool keeps 0.05 pt); its slider runs across the Tool Options row on a logarithmic rail from 0.01 to 20 px, the field shows hundredths, and the slider's and the field's arrow keys step hundredths up to 0.1 px, tenths up to 1 px and whole pixels above (Shift ×10) (#3328); Alt key toggles to Smooth Tool (#2001); Shift held at the press draws a straight line at 45° steps from the Constrain Angle, and Alt held at the press (with Alt key toggles to Smooth Tool off) a straight line at any angle (#3799), with a straight-line cursor (a short level line) while Shift or Alt is held before the press, and a path-continuation cursor (a short rising line) over an end of a selected open path, where the stroke continues the path, freehand or straight, and while a continuing stroke ends near an open end of another selected path it connects to; Edit Selected Paths continues a selected path from an end and, from a stroke that starts and ends on a selected path, redraws the section between (the shorter way round a closed path, #2850); with Keep selected (Pencil and Paintbrush, on by default) on, a stroke selects the path it draws or continues and replaces the earlier selection, and off a new stroke leaves nothing selected, the earlier selection included, while a continued or redrawn path is deselected and the rest of the selection stays; a filled Pencil path keeps a stroke of None (#1804, part of #1679); while a freehand stroke is drawn back to within Close Paths When Ends Are Within of its start, or a stroke continuing a selected path (a straight line too) to within it of that path's other end, the Pencil and the Paintbrush show the closing cursor, and a release there closes the path; a Pencil stroke from an end of one selected open path to an end of another connects the two into one path | | Drop Shadow, Outer Glow and Inner Glow: Opacity, the offsets, Blur and Darkness have spinner arrows that step by one (a unit for distances), repeat when held and stop at the fields' ends (#5367, #5368) | done | dialogs::effect test drop_shadow_and_outer_glow_fields_have_spinner_arrows; other effect dialogs' number fields still have none | | Effect dialogs' colour fields: a swatch beside the hex follows typed values, and a double-click opens the Color Picker over the dialog, whose OK fills it and Cancel comes back unchanged (#5365) | done | dialogs::effect tests outer_glow_colour_swatch_opens_the_color_picker and drop_shadow_colour_swatch_opens_the_color_picker (Drop Shadow's Color, #5364); the picker previews nothing itself, the effect dialog previews the returned colour | | Join tool scrubbing, Average, Join | done | Join merges the direct-selected anchors lying on one another along a path, as after Average, into one (#1533); the Join tool trims two crossing open paths whose overhanging ends it scrubs over and joins them at the crossing, instead of drawing a segment between their ends (#5844) | From 8a449cb98edf71e59289a414f150e21435a07301 Mon Sep 17 00:00:00 2001 From: ncic <1848265+ncic@users.noreply.github.com> Date: Sat, 14 Nov 2026 01:45:40 +0100 Subject: [PATCH 2/4] Pencil: badged cursors keep their badge with Use Precise Cursors The Pencil's and the Paintbrush's new-path, continue, close and straight-line cursors are drawn on the crosshair already; Precise Cursors no longer turns them into the plain crosshair, so the badge still says what the press or the release does. --- crates/tools/src/cursor.rs | 9 +++------ crates/tools/src/cursors.toml | 13 +++++++++++-- crates/tools/src/draw2/gesture/tests.rs | 4 ++-- crates/tools/src/lib.rs | 5 ++++- docs/ui-parity.md | 2 +- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/crates/tools/src/cursor.rs b/crates/tools/src/cursor.rs index 85b9cf258e..56e2cd2ae7 100644 --- a/crates/tools/src/cursor.rs +++ b/crates/tools/src/cursor.rs @@ -259,7 +259,9 @@ impl Cursor { /// General › Use Precise Cursors: the drawing tools' pointers (the Pen's in every state, the /// Anchor Point and Curvature tools', the Eyedropper's, the Slice and Blend tools', the Live - /// Paint tools') become a plain crosshair at the hotspot. + /// Paint tools') become a plain crosshair at the hotspot. The Pencil's and the Paintbrush's + /// badged cursors are drawn on the crosshair already and keep their badge, which says what the + /// press or the release does (a new path, continuing, closing, a straight line). pub fn precise(self) -> Self { match self { Cursor::Pen @@ -282,12 +284,7 @@ impl Cursor { | Cursor::Lasso | Cursor::MagicWand | Cursor::Pencil - | Cursor::PencilNew - | Cursor::PencilClose - | Cursor::PencilStraight - | Cursor::PencilContinue | Cursor::Paintbrush - | Cursor::PaintbrushClose | Cursor::BlobBrush | Cursor::Eraser | Cursor::Smooth diff --git a/crates/tools/src/cursors.toml b/crates/tools/src/cursors.toml index 1c15b37d71..efa31a515d 100644 --- a/crates/tools/src/cursors.toml +++ b/crates/tools/src/cursors.toml @@ -628,7 +628,10 @@ hotspot = "The lead tip." [[cursor]] id = "PencilNew" group = "Drawing and editing tools" -shown_when = ["The Pencil tool before a press that draws a new freehand path: the pointer is not within Edit Selected Paths of a selected path that the press would continue from an end or redraw. Over such a path, and while a stroke is drawn, the Pencil cursor has no star."] +shown_when = [ + "The Pencil tool before a press that draws a new freehand path: the pointer is not within Edit Selected Paths of a selected path that the press would continue from an end or redraw. Over such a path, and while a stroke is drawn, the Pencil cursor has no star.", + "With Use Precise Cursors on it shows as well: it is drawn on the crosshair, and the badge stays.", +] hotspot = "The lead tip." look = "The Pencil cursor with a small star badge, where PencilClose has its ring." @@ -639,6 +642,7 @@ shown_when = [ "The Pencil tool with Alt pressed after the drag starts, when Alt key toggles to Smooth Tool is off: releasing the mouse button with Alt still held closes the path. A drag started with Shift or Alt held draws an open straight line and does not show this cursor.", "The Pencil tool while a freehand stroke is drawn back to within Close Paths When Ends Are Within of its start: releasing the mouse button there closes the path.", "The Pencil tool while a stroke continuing a selected open path, freehand or a straight line, is drawn to within Close Paths When Ends Are Within of that path's other end: releasing the mouse button there closes the path.", + "With Use Precise Cursors on it shows as well: it is drawn on the crosshair, and the badge stays.", ] hotspot = "The lead tip." look = "The Pencil cursor with a ring badge (closing the loop), like PenClose." @@ -646,7 +650,10 @@ look = "The Pencil cursor with a ring badge (closing the loop), like PenClose." [[cursor]] id = "PencilStraight" group = "Drawing and editing tools" -shown_when = ["The Pencil tool with Shift or Alt held before a press, except with Alt held when Alt key toggles to Smooth Tool is on: the press draws a straight line, at 45° steps from the Constrain Angle when Shift is held and at any angle with Alt alone."] +shown_when = [ + "The Pencil tool with Shift or Alt held before a press, except with Alt held when Alt key toggles to Smooth Tool is on: the press draws a straight line, at 45° steps from the Constrain Angle when Shift is held and at any angle with Alt alone.", + "With Use Precise Cursors on it shows as well: it is drawn on the crosshair, and the badge stays.", +] hotspot = "The lead tip." look = "The Pencil cursor with a short level-line badge, where PencilClose has its ring." @@ -656,6 +663,7 @@ group = "Drawing and editing tools" shown_when = [ "The Pencil tool over an end of a selected open path, within Edit Selected Paths, before a press, except with Alt held when Alt key toggles to Smooth Tool is on: the press continues the path from that end, freehand, or with a straight line when Shift or Alt is held.", "The Pencil tool while a stroke continuing a selected path ends within Edit Selected Paths of an open end of another path selected at the press: releasing the mouse button connects the two.", + "With Use Precise Cursors on it shows as well: it is drawn on the crosshair, and the badge stays.", ] hotspot = "The lead tip." look = "The Pencil cursor with a short badge line rising to the right, where PencilClose has its ring (the stroke carries on from the path)." @@ -672,6 +680,7 @@ group = "Drawing and editing tools" shown_when = [ "The Paintbrush tool with Alt held while drawing, or before a press when no path is selected that the Smooth tool can act on (with one selected, Smooth shows): releasing the mouse button with Alt still held closes the path.", "The Paintbrush tool while a stroke is drawn back to within Close Paths When Ends Are Within of its start: releasing the mouse button there closes the path.", + "With Use Precise Cursors on it shows as well: it is drawn on the crosshair, and the badge stays.", ] hotspot = "The bristle tip." look = "The Paintbrush cursor with a ring badge (closing the loop), like PenClose." diff --git a/crates/tools/src/draw2/gesture/tests.rs b/crates/tools/src/draw2/gesture/tests.rs index 65ad5f00e7..8ff1ac3d67 100644 --- a/crates/tools/src/draw2/gesture/tests.rs +++ b/crates/tools/src/draw2/gesture/tests.rs @@ -748,7 +748,7 @@ fn pencil_shows_the_straight_line_cursor_when_a_press_draws_a_line() { t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 40.0, -10.0).with_mods(shift)); assert_eq!(t.cursor(&cx, at, shift), Cursor::Pencil, "a freehand stroke"); assert_eq!(GestureTool::new("paintbrush").cursor(&cx, at, shift), Cursor::Paintbrush); - assert_eq!(Cursor::PencilStraight.precise(), Cursor::Crosshair, "Use Precise Cursors"); + assert_eq!(Cursor::PencilStraight.precise(), Cursor::PencilStraight, "Use Precise Cursors keeps the badge"); } /// Before a press, the Pencil shows its new-path cursor (the star) exactly when the press draws @@ -883,7 +883,7 @@ fn pencil_shows_the_continuation_cursor_over_an_end_a_line_continues() { let cx_none = crate::testutil::cx(&d, &none, &p); assert_eq!(GestureTool::new("pencil").cursor(&cx_none, end, shift), Cursor::PencilStraight, "nothing selected"); assert_eq!(GestureTool::new("paintbrush").cursor(&cx, end, shift), Cursor::Paintbrush); - assert_eq!(Cursor::PencilContinue.precise(), Cursor::Crosshair, "Use Precise Cursors"); + assert_eq!(Cursor::PencilContinue.precise(), Cursor::PencilContinue, "Use Precise Cursors keeps the badge"); } /// A Pencil stroke continuing a selected path shows the continuation cursor while a release would diff --git a/crates/tools/src/lib.rs b/crates/tools/src/lib.rs index 17e6f038e9..1a629d7ab8 100644 --- a/crates/tools/src/lib.rs +++ b/crates/tools/src/lib.rs @@ -660,7 +660,10 @@ mod cursor_tests { // Away from a selected path the Pencil shows its new-path cursor, drawn with a star; its // own cursor, for drawing and for a selected path under the pointer, borrows the crosshair. assert_eq!(cursor_at("pencil", &d, away), Cursor::PencilNew); - assert_eq!(Cursor::PencilNew.precise(), Cursor::Crosshair); + // Its badged cursors are drawn on the crosshair, and Use Precise Cursors keeps the badge. + for c in [Cursor::PencilNew, Cursor::PencilClose, Cursor::PencilStraight, Cursor::PencilContinue, Cursor::PaintbrushClose] { + assert_eq!(c.precise(), c, "{c:?}"); + } assert_eq!(Cursor::PencilNew.stand_in(), None); assert_eq!(Cursor::Pencil.precise(), Cursor::Crosshair); assert_eq!(Cursor::Pencil.stand_in(), Some(Cursor::Crosshair)); diff --git a/docs/ui-parity.md b/docs/ui-parity.md index a7adedb22d..8b97b1a88b 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -341,7 +341,7 @@ tools, never checked side by side) isn't captured row by row. | Polygon/Star exact-size placement remembers the chosen sides/points for later drags and dialog openings; Cancel preserves the previous count | done | Session tool options; existing dialog-opening gestures retained; regression in `toolbar.rs` | | Tool double-click opens its options | done | | | Per-platform mapping (Cmd ↔ Ctrl, Option ↔ Alt) | done | | -| Eyedropper cursor; Use Precise Cursors | partial | original pipette at the sampling tip, distinct from the precise crosshair at five DPI scales (#1269); OS cursor display unverified; while Caps Lock is on, the tools that Use Precise Cursors changes show the crosshair, and the saved preference does not change (the desktop app reads the lock state on macOS, Windows and Linux X11, not on native Wayland; `view.capsLock` reads or sets it); a Use Precise Cursors command toggles the preference and can take a shortcut (#1663); cursors are painted into the window on Wayland (#1652, not run on Linux) | +| Eyedropper cursor; Use Precise Cursors | partial | original pipette at the sampling tip, distinct from the precise crosshair at five DPI scales (#1269); OS cursor display unverified; while Caps Lock is on, the tools that Use Precise Cursors changes show the crosshair, and the saved preference does not change; the Pencil's and the Paintbrush's cursors with a badge (new path, continue, close, straight line) are drawn on the crosshair and keep their badge (the desktop app reads the lock state on macOS, Windows and Linux X11, not on native Wayland; `view.capsLock` reads or sets it); a Use Precise Cursors command toggles the preference and can take a shortcut (#1663); cursors are painted into the window on Wayland (#1652, not run on Linux) | | macOS canvas cursor delivery (#2292) | partial | The UI prepares the existing glyphs as aligned 1×/2× RGBA requests; the desktop host builds AppKit cursors and owns cursor rectangles for the root document window. While it owns the cursor, the host sets it again on every pass, so the tool's cursor stays when the pointer moves (#6362). Detached document viewports and requests without a usable native bitmap retain the visible system fallback; precise-cursor and UI-scale preferences retained. Headless regressions cover routing, pixels/hotspots and lifecycle; native focus/menu/handback timing, mixed displays, Spotlight and full-screen Spaces remain unverified. Wayland still paints its glyphs. | | Every tool's modifiers checked against the shortcut reference | missing | this is the fidelity pass | 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 3/4] 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] From fc47e4782b56f3b0649c2b1a3f4fb519607cf7d1 Mon Sep 17 00:00:00 2001 From: ncic <1848265+ncic@users.noreply.github.com> Date: Sat, 14 Nov 2026 13:23:22 +0100 Subject: [PATCH 4/4] Pencil: Use Precise Cursors shows the plain crosshair again; a far close is a straight segment --- crates/engine/src/cmd/draw2/freehand.rs | 8 ++++++++ crates/engine/src/tests/draw2.rs | 16 ++++++++++++++++ crates/engine/src/tests/prefs.rs | 12 +++++------- crates/tools/src/cursor.rs | 9 ++++++--- crates/tools/src/cursors.toml | 5 ----- crates/tools/src/draw2/gesture/tests.rs | 4 ++-- crates/tools/src/lib.rs | 4 ++-- docs/target-app-parity.md | 4 ++-- docs/ui-parity.md | 4 ++-- 9 files changed, 43 insertions(+), 23 deletions(-) diff --git a/crates/engine/src/cmd/draw2/freehand.rs b/crates/engine/src/cmd/draw2/freehand.rs index b8dd92eb6a..401bdd2977 100644 --- a/crates/engine/src/cmd/draw2/freehand.rs +++ b/crates/engine/src/cmd/draw2/freehand.rs @@ -253,7 +253,15 @@ pub(super) fn freehand(s: &mut Session, p: &Value) -> Result { a0.h_in = l.h_in; sp.closed = true; } else if closing && !ends_on_first && sp.anchors.len() > 2 { + // Closed from away from the other end: a straight segment back to it, whatever + // handles the open ends carried. sp.closed = true; + if let Some(l) = sp.anchors.last_mut() { + l.h_out = l.p; + } + if let Some(f) = sp.anchors.first_mut() { + f.h_in = f.p; + } } for a in &mut sp.anchors { *a = Anchor::with_handles(a.p, a.h_in, a.h_out); diff --git a/crates/engine/src/tests/draw2.rs b/crates/engine/src/tests/draw2.rs index 50b2cea177..b230605cdc 100644 --- a/crates/engine/src/tests/draw2.rs +++ b/crates/engine/src/tests/draw2.rs @@ -5203,6 +5203,22 @@ fn pencil_extend_closes_as_closed_says() { } } +/// A continued path closed from away from its other end closes with a straight segment, even when +/// that end carries a handle the open path didn't use. +#[test] +fn pencil_extend_closes_far_away_with_a_straight_segment() { + let mut s = session(); + let anchors = json!([{"x": 0, "y": 0, "in": [-40, 30]}, {"x": 200, "y": 0}]); + let l = NodeId(s.execute("path.create", &json!({"anchors": anchors})).unwrap()["id"].as_u64().unwrap()); + let points = json!([[200, 0], [200, 100], [100, 150], [0, 50]]); + let p = json!({"points": points, "fidelity": 1.5, "closed": true, "extend": {"id": l.0}, "keepSelected": false}); + s.execute("path.freehand", &p).unwrap(); + let sp = path(&s, l).subpaths[0].clone(); + assert!(sp.closed); + let (first, last) = (sp.anchors[0], sp.anchors[sp.anchors.len() - 1]); + assert_eq!((first.h_in, last.h_out), (first.p, last.p), "the closing segment has no handles: {:?}", sp.anchors); +} + /// At Fidelity 0.01 short, one-point and repeated-point strokes still draw or are refused, never /// crash, and what they draw has finite anchors. #[test] diff --git a/crates/engine/src/tests/prefs.rs b/crates/engine/src/tests/prefs.rs index 67a740d4a3..d88b8250a2 100644 --- a/crates/engine/src/tests/prefs.rs +++ b/crates/engine/src/tests/prefs.rs @@ -723,10 +723,8 @@ 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, 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 own { + assert_eq!(cursor(&mut s, tool), Cursor::Crosshair, "{tool} on"); } for tool in always { assert_eq!(cursor(&mut s, tool), Cursor::Crosshair, "{tool} on"); @@ -760,8 +758,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. Use Precise Cursors keeps -/// them: they are drawn on the crosshair, and the ring says the release closes the path. +/// With Alt held while drawing, the Pencil and the Paintbrush show their closing cursors, and Use Precise Cursors +/// turns those into a crosshair too. #[test] fn alt_shows_the_pencil_and_paintbrush_closing_cursors() { use vectorcraft_tools::{Cursor, Mods, PointerEvent, PointerKind}; @@ -775,7 +773,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()), c, "{tool}, precise {precise}"); + assert_eq!(s.cursor(at, alt, ViewInfo::default()), if precise { Cursor::Crosshair } else { c }, "{tool}, precise {precise}"); s.pointer(&PointerEvent::new(PointerKind::Up, 300.0, 300.0), ViewInfo::default()).unwrap(); } } diff --git a/crates/tools/src/cursor.rs b/crates/tools/src/cursor.rs index 56e2cd2ae7..85b9cf258e 100644 --- a/crates/tools/src/cursor.rs +++ b/crates/tools/src/cursor.rs @@ -259,9 +259,7 @@ impl Cursor { /// General › Use Precise Cursors: the drawing tools' pointers (the Pen's in every state, the /// Anchor Point and Curvature tools', the Eyedropper's, the Slice and Blend tools', the Live - /// Paint tools') become a plain crosshair at the hotspot. The Pencil's and the Paintbrush's - /// badged cursors are drawn on the crosshair already and keep their badge, which says what the - /// press or the release does (a new path, continuing, closing, a straight line). + /// Paint tools') become a plain crosshair at the hotspot. pub fn precise(self) -> Self { match self { Cursor::Pen @@ -284,7 +282,12 @@ impl Cursor { | Cursor::Lasso | Cursor::MagicWand | Cursor::Pencil + | Cursor::PencilNew + | Cursor::PencilClose + | Cursor::PencilStraight + | Cursor::PencilContinue | Cursor::Paintbrush + | Cursor::PaintbrushClose | Cursor::BlobBrush | Cursor::Eraser | Cursor::Smooth diff --git a/crates/tools/src/cursors.toml b/crates/tools/src/cursors.toml index db189adfc8..51756bf0fd 100644 --- a/crates/tools/src/cursors.toml +++ b/crates/tools/src/cursors.toml @@ -631,7 +631,6 @@ id = "PencilNew" group = "Drawing and editing tools" shown_when = [ "The Pencil tool before a press that draws a new freehand path: the pointer is not within Edit Selected Paths of a selected path that the press would continue from an end or redraw. Over such a path, and while a stroke is drawn, the Pencil cursor has no star.", - "With Use Precise Cursors on it shows as well: it is drawn on the crosshair, and the badge stays.", ] hotspot = "The lead tip." look = "The Pencil cursor with a small star badge, where PencilClose has its ring." @@ -643,7 +642,6 @@ shown_when = [ "The Pencil tool with Alt pressed after the drag starts, when Alt key toggles to Smooth Tool is off: releasing the mouse button with Alt still held closes the path. A drag started with Shift or Alt held draws an open straight line and does not show this cursor.", "The Pencil tool while a freehand stroke is drawn back to within Close Paths When Ends Are Within of its start: releasing the mouse button there closes the path.", "The Pencil tool while a stroke continuing a selected open path, freehand or a straight line, is drawn to within Close Paths When Ends Are Within of that path's other end: releasing the mouse button there closes the path.", - "With Use Precise Cursors on it shows as well: it is drawn on the crosshair, and the badge stays.", ] hotspot = "The lead tip." look = "The Pencil cursor with a ring badge (closing the loop), like PenClose." @@ -653,7 +651,6 @@ id = "PencilStraight" group = "Drawing and editing tools" shown_when = [ "The Pencil tool with Shift or Alt held before a press, except with Alt held when Alt key toggles to Smooth Tool is on: the press draws a straight line, at 45° steps from the Constrain Angle when Shift is held and at any angle with Alt alone.", - "With Use Precise Cursors on it shows as well: it is drawn on the crosshair, and the badge stays.", ] hotspot = "The lead tip." look = "The Pencil cursor with a short level-line badge, where PencilClose has its ring." @@ -664,7 +661,6 @@ group = "Drawing and editing tools" shown_when = [ "The Pencil tool over an end of a selected open path, within Edit Selected Paths, before a press, except with Alt held when Alt key toggles to Smooth Tool is on: the press continues the path from that end, freehand, or with a straight line when Shift or Alt is held.", "The Pencil tool while a stroke continuing a selected path ends within Edit Selected Paths of an open end of another path selected at the press: releasing the mouse button connects the two.", - "With Use Precise Cursors on it shows as well: it is drawn on the crosshair, and the badge stays.", ] hotspot = "The lead tip." look = "The Pencil cursor with a short badge line rising to the right, where PencilClose has its ring (the stroke carries on from the path)." @@ -681,7 +677,6 @@ group = "Drawing and editing tools" shown_when = [ "The Paintbrush tool with Alt held while drawing, or before a press when no path is selected that the Smooth tool can act on (with one selected, Smooth shows): releasing the mouse button with Alt still held closes the path.", "The Paintbrush tool while a stroke is drawn back to within Close Paths When Ends Are Within of its start: releasing the mouse button there closes the path.", - "With Use Precise Cursors on it shows as well: it is drawn on the crosshair, and the badge stays.", ] hotspot = "The bristle tip." look = "The Paintbrush cursor with a ring badge (closing the loop), like PenClose." diff --git a/crates/tools/src/draw2/gesture/tests.rs b/crates/tools/src/draw2/gesture/tests.rs index 2159b8af2c..33d6520b84 100644 --- a/crates/tools/src/draw2/gesture/tests.rs +++ b/crates/tools/src/draw2/gesture/tests.rs @@ -756,7 +756,7 @@ fn pencil_shows_the_straight_line_cursor_when_a_press_draws_a_line() { t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 40.0, -10.0).with_mods(shift)); assert_eq!(t.cursor(&cx, at, shift), Cursor::Pencil, "a freehand stroke"); assert_eq!(GestureTool::new("paintbrush").cursor(&cx, at, shift), Cursor::Paintbrush); - assert_eq!(Cursor::PencilStraight.precise(), Cursor::PencilStraight, "Use Precise Cursors keeps the badge"); + assert_eq!(Cursor::PencilStraight.precise(), Cursor::Crosshair, "Use Precise Cursors"); } /// Before a press, the Pencil shows its new-path cursor (the star) exactly when the press draws @@ -891,7 +891,7 @@ fn pencil_shows_the_continuation_cursor_over_an_end_a_line_continues() { let cx_none = crate::testutil::cx(&d, &none, &p); assert_eq!(GestureTool::new("pencil").cursor(&cx_none, end, shift), Cursor::PencilStraight, "nothing selected"); assert_eq!(GestureTool::new("paintbrush").cursor(&cx, end, shift), Cursor::Paintbrush); - assert_eq!(Cursor::PencilContinue.precise(), Cursor::PencilContinue, "Use Precise Cursors keeps the badge"); + assert_eq!(Cursor::PencilContinue.precise(), Cursor::Crosshair, "Use Precise Cursors"); } /// A Pencil stroke continuing a selected path shows the continuation cursor while a release would diff --git a/crates/tools/src/lib.rs b/crates/tools/src/lib.rs index ee75655b26..15fec287b5 100644 --- a/crates/tools/src/lib.rs +++ b/crates/tools/src/lib.rs @@ -673,9 +673,9 @@ mod cursor_tests { // Away from a selected path the Pencil shows its new-path cursor, drawn with a star; its // own cursor, for drawing and for a selected path under the pointer, borrows the crosshair. assert_eq!(cursor_at("pencil", &d, away), Cursor::PencilNew); - // Its badged cursors are drawn on the crosshair, and Use Precise Cursors keeps the badge. + // Its badged cursors too: Use Precise Cursors shows the plain crosshair. for c in [Cursor::PencilNew, Cursor::PencilClose, Cursor::PencilStraight, Cursor::PencilContinue, Cursor::PaintbrushClose] { - assert_eq!(c.precise(), c, "{c:?}"); + assert_eq!(c.precise(), Cursor::Crosshair, "{c:?}"); } assert_eq!(Cursor::PencilNew.stand_in(), None); assert_eq!(Cursor::Pencil.precise(), Cursor::Crosshair); diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 66e6c368ec..ebd46d944c 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -59,7 +59,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| | 2026-11-14 | minor | Pencil and Paintbrush: a closed stroke (Alt at the release, or ending within Close Paths When Ends Are Within of its start) is fitted as the open stroke would be and closed by a straight segment back to its start, instead of curving on past both ends (`pencil_alt_closes_a_stroke_straight_back_to_its_start` in `crates/engine/src/tests/draw2.rs`). Scores and estimates unchanged. | -| 2026-11-13 | minor | Pencil cursors: the continuation cursor (a short rising line) shows over an open end of a selected path before any press, freehand too, and while a continuing stroke nears another selected path's open end it connects to; the straight-line cursor is a short level line; a continued path, a straight line too, shows the closing cursor and closes within Close Paths When Ends Are Within of its other end, and with Alt at the release; with Use Precise Cursors (Caps Lock) the Pencil's and the Paintbrush's badged cursors keep their badge (`a_continued_stroke_shows_where_it_connects_or_closes` in `crates/tools/src/draw2/gesture/tests.rs`, `pencil_extend_closes_as_closed_says` in `crates/engine/src/tests/draw2.rs`). Scores and estimates unchanged. | +| 2026-11-13 | minor | Pencil cursors: the continuation cursor (a short rising line) shows over an open end of a selected path before any press, freehand too, and while a continuing stroke nears another selected path's open end it connects to; the straight-line cursor is a short level line; a continued path, a straight line too, shows the closing cursor and closes within Close Paths When Ends Are Within of its other end, and with Alt at the release (`a_continued_stroke_shows_where_it_connects_or_closes` in `crates/tools/src/draw2/gesture/tests.rs`, `pencil_extend_closes_as_closed_says` in `crates/engine/src/tests/draw2.rs`). Scores and estimates unchanged. | | 2026-11-13 | minor | Snap to Point: with Smart Guides off it still pulls drawn and dragged points onto anchors and ruler guides, but draws none of the Smart Guides labels ("anchor") or lines (#7250; `guides::tests::snap_to_point_without_smart_guides_shows_no_guides`). Scores and estimates unchanged. | | 2026-11-13 | minor | Graphic Styles: in a panel made taller the list fills the space above the footer instead of stopping at 220 pt (#2992; `the_list_fills_a_tall_panel_above_its_footer` in `crates/ui-egui/src/panels/graphic_styles.rs`). Scores and estimates unchanged. | @@ -1090,7 +1090,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Curvature point markers are circular, with no direction handles. While drawing, its own point feedback replaces generic selection markers; existing paths use the selection overlay, including partial selections and Hide Edges. Anchor Size applies, and Direct Selection and Pen retain their handles. The real egui-frame regressions in `crates/ui-egui/src/canvas.rs` verify marker positions, counts and unchanged documents (#1236, in part); the existing-path interpolation request remains open. The curve through the points (`curvature_spline` in `crates/tools/src/draw2.rs`) makes each stretch of three points follow the circle through them: a smooth point's handles lie along that circle's tangent, each `chord / (3 cos²(α/2))` long (α between handle and chord, capped at 90°), and the ends of an open path mirror their neighbour's tangent in their chord, so a J or a U curves evenly, a turn tighter than 30° comes to a point, and uneven spacing doesn't loop; the rubber band, a dragged or inserted point and `path.convertAnchor` to smooth use the same rule, while the Pencil's fitting keeps its own; `tests/draw2.rs` checks that the rubber band is the path the click makes (#1236, in part). The Smooth tool smooths only the stretch of path under the drag (#1547, fixes #1512). A path the Smooth tool changes stays selected whole, and the anchor points and segments selected on it with Direct Selection are deselected. Selected paths the drag does not change keep their selected anchors, and undo restores the earlier selection (`tests/draw2.rs`, `smoothing_a_path_drops_its_direct_selected_anchors`). While the Smooth tool drags with a path selected that it can smooth, a dashed outline shows the band it smooths: everything within its radius of the drag (12 screen pixels, VectorCraft's own value), round at both ends; Alt smoothing with the Pencil, Paintbrush and Blob Brush shows the same band. While Alt is held during a Paintbrush stroke or before a press with no path selected that the Smooth tool can act on, the Paintbrush shows a closing cursor (a crosshair with a ring), and so does the Pencil with Alt key toggles to Smooth Tool off when Alt is pressed after the drag starts; releasing the mouse button with Alt still held closes the path. Before a Paintbrush press with such a path selected, Alt shows the Smooth cursor, and holding Alt at the press smooths that path with the Smooth tool's radius and Smooth Tool Options' Fidelity. A Pencil drag started with Shift or Alt held draws a straight line instead, which stays open. Smooth Tool Options' Fidelity is in pixels, as [the help page](https://help.adobe.com/archive/en_US/illustrator/cs3/illustrator_cs3_help.pdf#page=79) gives it: the Smooth tool, and an Alt smoothing drag with the Pencil, Paintbrush or Blob Brush, refits the path at Fidelity divided by the zoom, so a drag smooths a path as much on screen at 25 % as at 800 %. Reading the pixels as screen pixels is VectorCraft's own rule, as for the Pencil and the Paintbrush (#5729); the range (0.05 to 100 px) and the default (2.5 px) are VectorCraft's own values, and a saved Fidelity keeps its number (`crates/tools/src/draw2/gesture/tests.rs` test `smooth_fidelity_is_in_screen_pixels_at_any_zoom`, `crates/engine/src/tests/draw2.rs` test `smooth_region_refits_at_the_tolerance_sent_at_any_zoom`). - With Shift held at the press, the Pencil draws a straight line from the press point to the pointer at 45° steps from the Constrain Angle, as [Draw straight lines with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/draw-straight-lines-with-the-pencil-tool.html) describes; pressed on an end of a selected open path, the line continues the path from that end (part of #3799). With Option/Alt held at the press and Alt key toggles to Smooth Tool off, the Pencil draws a straight line at any angle, whose ends snap to Smart Guides, and it stays open when Alt is still held at the release; Shift and Alt together keep the 45° steps (#3799). While Shift or Alt is held before the press, the Pencil shows a straight-line cursor (a crosshair with a short level line), as the page describes. Over an end of a selected open path, where the stroke continues the path, freehand or as a straight line, it shows a path-continuation cursor (a crosshair with a short line rising to the right), as [Extend paths with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/extend-paths-with-the-pencil-tool.html) describes, and again while a continuing stroke ends near an open end of another selected path, where the release connects the two ([Connect two paths with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/connect-two-paths-with-the-pencil-tool.html)). A continuing stroke, a straight line too, that ends within Close Paths When Ends Are Within of the path's other end shows the closing cursor, and the release closes the path; Alt held at the release closes it too. + With Shift held at the press, the Pencil draws a straight line from the press point to the pointer at 45° steps from the Constrain Angle, as [Draw straight lines with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/draw-straight-lines-with-the-pencil-tool.html) describes; pressed on an end of a selected open path, the line continues the path from that end (part of #3799). With Option/Alt held at the press and Alt key toggles to Smooth Tool off, the Pencil draws a straight line at any angle, whose ends snap to Smart Guides, and it stays open when Alt is still held at the release; Shift and Alt together keep the 45° steps (#3799). While Shift or Alt is held before the press, the Pencil shows a straight-line cursor (a crosshair with a short level line), as the page describes. Over an end of a selected open path, where the stroke continues the path, freehand or as a straight line, it shows a path-continuation cursor (a crosshair with a short line rising to the right), as [Extend paths with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/extend-paths-with-the-pencil-tool.html) describes, and again while a continuing stroke ends near an open end of another selected path, where the release connects the two ([Connect two paths with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/connect-two-paths-with-the-pencil-tool.html)). A continuing stroke, a straight line too, that ends within Close Paths When Ends Are Within of the path's other end shows the closing cursor, and the release closes the path; Alt held at the release closes a continuing freehand stroke too (a straight line stays open, as above). - **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools. Space held while dragging out a shape, line, arc, spiral or grid moves it at its current size. While a grid is dragged out, Up/Down change its rows (concentric dividers) and Left/Right its columns (radial dividers). A click with a grid tool asks for the size, the divider counts and the grid options (#1377, in part): a skew for each set of dividers, Use Outside Rectangle As Frame or Create Compound Path From Ellipses, and Fill Grid; they are parameters of `shape.rectangularGrid` and `shape.polarGrid`, stay as the tool's options and apply to drags too. A shape just drawn stays adjustable with its tool: its Live Shape widgets drag, a press on its centre point (Show Center) moves the selection with the Selection tool's move arrow, snapping and Alt copy, and a press in the rotate zone just outside a bounding-box corner turns it about the centre with the Selection tool's rotate cursor (#5477; tests `a_shape_tool_moves_the_selection_by_its_centre_point_and_turns_it_from_a_corner` and, through the engine, `shapetools.rs`). A drag on one of its eight bounding-box handles scales it as the Selection tool's handles do, with their cursors and snapping, instead of drawing a new shape from the handle. A shape tool takes a press as a handle's only on the drawn handle square (VectorCraft's own rule: 3 screen points from its centre along each axis at the default Anchor Size, a half point more or less per step; the Selection tool takes 5), so a press just beside a handle still draws (tests `a_shape_tool_scales_the_selection_by_its_bounding_box_handles`, `a_shape_tool_draws_from_a_press_just_beside_a_handle` and `a_shape_tool_scales_the_shape_just_drawn_by_its_bounding_box_handle`). The curve a skew percentage follows and the direction of the radial skew are our own (the public documentation gives neither), and the dialogs have no reference point locator. An ellipse's Pie Start and End Angle and Invert Pie (Transform and Properties panels' Ellipse Properties, `object.setLiveShape {pieStart, pieEnd, invertPie}`) cut it as a pie (#812). With the bounding box showing, the Selection tool shows a selected ellipse's two pie widgets just outside it; dragging one sets its angle in one undo step and double-clicking one makes the ellipse whole again. A live polygon's Polygon Properties there: side count, angle, corner type and radius, radius, side length and Make Sides Equal once an uneven scale has made its sides differ (`object.setLiveShape {sides, polygonAngle, polygonRadius, sideLength, makeSidesEqual}`, part of #812); a star is a plain path, as in Illustrator. The Flare Tool Options last as the tool's options: the next flare dragged out uses them, a click's dialog shows them and double-clicking the tool opens them (part of #812). - **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools. Space held while dragging out a shape, line, arc, spiral or grid moves it at its current size. The Flare tool's center drag turns the rays by 0.1 radian at each drag update in which the pointer's document y grows, and back by as much at each one in which it falls, Shift held keeps their turn, and Space held during either of its drags moves the flare. While a grid is dragged out, Up/Down change its rows (concentric dividers) and Left/Right its columns (radial dividers). A click with a grid tool asks for the size, the divider counts and the grid options (#1377, in part): a skew for each set of dividers, Use Outside Rectangle As Frame or Create Compound Path From Ellipses, and Fill Grid; they are parameters of `shape.rectangularGrid` and `shape.polarGrid`, stay as the tool's options and apply to drags too. The curve a skew percentage follows and the direction of the radial skew are our own (the public documentation gives neither), and the dialogs have no reference point locator. An ellipse's Pie Start and End Angle and Invert Pie (Transform and Properties panels' Ellipse Properties, `object.setLiveShape {pieStart, pieEnd, invertPie}`) cut it as a pie (#812). With the bounding box showing, the Selection tool shows a selected ellipse's two pie widgets just outside it; dragging one sets its angle in one undo step and double-clicking one makes the ellipse whole again. A live polygon's Polygon Properties there: side count, angle, corner type and radius, radius, side length and Make Sides Equal once an uneven scale has made its sides differ (`object.setLiveShape {sides, polygonAngle, polygonRadius, sideLength, makeSidesEqual}`, part of #812); a star is a plain path, as in Illustrator. The Flare Tool Options last as the tool's options: the next flare dragged out uses them, a click's dialog shows them and double-clicking the tool opens them (part of #812). - **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools. Space held while dragging out a shape, line, arc, spiral or grid moves it at its current size. The Flare tool's center drag turns the rays with the pointer, Shift keeps them in their original orientation (unturned, or turned by a nonzero General › Constrain Angle), and Space held during either of its drags moves the flare. While a grid is dragged out, Up/Down change its rows (concentric dividers) and Left/Right its columns (radial dividers). A click with a grid tool asks for the size, the divider counts and the grid options (#1377, in part): a skew for each set of dividers, Use Outside Rectangle As Frame or Create Compound Path From Ellipses, and Fill Grid; they are parameters of `shape.rectangularGrid` and `shape.polarGrid`, stay as the tool's options and apply to drags too. The curve a skew percentage follows and the direction of the radial skew are our own (the public documentation gives neither), and the dialogs have no reference point locator. An ellipse's Pie Start and End Angle and Invert Pie (Transform and Properties panels' Ellipse Properties, `object.setLiveShape {pieStart, pieEnd, invertPie}`) cut it as a pie (#812). With the bounding box showing, the Selection tool shows a selected ellipse's two pie widgets just outside it; dragging one sets its angle in one undo step and double-clicking one makes the ellipse whole again. A live polygon's Polygon Properties there: side count, angle, corner type and radius, radius, side length and Make Sides Equal once an uneven scale has made its sides differ (`object.setLiveShape {sides, polygonAngle, polygonRadius, sideLength, makeSidesEqual}`, part of #812); a star is a plain path, as in Illustrator. The Flare Tool Options last as the tool's options: the next flare dragged out uses them, a click's dialog shows them and double-clicking the tool opens them (part of #812). diff --git a/docs/ui-parity.md b/docs/ui-parity.md index c812ee3db2..de00b9cc69 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -52,7 +52,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| | 2026-11-14 | minor | Layers panel: right-clicking a layer/group/object row shows a context menu with Options…, Duplicate, Delete, Lock/Hide Others, Locate Object, and Collect for Export — matching standard desktop layer panel conventions. Scores unchanged. | -| 2026-11-13 | minor | Pencil cursors: the continuation cursor (a short rising line) shows over an open end of a selected path before any press, freehand too, and while a continuing stroke nears another selected path's open end it connects to; the straight-line cursor is a short level line; a continued path, a straight line too, shows the closing cursor and closes within Close Paths When Ends Are Within of its other end, and with Alt at the release; with Use Precise Cursors (Caps Lock) the Pencil's and the Paintbrush's badged cursors keep their badge. Scores and estimates unchanged. | +| 2026-11-13 | minor | Pencil cursors: the continuation cursor (a short rising line) shows over an open end of a selected path before any press, freehand too, and while a continuing stroke nears another selected path's open end it connects to; the straight-line cursor is a short level line; a continued path, a straight line too, shows the closing cursor and closes within Close Paths When Ends Are Within of its other end, and with Alt at the release. Scores and estimates unchanged. | | 2026-11-13 | minor | Graphic Styles: in a panel made taller the list fills the space above the footer instead of stopping at 220 pt (#2992; `the_list_fills_a_tall_panel_above_its_footer` in `crates/ui-egui/src/panels/graphic_styles.rs`). Scores and estimates unchanged. | | 2026-11-13 | minor | Library panels (swatch, Color Book, brush, symbol and graphic style libraries): in a panel made taller the list fills the space above the footer instead of scrolling inside 220 or 280 pt (#2992; `the_list_fills_a_tall_panel_above_its_footer` in `crates/ui-egui/src/panels/library_panel/tests.rs`). Scores and estimates unchanged. | | 2026-11-13 | minor | Swatches: in a panel made taller the swatch list fills the space above the footer instead of scrolling inside 150 or 200 pt (#1638). Scores unchanged. | @@ -342,7 +342,7 @@ tools, never checked side by side) isn't captured row by row. | Polygon/Star exact-size placement remembers the chosen sides/points for later drags and dialog openings; Cancel preserves the previous count | done | Session tool options; existing dialog-opening gestures retained; regression in `toolbar.rs` | | Tool double-click opens its options | done | | | Per-platform mapping (Cmd ↔ Ctrl, Option ↔ Alt) | done | | -| Eyedropper cursor; Use Precise Cursors | partial | original pipette at the sampling tip, distinct from the precise crosshair at five DPI scales (#1269); OS cursor display unverified; while Caps Lock is on, the tools that Use Precise Cursors changes show the crosshair, and the saved preference does not change; the Pencil's and the Paintbrush's cursors with a badge (new path, continue, close, straight line) are drawn on the crosshair and keep their badge (the desktop app reads the lock state on macOS, Windows and Linux X11, not on native Wayland; `view.capsLock` reads or sets it); a Use Precise Cursors command toggles the preference and can take a shortcut (#1663); cursors are painted into the window on Wayland (#1652, not run on Linux) | +| Eyedropper cursor; Use Precise Cursors | partial | original pipette at the sampling tip, distinct from the precise crosshair at five DPI scales (#1269); OS cursor display unverified; while Caps Lock is on, the tools that Use Precise Cursors changes show the crosshair, and the saved preference does not change (the desktop app reads the lock state on macOS, Windows and Linux X11, not on native Wayland; `view.capsLock` reads or sets it); a Use Precise Cursors command toggles the preference and can take a shortcut (#1663); cursors are painted into the window on Wayland (#1652, not run on Linux) | | macOS canvas cursor delivery (#2292) | partial | The UI prepares the existing glyphs as aligned 1×/2× RGBA requests; the desktop host builds AppKit cursors and owns cursor rectangles for the root document window. While it owns the cursor, the host sets it again on every pass, so the tool's cursor stays when the pointer moves (#6362). Detached document viewports and requests without a usable native bitmap retain the visible system fallback; precise-cursor and UI-scale preferences retained. Headless regressions cover routing, pixels/hotspots and lifecycle; native focus/menu/handback timing, mixed displays, Spotlight and full-screen Spaces remain unverified. Wayland still paints its glyphs. | | Every tool's modifiers checked against the shortcut reference | missing | this is the fidelity pass |