Scissors: Alt held switches to the Add Anchor Point tool

A click with the Scissors tool cut the path whether or not Alt was
held, and the cursor stayed the Scissors cursor. The reference app's
shortcut list has "Switch Scissors tool to Add Anchor Point tool" on
Alt (Option).

While Alt is held, a click with the Scissors tool now does what a click
with the Add Anchor Point tool does: on a segment it adds an anchor
point with path.insertAnchor in one undo step, and the path is not cut.
The cursor is the Add Anchor Point tool's while Alt is held. When Alt
is let go, the cursor is the Scissors again and a click cuts.

Docs: docs/target-app-parity.md, crates/tools/src/cursors.toml.
This commit is contained in:
dotCooCoo
2026-10-29 10:02:43 +07:00
committed by bflatastic
parent f0c5de420b
commit 914a109dc9
5 changed files with 57 additions and 4 deletions
+20
View File
@@ -774,6 +774,26 @@ fn scissors_opens_closed_path() {
assert!(near(p.subpaths[0].anchors[5].p, Point::new(200.0, 150.0)));
}
/// Alt held with the Scissors tool switches it to the Add Anchor Point tool: a click adds an anchor
/// in one undo step and the path stays closed. Once Alt is let go, a click cuts again.
#[test]
fn scissors_with_alt_adds_an_anchor_point() {
let mut s = session();
let id = rect(&mut s, 100.0, 100.0, 100.0, 100.0);
s.select_tool("scissors", view()).unwrap();
assert_eq!(gesture(&mut s, &[(200.0, 150.0)], Mods { alt: true, ..Mods::default() }), 1);
let p = path(&s, id);
assert_eq!(paths(&s).len(), 1);
assert!(p.is_closed());
assert_eq!(p.anchor_count(), 5);
assert!(near(p.subpaths[0].anchors[2].p, Point::new(200.0, 150.0)));
assert_eq!(s.tool_id(), "scissors");
gesture(&mut s, &[(150.0, 200.0)], Mods::default());
let p = path(&s, id);
assert!(!p.is_closed());
assert_eq!(p.anchor_count(), 7);
}
#[test]
fn scissors_splits_open_path_in_two() {
let mut s = session();
+3 -3
View File
@@ -163,7 +163,7 @@ id = "PenAdd"
group = "Pen"
shown_when = [
"The Pen over a segment of a selected path with Auto Add/Delete on (Shift off), or over a selected blend's spine.",
"The Add Anchor Point tool everywhere (and the Delete Anchor Point tool with Alt).",
"The Add Anchor Point tool everywhere (and the Delete Anchor Point tool and the Scissors tool with Alt).",
"The Curvature tool over a segment.",
"Stands in for MeshAdd and PuppetPin until they're drawn.",
]
@@ -622,14 +622,14 @@ hotspot = "The blade's point."
[[cursor]]
id = "Scissors"
group = "Drawing and editing tools"
shown_when = ["The Scissors tool over a path segment: a click splits the path there."]
shown_when = ["The Scissors tool over a path segment, Alt up: a click splits the path there."]
hotspot = "Where the blades cross, exact enough to cut where aimed."
look = "Scissors. ScissorsNotAllowed is this drawing with a badge."
[[cursor]]
id = "ScissorsNotAllowed"
group = "Drawing and editing tools"
shown_when = ["The Scissors tool away from every path segment: a click does nothing."]
shown_when = ["The Scissors tool away from every path segment, Alt up: a click does nothing."]
hotspot = "The same point as Scissors."
look = "The Scissors with a small not-allowed badge, so the cursor keeps its size and shape and only the badge says a click won't cut."
+5 -1
View File
@@ -10,7 +10,7 @@
//! still held: at 45° steps from where it was). Space held during a convert- or handle-drag moves
//! the anchor, handles and all, as while drawing with the Pen. Blend spine points and handles
//! follow the same conversions (#2544).
//! Scissors: click a path to split it there.
//! Scissors: click a path to split it there (Alt = add, as the Add Anchor Point tool does).
use serde_json::json;
use vectorcraft_doc::NodeId;
@@ -316,6 +316,9 @@ impl Tool for AnchorTool {
}
}
("scissors", PointerKind::Down) => {
if ev.mods.alt {
return Self::add(cx, p);
}
if let Some((id, si, ai)) = hit_anchor(cx, p, point) {
return vec![Action::Exec("path.split".into(), json!({"id": id.0, "subpath": si, "anchor": ai}))];
}
@@ -554,6 +557,7 @@ impl Tool for AnchorTool {
"addAnchor" => Cursor::PenDelete,
"deleteAnchor" if !m.alt => Cursor::PenDelete,
"deleteAnchor" => Cursor::PenAdd,
"scissors" if m.alt => Cursor::PenAdd,
"scissors" => {
if hit_segment(cx, p, cx.pick_tol()).is_some() {
Cursor::Scissors
+28
View File
@@ -301,6 +301,34 @@ fn scissors_splits_at_segment() {
);
}
/// With Alt held the Scissors tool acts as the Add Anchor Point tool: a click sends what the Add
/// Anchor Point tool's click sends (an anchor inserted on a segment, nothing away from every path)
/// and never splits, and the cursor is the Add Anchor Point tool's everywhere. Without Alt it cuts
/// again.
#[test]
fn scissors_with_alt_adds_an_anchor() {
let (d, id) = doc_with_rect();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let mut t = AnchorTool::new("scissors");
let mut add = AnchorTool::new("addAnchor");
let alt = Mods { alt: true, ..Mods::default() };
for (x, y) in [(200.0, 150.0), (150.0, 101.0), (200.0, 100.0), (400.0, 400.0)] {
let down = PointerEvent::new(PointerKind::Down, x, y);
let a = t.pointer(&cx, &down.with_mods(alt));
assert_eq!(a, add.pointer(&cx, &down), "({x}, {y})");
assert!(!a.iter().any(|a| matches!(a, Action::Exec(c, _) if c == "path.split")), "({x}, {y})");
assert_eq!(t.cursor(&cx, Point::new(x, y), alt), Cursor::PenAdd, "({x}, {y})");
}
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 200.0, 150.0).with_mods(alt));
assert!(matches!(&a[..], [Action::Exec(c, v)] if c == "path.insertAnchor" && v["id"] == id.0 && v["segment"] == 1));
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 200.0, 150.0));
assert!(matches!(&a[..], [Action::Exec(c, v)] if c == "path.split" && v["id"] == id.0 && v["segment"] == 1));
assert_eq!(t.cursor(&cx, Point::new(200.0, 150.0), Mods::default()), Cursor::Scissors);
assert_eq!(t.cursor(&cx, Point::new(400.0, 400.0), Mods::default()), Cursor::ScissorsNotAllowed);
}
/// The Add Anchor Point, Delete Anchor Point, Anchor Point and Scissors tools pick anchors and
/// segments only on the art a click can pick: not on a Shaper Group's original shapes unless
/// they are isolated (Construction mode), and in isolation mode only on the isolated object.
+1
View File
@@ -651,6 +651,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- **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. 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).
While an arc is dragged out, C switches it between open and closed, F switches its base between the X and Y axes in the same box, and Up/Down raise and lower its slope by 10 within −100…100; each press switches once, and the values stay as the Arc tool's options. The [default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) (archived copy) list C, F and Up/Down for an arc being dragged out. F as the base switch, the arrows changing the slope, the step of 10 and one switch per press are our own rules.
- **Cutting:** Eraser, Scissors, Knife, Mirror & Cut, Line Cut and Rectangle Cut (real geometry, compound paths keep their holes).
While Alt is held, the Scissors tool works as the Add Anchor Point tool ([Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)): a click on a segment adds an anchor point without cutting (`path.insertAnchor`), and the pointer shows the Add Anchor Point cursor. When Alt is let go, a click cuts again.
Eraser Tool Options has Angle and Roundness besides Size, as [Erase paths using the Eraser tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/modify-paths/erase-paths-using-eraser-tool.html) describes: the Eraser's tip is an ellipse Size across, its long axis at Angle and its short axis Roundness percent of the long one, and a drag with a tip that is not round shows the outline of the area it erases (`path.eraseRegion` `angle` and `roundness`).
- **Transform:** Rotate, Reflect, Scale, Shear (click or Alt-click snaps the reference point to anchors and centres; Alt-click opens the tool's dialog, and a drag with Alt down at the release, from the press or pressed mid-drag, transforms a copy), Reshape, Free Transform (free and perspective distort and shear, switched by the keys held during a handle drag).
Align, Distribute, Distribute Spacing, Reflect and Shear return an error for any `horizontal`, `vertical`, `to`, `axis`, `bounds` or `spacing` value they don't take, listing the choices, and Align returns one when a call gives neither `horizontal` nor `vertical`; nothing moves and no undo step is recorded. Align and Distribute act on the anchor points selected with Direct Selection: the points of one path align to each other, not to the artboard, and Distribute spaces them (#1641, fixes #1636; aligning to a key anchor is still missing).