Curvature: Alt-click on an open end goes on from it, and starts a path with a corner

An Alt-click on an end of a selected open path makes that end the current
point without converting it, so the next click extends the path from it.
This applies to the other end too while one end is current; a plain click
there still closes the path. Alt-click on any other point still toggles it
between smooth and corner, and a double-click toggles any point, ends
included. An Alt-click that starts a new path places a corner point, as
later Alt-clicks do.

Changed behavior: an Alt-click on an end of a selected open path no longer
converts it; double-click converts it.

Help page: https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/draw-shapes-with-the-curvature-tool.html
This commit is contained in:
dotCooCoo
2026-11-12 17:23:58 -08:00
parent 487ffbba4b
commit 057db09c12
5 changed files with 85 additions and 22 deletions
+1 -1
View File
@@ -232,7 +232,7 @@ id = "Curvature"
group = "Pen"
shown_when = [
"The Curvature tool over empty canvas, or anywhere while drawing a path away from its points.",
"Elsewhere the Curvature tool borrows Pen-family cursors: PenAdd over a segment, Move over an anchor, PenContinue at an open end, PenConvert with Alt, PenClose over the first point of the path being drawn.",
"Elsewhere the Curvature tool borrows Pen-family cursors: PenAdd over a segment, Move over an anchor, PenContinue at an open end of a selected path (Alt held or not), PenConvert with Alt over any other anchor or a point of the path being drawn, PenClose over the first point of the path being drawn.",
]
hotspot = "The nib's tip."
look = "The Pen nib with a curve badge."
+25 -15
View File
@@ -1,6 +1,7 @@
//! The Curvature tool (Shift+~).
//!
//! Each click adds a point and the path curves smoothly through all points. Alt-click or
//! Each click adds a point and the path curves smoothly through all points; an Alt-click adds a
//! corner point, the first point included. Alt-click or
//! double-click a point toggles it between smooth and corner; drag a point to move it; click the
//! first point to close; Backspace/Delete removes the last touched point; Esc/Enter ends the path.
//! A rubber band shows the curve to the cursor (Enable Rubber Band for Curvature Tool). Each point
@@ -8,8 +9,10 @@
//!
//! It edits any selected path too, whatever drew it, keeping its shape except where edited
//! (#798). A click on one of its points makes that the current point (direct-selected); a drag
//! moves it, re-curving only the two segments at it ([`curvature_move`]); Alt-click or
//! double-click toggles it between smooth and corner and Backspace/Delete removes it, the curve
//! moves it, re-curving only the two segments at it ([`curvature_move`]); double-click, or
//! Alt-click on any point but an end of an open path, toggles it between smooth and corner. An
//! Alt-click on an end of an open path makes it the current point without changing it, so the
//! next click goes on from it. Backspace/Delete removes the current point, the curve
//! where it was then following the remaining points ([`curvature_remove`]). A click on
//! a segment adds a smooth point there without changing the shape ([`curvature_insert`]), and a
//! drag moves the new point at once. While the current point is an end of an open path, clicks go
@@ -429,12 +432,16 @@ fn delete_point((id, si, ai): (NodeId, usize, usize)) -> Action {
fn toggle(cx: &ToolContext, (id, si, ai): (NodeId, usize, usize)) -> Option<Action> {
let sp = subpath(cx, id, si)?;
let a = sp.anchors.get(ai)?;
let open_end = !sp.closed && (ai == 0 || ai + 1 == sp.anchors.len());
let smooth = if open_end { a.has_in() || a.has_out() } else { a.kind == AnchorKind::Smooth };
let smooth = if open_end(sp, ai) { a.has_in() || a.has_out() } else { a.kind == AnchorKind::Smooth };
let to = if smooth { "corner" } else { "smooth" };
Some(Action::Exec("path.convertAnchor".into(), json!({"id": id.0, "subpath": si, "anchor": ai, "to": to})))
}
/// Whether anchor `ai` is an end of the open subpath `sp`.
fn open_end(sp: &SubPath, ai: usize) -> bool {
!sp.closed && (ai == 0 || ai + 1 == sp.anchors.len())
}
/// Snap targets for dragging anchors `anchors` of subpath `si` of `id` (the rest of the path
/// pulls).
fn hold_targets(snap: &mut DrawSnap, cx: &ToolContext, id: NodeId, si: usize, anchors: &[usize]) {
@@ -507,12 +514,13 @@ impl CurvatureTool {
}
/// A press on a selected path the tool isn't drawing: on the other end of the open path going
/// on (it closes), on a point, anywhere while an end is current (the path goes on), or on a
/// on (without Alt it closes), on a point, anywhere while an end is current (the path goes on), or on a
/// segment. None elsewhere: a new path starts.
fn press_selected(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Option<Vec<Action>> {
let (p, tol) = (ev.pos, point_tol(cx));
let end = current_end(cx);
if let Some(e) = end.filter(|e| e.anchors >= 3 && p.distance(e.other) <= tol) {
// An Alt-click on the other end makes it current instead (below).
if let Some(e) = end.filter(|e| !ev.mods.alt && e.anchors >= 3 && p.distance(e.other) <= tol) {
let how = self.how(&e);
self.placed = None;
let v = json!({"id": e.id.0, "subpath": e.si, "op": "close", "end": e.name(), "from": how.name()});
@@ -520,11 +528,14 @@ impl CurvatureTool {
}
if let Some(hit) = anchor_in(cx, editable_paths(cx), p, tol) {
let mut out = vec![select_point(hit)];
let (id, si, ai) = hit;
if ev.mods.alt {
out.extend(toggle(cx, hit));
// On an end of an open path the path goes on from it; any other point toggles.
if !subpath(cx, id, si).is_some_and(|sp| open_end(sp, ai)) {
out.extend(toggle(cx, hit));
}
return Some(out);
}
let (id, si, ai) = hit;
hold_targets(&mut self.snap, cx, id, si, &[ai]);
self.drag = Some((Grab::Anchor(id, si, ai), p, false));
return Some(out);
@@ -596,7 +607,7 @@ impl Tool for CurvatureTool {
// Start a new path.
self.reset();
let p = self.snap.press(cx, p, &[], None);
self.pts.push((p, false));
self.pts.push((p, ev.mods.alt));
self.current = Some(0);
vec![Action::Exec("path.curvature".into(), self.params()), Action::Notify("created".into())]
}
@@ -762,15 +773,14 @@ impl Tool for CurvatureTool {
}
let tol = point_tol(cx);
let end = current_end(cx);
if end.is_some_and(|e| e.anchors >= 3 && p.distance(e.other) <= tol) {
if !m.alt && end.is_some_and(|e| e.anchors >= 3 && p.distance(e.other) <= tol) {
return Cursor::PenClose;
}
if let Some((id, si, ai)) = anchor_in(cx, editable_paths(cx), p, tol) {
let open_end = subpath(cx, id, si).is_some_and(|sp| !sp.closed && (ai == 0 || ai + 1 == sp.anchors.len()));
return if m.alt {
Cursor::PenConvert
} else if open_end {
return if subpath(cx, id, si).is_some_and(|sp| open_end(sp, ai)) {
Cursor::PenContinue
} else if m.alt {
Cursor::PenConvert
} else {
Cursor::Move
};
+55 -3
View File
@@ -300,17 +300,69 @@ fn curvature_remove_keeps_a_corner_neighbors_handle_direction() {
assert_eq!((sp.anchors[1].h_in, sp.anchors[1].h_out), (corner.h_in, corner.p));
}
/// A curved end counts as smooth for the toggle: Alt-click turns it into a corner.
/// A curved end counts as smooth for the toggle: a double-click turns it into a corner.
#[test]
fn alt_click_on_a_curved_end_makes_it_a_corner() {
fn double_click_on_a_curved_end_makes_it_a_corner() {
let sp = curvature_spline(&[(pt(100.0, 300.0), false), (pt(200.0, 200.0), false), (pt(300.0, 300.0), false)], false);
let (d, id) = doc_with(sp);
let (s, p) = (selected(id, None), paint());
let cx = quiet(&d, &s, &p);
let a = press(&mut CurvatureTool::default(), &cx, 100.0, 300.0, true);
let a = CurvatureTool::default().pointer(&cx, &PointerEvent::new(PointerKind::DoubleClick, 100.0, 300.0));
assert!(a.iter().any(|a| matches!(a, Action::Exec(c, v) if c == "path.convertAnchor" && v["anchor"] == 0 && v["to"] == "corner")), "{a:?}");
}
/// An Alt-click on either end of a selected open path makes it the current point without
/// converting it, and the next click goes on from it.
#[test]
fn alt_click_on_an_open_end_goes_on_from_it() {
let (d, id) = doc_with(wave());
let (s, p) = (selected(id, None), paint());
let cx = quiet(&d, &s, &p);
let mut t = CurvatureTool::default();
let alt = Mods { alt: true, ..Mods::default() };
for (x, y, ai) in [(450.0, 60.0, 4), (0.0, 100.0, 0)] {
assert_eq!(t.cursor(&cx, pt(x, y), alt), Cursor::PenContinue);
let a = press(&mut t, &cx, x, y, true);
assert_eq!(a, vec![select_point((id, 0, ai))]);
assert!(!t.busy(), "an Alt-click does not grab the point");
}
// As the engine does it: the Alt-clicked end is current, and the next click goes on from it.
let s = selected(id, Some(0));
let a = press(&mut t, &quiet(&d, &s, &p), -50.0, 150.0, false);
assert!(matches!(edit_op(&a[0]), Some(("extend", v)) if v["end"] == "start" && v["from"] == "keep"), "{a:?}");
// With the start current, an Alt-click on the other end makes that end current; a plain click
// there closes the path.
let cx = quiet(&d, &s, &p);
assert_eq!(t.cursor(&cx, pt(450.0, 60.0), alt), Cursor::PenContinue);
assert_eq!(press(&mut t, &cx, 450.0, 60.0, true), vec![select_point((id, 0, 4))]);
assert_eq!(t.cursor(&cx, pt(450.0, 60.0), Mods::default()), Cursor::PenClose);
assert!(matches!(edit_op(&press(&mut t, &cx, 450.0, 60.0, false)[0]), Some(("close", v)) if v["end"] == "start"));
// A closed path has no ends: an Alt-click on its first point converts it.
let mut closed = wave();
closed.closed = true;
let (d, id) = doc_with(closed);
let s = selected(id, None);
let cx = quiet(&d, &s, &p);
assert_eq!(t.cursor(&cx, pt(0.0, 100.0), alt), Cursor::PenConvert);
let a = press(&mut t, &cx, 0.0, 100.0, true);
assert!(a.iter().any(|a| matches!(a, Action::Exec(c, v) if c == "path.convertAnchor" && v["anchor"] == 0)), "{a:?}");
}
/// An Alt-click that starts a path places a corner point, as later Alt-clicks do; a plain click
/// places a smooth one.
#[test]
fn alt_click_starts_a_path_with_a_corner() {
let d = Document::new(800.0, 600.0);
let (s, p) = (Selection::default(), paint());
let cx = quiet(&d, &s, &p);
for alt in [true, false] {
let mut t = CurvatureTool::default();
let a = press(&mut t, &cx, 100.0, 120.0, alt);
assert!(matches!(&a[0], Action::Exec(c, v) if c == "path.curvature" && v["points"][0]["corner"] == alt), "{a:?}");
assert_eq!(t.pts, vec![(pt(100.0, 120.0), alt)]);
}
}
/// A document with one path, `sp`.
fn doc_with(sp: SubPath) -> (Document, NodeId) {
let mut d = Document::new(800.0, 600.0);
+3 -2
View File
@@ -4793,8 +4793,9 @@ the curve at an end), and the neighbours' handles keep their directions (their l
moved there: the tool's click-and-drag on a segment). `op: "extend"` with `x`, `y` goes on from the `end` (default)
or the `start` of an open subpath (the new end curves into the path unless `corner` is true), and `op: "close"` closes it from that end; `from` says how the old end bends into
the new segment: `"keep"` (default) keeps its curve, `"smooth"` curves it through its neighbours (a point the tool
placed), `"corner"` leaves it without handles. Alt-click or double-click toggles a point with `path.convertAnchor`, and
Backspace/Delete removes the current point with `path.removeAnchor`.
placed), `"corner"` leaves it without handles. Double-click, or Alt-click on any point but an end of an open subpath,
toggles a point with `path.convertAnchor`; an Alt-click on an end of an open subpath only makes it the current point
(`select.anchors`), so the next click extends from it. Backspace/Delete removes the current point with `path.removeAnchor`.
```json
{"name":"run_command","arguments":{"command":"path.curvatureEdit","params":{"id":12,"op":"move","anchor":2,"x":260,"y":140}}}
+1 -1
View File
@@ -993,7 +993,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. While Draw Inside lasts, a dotted open rectangle marks the corners of the object drawn into (#4319).
- **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).
- **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; double-click, or Alt-click on any point but an end of an open path, 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`). An Alt-click on an end of a selected open path makes that end current without changing it, so the next click goes on from it, as [the help page](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/draw-shapes-with-the-curvature-tool.html) describes for continuing a path; an Alt-click that starts a new path places a corner point, as later Alt-clicks do. 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).
Blob Brush and Eraser Tool Options' Size is Fixed or Pressure with a Variation (#4590, #3961): with Pressure a pen stroke is Size − Variation wide where it presses lightest and Size + Variation where it presses hardest, and a mouse stroke keeps the Size.
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.
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 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.