mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Pencil: the closing cursor shows while a stroke is drawn back near its start
A Pencil or Paintbrush stroke released within Close Paths When Ends Are Within of its start closes, but the cursor gave no sign of it. While the pointer is where a release would close the stroke, the cursor is now PencilClose (PaintbrushClose for the Paintbrush), the crosshair with a ring, as Draw straight lines with the Pencil tool describes: https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/draw-straight-lines-with-the-pencil-tool.html The check is the one the release makes, on the end the stroke would take (snapped for the Pencil). A straight line (Shift or Alt held at the press), a stroke that continues a selected path, a stroke that redraws one and a smoothing drag show no ring. Docs: docs/target-app-parity.md, docs/ui-parity.md.
This commit is contained in:
@@ -536,7 +536,10 @@ hotspot = "The lead tip."
|
||||
[[cursor]]
|
||||
id = "PencilClose"
|
||||
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."]
|
||||
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.",
|
||||
]
|
||||
hotspot = "The lead tip."
|
||||
look = "The Pencil cursor with a ring badge (closing the loop), like PenClose."
|
||||
|
||||
@@ -556,7 +559,10 @@ hotspot = "The bristle tip."
|
||||
[[cursor]]
|
||||
id = "PaintbrushClose"
|
||||
group = "Drawing and editing tools"
|
||||
shown_when = ["The Paintbrush tool with Alt held, before a press and while drawing: releasing the mouse button with Alt still held closes the path."]
|
||||
shown_when = [
|
||||
"The Paintbrush tool with Alt held, before a press and while drawing: 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.",
|
||||
]
|
||||
hotspot = "The bristle tip."
|
||||
look = "The Paintbrush cursor with a ring badge (closing the loop), like PenClose."
|
||||
|
||||
|
||||
@@ -30,6 +30,9 @@ pub struct GestureTool {
|
||||
pressures: Vec<f32>,
|
||||
active: bool,
|
||||
began: bool,
|
||||
/// 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,
|
||||
/// Pencil/Paintbrush continuing a selected open path: (path, continue from its start).
|
||||
extend: Option<(NodeId, bool)>,
|
||||
/// Pencil/Paintbrush starting on a selected path away from its ends: (path, Edit Selected
|
||||
@@ -93,6 +96,7 @@ impl GestureTool {
|
||||
pressures: vec![],
|
||||
active: false,
|
||||
began: false,
|
||||
near_start: false,
|
||||
extend: None,
|
||||
redraw: None,
|
||||
fidelity,
|
||||
@@ -250,6 +254,19 @@ impl GestureTool {
|
||||
})
|
||||
}
|
||||
|
||||
/// Does a stroke that ends at `end` end near its start (Close Paths When Ends Are Within, off
|
||||
/// at 0 pixels)?
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
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)
|
||||
}
|
||||
|
||||
/// Parameters of `path.freehand` for the current stroke.
|
||||
pub fn freehand_params(&self, closed: bool) -> Value {
|
||||
let mut v = json!({
|
||||
@@ -284,7 +301,7 @@ impl GestureTool {
|
||||
return vec![];
|
||||
}
|
||||
// Close Paths When Ends Are Within is off at 0 pixels.
|
||||
let near = self.close_within > 0.0 && pts.len() > 2 && pts[0].distance(pts[pts.len() - 1]) <= cx.tol(self.close_within);
|
||||
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.alt_smooth) || (self.extend.is_none() && near));
|
||||
vec![Action::Preview("path.freehand".into(), self.freehand_params(closed)), Action::Commit]
|
||||
@@ -327,6 +344,7 @@ impl Tool for GestureTool {
|
||||
PointerKind::Down => {
|
||||
self.active = true;
|
||||
self.began = false;
|
||||
self.near_start = false;
|
||||
self.smoothing = self.id == "pencil" && self.alt_smooth && ev.mods.alt;
|
||||
// Alt draws a straight line only while it doesn't lend the Smooth tool; with Shift
|
||||
// too, the line keeps Shift's 45° steps.
|
||||
@@ -356,9 +374,10 @@ impl Tool for GestureTool {
|
||||
if !self.began && !self.straight && self.id == "pencil" && self.alt_smooth && ev.mods.alt {
|
||||
self.smoothing = true;
|
||||
}
|
||||
let mut end = p;
|
||||
if self.snaps() && !self.smoothing && !self.straight {
|
||||
// Where the stroke would end: shown, taken on release.
|
||||
self.snap.drag(cx, p, None);
|
||||
end = self.snap.drag(cx, p, None);
|
||||
}
|
||||
let p = if self.straight && self.unconstrained && self.snaps() {
|
||||
// Alt's line ends where a stroke would: shown, taken on release.
|
||||
@@ -379,9 +398,11 @@ impl Tool for GestureTool {
|
||||
}
|
||||
self.points.truncate(1);
|
||||
} else if self.points.last().is_some_and(|l| l.distance(p) < cx.tol(1.0)) || self.points.len() >= crate::MAX_STROKE_SAMPLES {
|
||||
self.near_start = self.release_closes_near_start(cx, end);
|
||||
return vec![];
|
||||
}
|
||||
self.push(p, ev.pressure);
|
||||
self.near_start = self.release_closes_near_start(cx, end);
|
||||
if !self.draws_path() || self.smoothing {
|
||||
return vec![];
|
||||
}
|
||||
@@ -424,6 +445,7 @@ impl Tool for GestureTool {
|
||||
let out = if cancel { vec![Action::Cancel] } else { self.finish(cx, ev.mods) };
|
||||
self.active = false;
|
||||
self.began = false;
|
||||
self.near_start = false;
|
||||
self.smoothing = false;
|
||||
self.straight = false;
|
||||
self.unconstrained = false;
|
||||
@@ -446,6 +468,7 @@ impl Tool for GestureTool {
|
||||
self.snap.clear();
|
||||
self.active = false;
|
||||
self.began = false;
|
||||
self.near_start = false;
|
||||
self.smoothing = false;
|
||||
self.straight = false;
|
||||
self.unconstrained = false;
|
||||
@@ -470,6 +493,7 @@ impl Tool for GestureTool {
|
||||
.then(|| Action::Preview("path.freehand".into(), self.freehand_params(false)));
|
||||
self.snap.clear();
|
||||
self.active = false;
|
||||
self.near_start = false;
|
||||
self.began = false;
|
||||
self.straight = false;
|
||||
self.unconstrained = false;
|
||||
@@ -505,6 +529,11 @@ impl Tool for GestureTool {
|
||||
if self.draws_path() && !self.alt_smooth && m.alt && !self.straight && (self.active || self.id != "pencil") {
|
||||
return if self.id == "paintbrush" { Cursor::PaintbrushClose } else { Cursor::PencilClose };
|
||||
}
|
||||
// A release near a freehand stroke's start closes it too (`finish`): the ring shows while
|
||||
// the pointer is there.
|
||||
if self.draws_path() && self.active && self.near_start {
|
||||
return if self.id == "paintbrush" { Cursor::PaintbrushClose } else { Cursor::PencilClose };
|
||||
}
|
||||
match self.id {
|
||||
"paintbrush" => Cursor::Paintbrush,
|
||||
"blobBrush" => Cursor::BlobBrush,
|
||||
@@ -1142,6 +1171,52 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// While a stroke is drawn back near its start, the cursor shows the closing ring when a
|
||||
/// release there closes the path (Close Paths When Ends Are Within), and the tool's own cursor
|
||||
/// away from the start, at 0 pixels and after the release.
|
||||
#[test]
|
||||
fn a_stroke_near_its_start_shows_the_closing_cursor() {
|
||||
let (d, _) = doc_with_rect();
|
||||
let s = Selection::default();
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let none = Mods::default();
|
||||
let tools = [("pencil", Cursor::Pencil, Cursor::PencilClose), ("paintbrush", Cursor::Paintbrush, Cursor::PaintbrushClose)];
|
||||
for (tool, own, closing) in tools {
|
||||
for within in [15.0, 0.0] {
|
||||
let near = if within > 0.0 { closing } else { own };
|
||||
let mut t = GestureTool::new(tool);
|
||||
t.set_option("closeWithin", &json!(within));
|
||||
let mut at = |k, x, y| {
|
||||
let acts = t.pointer(&cx, &PointerEvent::new(k, x, y));
|
||||
(acts, t.cursor(&cx, Point::new(x, y), none))
|
||||
};
|
||||
assert_eq!(at(PointerKind::Down, 310.0, 330.0).1, own, "{tool} at the press");
|
||||
assert_eq!(at(PointerKind::Drag, 360.0, 350.0).1, own, "{tool} away from the start");
|
||||
assert_eq!(at(PointerKind::Drag, 330.0, 390.0).1, own, "{tool} away from the start");
|
||||
assert_eq!(at(PointerKind::Drag, 318.0, 335.0).1, near, "{tool} near the start at {within} px");
|
||||
assert_eq!(at(PointerKind::Drag, 345.0, 340.0).1, own, "{tool} away from the start again");
|
||||
assert_eq!(at(PointerKind::Drag, 318.0, 335.0).1, near, "{tool} back near the start at {within} px");
|
||||
let (acts, after) = at(PointerKind::Up, 318.0, 335.0);
|
||||
let Some(Action::Preview(_, v)) = acts.first() else { panic!("{tool}: {acts:?}") };
|
||||
assert_eq!(v["closed"], json!(within > 0.0), "{tool}: the release does what the cursor showed");
|
||||
assert_eq!(after, own, "{tool} after the release");
|
||||
}
|
||||
}
|
||||
// A straight Pencil line (Shift or Alt held at the press) stays open, so it shows no ring
|
||||
// near its start.
|
||||
for held in [Mods { shift: true, ..Mods::default() }, Mods { alt: true, ..Mods::default() }] {
|
||||
let mut t = GestureTool::new("pencil");
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 310.0, 330.0).with_mods(held));
|
||||
for (x, y) in [(360.0, 330.0), (330.0, 390.0), (318.0, 335.0)] {
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, x, y).with_mods(held));
|
||||
}
|
||||
for m in [held, none] {
|
||||
assert_eq!(t.cursor(&cx, Point::new(318.0, 335.0), m), Cursor::Pencil, "a straight line, {held:?}, {m:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eraser_and_knife_exec_on_release() {
|
||||
let (d, _) = doc_with_rect();
|
||||
|
||||
@@ -576,7 +576,8 @@ pub enum Cursor {
|
||||
Pencil,
|
||||
/// 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. A crosshair with a ring.
|
||||
/// 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.
|
||||
PencilClose,
|
||||
/// The Pencil tool before a press that draws a straight line (Shift or Alt held). A crosshair with a
|
||||
/// short straight line.
|
||||
@@ -584,7 +585,8 @@ pub enum Cursor {
|
||||
/// The Paintbrush tool.
|
||||
Paintbrush,
|
||||
/// The Paintbrush tool with Alt held: releasing the mouse button with Alt still held closes
|
||||
/// the path. A crosshair with a ring.
|
||||
/// the path. Also while a stroke is drawn back near its start, where releasing the mouse button
|
||||
/// closes the path. A crosshair with a ring.
|
||||
PaintbrushClose,
|
||||
/// The Blob Brush tool.
|
||||
BlobBrush,
|
||||
|
||||
@@ -631,7 +631,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Saved selections:** Select ▸ Save Selection… asks for a name and keeps the selection in the document (saved with the file, undoable, up to 25); each one is listed at the bottom of the Select menu and a click selects its objects again; Edit Selection… lists them to rename or delete, applied together on OK. A file's saved selections are capped and checked against its objects when it opens.
|
||||
Select ▸ Update Selection replaces the objects of the saved selection last recalled with the selected objects, in one undo step that marks the document changed (`select.update`). After Edit Selection… renames that saved selection, Update Selection replaces it under the new name; after Edit Selection… deletes it, Update Selection is disabled until another saved selection is recalled or the delete is undone. Undo and Redo do not change which saved selection is recalled.
|
||||
- **Drawing modes:** in Draw Inside, a double-click with a selection tool outside the object being drawn into sets the drawing mode to Draw Normal, as Shift+D does. Pastes follow the drawing mode ([drawing modes](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/learn-drawing-basics/drawing-modes-overview.html)) (#3705): in Draw Inside, Paste, Paste in Place and Paste on All Artboards put the copies inside the object drawn into, in one undo step, Paste Remembers Layers aside; in Draw Behind, Paste in Place and Paste on All Artboards put them just below the selection; Paste in Front and Paste in Back ignore the mode. Shift+D cycles Draw Normal → Draw Behind → Draw Inside → Draw Normal, skipping Draw Inside unless one path, compound path or type is selected. With the object drawn into isolated (a double-click on it with the Selection tool), art drawn or pasted inside it takes isolation mode into its clip group, so it can be clicked and isn't dimmed, as art added in isolation mode belongs with what is isolated ([isolation mode](https://helpx.adobe.com/illustrator/desktop/manage-objects/select-objects/isolate-objects.html)) (#3705); undo or redo that removes the isolated object ends isolation mode.
|
||||
- **Drawing:** Pen, Curvature, anchor tools, Pencil, Paintbrush, Blob Brush, Smooth, Path Eraser, Join. Direction handles dragged with Direct Selection or the Anchor Point tool snap to smart guides, and Shift keeps them at 45° steps from their anchor. The Pen continues any open path from the end clicked, and while drawing, a click on an end of another open path joins the two into one (#776). The Curvature tool edits any selected path, whatever drew it, keeping its shape except where edited (#798): a click on a point makes it current and a drag moves it, re-curving only its two segments; Alt-click or double-click toggles it smooth or corner and Delete removes it; a click on a segment adds a point without changing the shape; with an end of an open path current, clicks go on from it and its other end closes it (`path.curvatureEdit`). Every point a drawing tool places snaps to Smart Guides through one snapper (#506): the Pen's and Curvature tool's anchors, the Pencil's ends, the shape tools' start and dragged corner or radius (Rectangle, Rounded Rectangle, Ellipse, Polygon, Star, Line Segment, Arc, Spiral, the grids), the Type tool's click and frame, a new artboard's corners. A filled Pencil path keeps a stroke of None (#1804, part of #1679). The [ and ] keys reach the Blob Brush, the Eraser and the Symbolism tools in the app (#1734, part of #1661).
|
||||
- **Drawing:** Pen, Curvature, anchor tools, Pencil, Paintbrush, Blob Brush, Smooth, Path Eraser, Join. Direction handles dragged with Direct Selection or the Anchor Point tool snap to smart guides, and Shift keeps them at 45° steps from their anchor. The Pen continues any open path from the end clicked, and while drawing, a click on an end of another open path joins the two into one (#776). The Curvature tool edits any selected path, whatever drew it, keeping its shape except where edited (#798): a click on a point makes it current and a drag moves it, re-curving only its two segments; Alt-click or double-click toggles it smooth or corner and Delete removes it; a click on a segment adds a point without changing the shape; with an end of an open path current, clicks go on from it and its other end closes it (`path.curvatureEdit`). Every point a drawing tool places snaps to Smart Guides through one snapper (#506): the Pen's and Curvature tool's anchors, the Pencil's ends, the shape tools' start and dragged corner or radius (Rectangle, Rounded Rectangle, Ellipse, Polygon, Star, Line Segment, Arc, Spiral, the grids), the Type tool's click and frame, a new artboard's corners. A filled Pencil path keeps a stroke of None (#1804, part of #1679). While a Pencil or Paintbrush stroke is drawn back to within Close Paths When Ends Are Within of its start, the cursor shows the closing ring and a release there closes the path, 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 for the Pencil. The [ and ] keys reach the Blob Brush, the Eraser and the Symbolism tools in the app (#1734, part of #1661).
|
||||
With the Blob Brush, Shift held during a drag keeps the stroke on the horizontal or vertical line through the press point, whichever is nearer to the pointer (90° steps from the Constrain Angle preference), and the points dragged since Shift went down move to the other line when the pointer crosses to it; the points dragged while Shift is up follow the pointer.
|
||||
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).
|
||||
While Alt is held, the Paintbrush, and the Pencil with Alt key toggles to Smooth Tool off, show a closing cursor (a crosshair with a ring); releasing the mouse button with Alt still held closes the path.
|
||||
|
||||
@@ -91,6 +91,7 @@ tools, never checked side by side) isn't captured row by row.
|
||||
| Curvature tool: click/drag points, double-click or Alt-click for a corner, temporary Direct Selection, point conversion | partial | circular point markers without direction handles or duplicate markers, Anchor Size and Hide Edges regression-tested (#1236, in part); curve geometry: each stretch of three points follows the circle through them (handles along its tangent, chord/(3·cos²(α/2)) long, shorter in a turn tighter than 30°), open ends curving in, the same rule for the rubber band, dragged, inserted and converted points (#1236, in part); neighbours of a point dragged on an existing path keep their tangents, and modifiers requested in #975 remain |
|
||||
| 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 |
|
||||
| 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 | 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); 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) off, the path a stroke draws or continues is not selected afterwards; 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 |
|
||||
| Pencil: Fidelity, Keep selected, Alt to Smooth, edit selected paths, close by proximity | done | Alt key toggles to Smooth Tool (#2001); Shift held at the press draws a straight line at 45° steps from the Constrain Angle, with a straight-line cursor while Shift is held before the press (part of #3799; Alt's unconstrained straight line is not done); 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) off, the path a stroke draws or continues is not selected afterwards; a filled Pencil path keeps a stroke of None (#1804, part of #1679) |
|
||||
| Pencil: Fidelity, Keep selected, Alt to Smooth, edit selected paths, close by proximity | done | 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); 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) off, the path a stroke draws or continues is not selected afterwards; a filled Pencil path keeps a stroke of None (#1804, part of #1679) |
|
||||
| Pencil: Fidelity, Keep selected, Alt to Smooth, edit selected paths, close by proximity | partial | Alt to Smooth is missing; 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) off, the path a stroke draws or continues is not selected afterwards; a filled Pencil path keeps a stroke of None (#1804, part of #1679) |
|
||||
|
||||
Reference in New Issue
Block a user