Fix #3799: Alt held at the press draws a straight Pencil line at any angle

With Alt key toggles to Smooth Tool off, holding Alt when a Pencil drag
starts now draws one straight segment from the press point to the
pointer, at the angle the pointer takes, as Shift does at 45° steps.
Pressed on an end of a selected open path, the line continues the path
from that end. Both ends snap to Smart Guides as a stroke's ends do, and
the line stays open when Alt is still held at the release. Shift and Alt
together keep the 45° steps.

Before, a drag started and released with Alt held drew a closed freehand
path. Alt pressed after the drag has started still closes the path at
the release, and with Alt key toggles to Smooth Tool on, Alt still switches
to the Smooth tool. Before a Pencil press, Alt no longer shows the closing
cursor.

Docs: docs/target-app-parity.md, docs/ui-parity.md.
This commit is contained in:
dotCooCoo
2026-10-24 17:29:32 +07:00
committed by bflatastic
parent 23c0e8c501
commit 4c5c54ccbc
7 changed files with 176 additions and 26 deletions
+23
View File
@@ -141,6 +141,29 @@ fn pencil_with_shift_draws_a_straight_line() {
assert!((angle - 75.0).abs() < 1e-6, "{angle}");
}
/// #3799: with Alt held from the press to the release, a wavy Pencil drag draws one open straight
/// segment from the press point to the release point, at the angle the pointer took.
#[test]
fn pencil_with_alt_draws_an_open_straight_line() {
let mut s = session();
let v = view();
let alt = Mods { alt: true, ..Default::default() };
s.select_tool("pencil", v).unwrap();
s.pointer(&PointerEvent::new(PointerKind::Down, 100.0, 400.0).with_mods(alt), v).unwrap();
for (x, y) in [(120.0, 380.0), (125.0, 330.0), (160.0, 320.0), (170.0, 333.0)] {
s.pointer(&PointerEvent::new(PointerKind::Drag, x, y).with_mods(alt), v).unwrap();
}
s.pointer(&PointerEvent::new(PointerKind::Up, 170.0, 333.0).with_mods(alt), v).unwrap();
let ps = paths(&s);
assert_eq!(ps.len(), 1);
assert!(!ps[0].1.is_closed());
let sp = &ps[0].1.subpaths[0];
assert_eq!(sp.anchors.len(), 2, "{sp:?}");
assert!(sp.anchors.iter().all(|a| a.h_in == a.p && a.h_out == a.p), "no handles: {sp:?}");
assert!(sp.anchors[0].p.distance(Point::new(100.0, 400.0)) < 1e-6, "{sp:?}");
assert!(sp.anchors[1].p.distance(Point::new(170.0, 333.0)) < 1e-6, "{sp:?}");
}
#[test]
fn pencil_continues_selected_open_path() {
let mut s = session();
+6 -3
View File
@@ -463,11 +463,11 @@ fn use_precise_cursors_swaps_the_tools_own_pointers_for_a_crosshair() {
}
}
/// With Alt held, 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, 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};
use vectorcraft_tools::{Cursor, Mods, PointerEvent, PointerKind};
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
let at = vectorcraft_geom::Point::new(300.0, 300.0);
@@ -476,7 +476,10 @@ fn alt_shows_the_pencil_and_paintbrush_closing_cursors() {
s.execute("prefs.set", &json!({"key": "usePreciseCursors", "value": precise})).unwrap();
for (tool, c) in [("pencil", Cursor::PencilClose), ("paintbrush", Cursor::PaintbrushClose)] {
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}");
s.pointer(&PointerEvent::new(PointerKind::Up, 300.0, 300.0), ViewInfo::default()).unwrap();
}
}
}
+2 -2
View File
@@ -530,13 +530,13 @@ look = "Crosshair with a minus badge."
[[cursor]]
id = "Pencil"
group = "Drawing and editing tools"
shown_when = ["The Pencil tool: draws a freeform path; starting near the end of a selected open path continues it; Alt while drawing closes the path."]
shown_when = ["The Pencil tool: draws a freeform path; starting near the end of a selected open path continues it; Shift or Alt held at the press draws a straight line; Alt while drawing closes the path."]
hotspot = "The lead tip."
[[cursor]]
id = "PencilClose"
group = "Drawing and editing tools"
shown_when = ["The Pencil tool with Alt held, before a press and while drawing, when Alt key toggles to Smooth Tool is off: releasing the mouse button with Alt still held closes the path."]
shown_when = ["The Pencil tool with Alt held while drawing, when Alt key toggles to Smooth Tool is off: releasing the mouse button with Alt still held closes the path."]
hotspot = "The lead tip."
look = "The Pencil cursor with a ring badge (closing the loop), like PenClose."
+140 -16
View File
@@ -4,7 +4,8 @@
//! Pencil and Paintbrush preview the fitted path live (`path.freehand` re-applied on the snapshot)
//! and commit on release; Alt on release (or ending near the start) closes the path, and starting
//! near an end of a selected open path continues it. Shift held at the press draws a straight
//! Pencil line at 45° steps from the Constrain Angle instead. The Pencil's new path starts and ends on
//! Pencil line at 45° steps from the Constrain Angle instead, and Alt held at the press (with Alt
//! key toggles to Smooth Tool off) draws a straight line at any angle. The Pencil's new path starts and ends on
//! Smart Guides ([`DrawSnap`]: an anchor, a path, in line with the art), hovering too; the points
//! between follow the hand. The Paintbrush records the pen pressure of each point (a stroke a pen
//! pressed less than fully on sends `[x, y, pressure]` points) for pressure-sensitive brushes. The
@@ -48,9 +49,12 @@ pub struct GestureTool {
pub alt_smooth: bool,
/// Active drag is smoothing with the Pencil tool.
smoothing: bool,
/// Active Pencil drag draws a straight line (Shift held at the press): the stroke is the
/// press point and the pointer constrained to 45° steps from it.
/// Active Pencil drag draws a straight line (Shift or Alt held at the press): the stroke is
/// the press point and the pointer.
straight: bool,
/// The straight line is Alt's: the pointer is not constrained to 45° steps, and the line's end
/// snaps to Smart Guides as a stroke's end does.
unconstrained: bool,
/// Where the pointer was for each Blob Brush point dragged since Shift went down: the last
/// `shift_raw.len()` of `points`.
shift_raw: Vec<Point>,
@@ -99,6 +103,7 @@ impl GestureTool {
alt_smooth: false,
smoothing: false,
straight: false,
unconstrained: false,
shift_raw: vec![],
shift_next: None,
shift_line: None,
@@ -140,6 +145,9 @@ impl GestureTool {
/// line. Without Shift the stroke takes `p`.
fn constrained(&mut self, cx: &ToolContext, p: Point, m: Mods) -> Point {
self.shift_next = None;
if self.straight && self.unconstrained {
return p;
}
if self.straight
&& let Some(&start) = self.points.first()
{
@@ -314,7 +322,10 @@ impl Tool for GestureTool {
self.active = true;
self.began = false;
self.smoothing = self.id == "pencil" && self.alt_smooth && ev.mods.alt;
self.straight = self.id == "pencil" && !self.smoothing && ev.mods.shift;
// Alt draws a straight line only while it doesn't lend the Smooth tool; with Shift
// too, the line keeps Shift's 45° steps.
self.straight = self.id == "pencil" && !self.smoothing && (ev.mods.shift || 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.redraw = if self.draws_path() && !self.smoothing && self.extend.is_none() { self.find_redraw(cx, p) } else { None };
@@ -343,9 +354,17 @@ impl Tool for GestureTool {
// Where the stroke would end: shown, taken on release.
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.
self.snap.drag(cx, p, None)
} else {
p
};
if self.straight {
// The press's Smart Guides would stay on screen: the line's end doesn't snap.
self.snap.clear();
if !self.unconstrained {
// The press's Smart Guides would stay on screen: the line's end doesn't snap.
self.snap.clear();
}
// The line runs from the press point to the pointer, wherever the drag went.
if self.points.first().is_some_and(|s| s.distance(p) < cx.tol(1.0)) {
// Back at the press point there is no line: the one shown goes.
@@ -376,12 +395,12 @@ impl Tool for GestureTool {
return vec![];
}
let p = self.constrained(cx, p, ev.mods);
let end = if self.snaps() && !self.smoothing && !self.straight { self.snap.drag(cx, p, None) } else { p };
let end = if self.snaps() && !self.smoothing && (!self.straight || self.unconstrained) { self.snap.drag(cx, p, None) } else { p };
self.snap.clear();
let mut cancel = false;
if self.straight {
self.points.truncate(1);
if self.points.first().is_some_and(|s| s.distance(p) >= cx.tol(1.0)) {
if self.points.first().is_some_and(|s| s.distance(end) >= cx.tol(1.0)) {
self.push(end, ev.pressure);
} else {
cancel = self.began;
@@ -401,6 +420,7 @@ impl Tool for GestureTool {
self.began = false;
self.smoothing = false;
self.straight = false;
self.unconstrained = false;
self.clear();
self.extend = None;
self.redraw = None;
@@ -422,6 +442,7 @@ impl Tool for GestureTool {
self.began = false;
self.smoothing = false;
self.straight = false;
self.unconstrained = false;
self.clear();
if began { vec![Action::Cancel] } else { vec![] }
}
@@ -445,6 +466,7 @@ impl Tool for GestureTool {
self.active = false;
self.began = false;
self.straight = false;
self.unconstrained = false;
self.clear();
if began { stale.into_iter().chain([Action::Commit]).collect() } else { vec![] }
}
@@ -468,8 +490,9 @@ impl Tool for GestureTool {
return Cursor::Smooth;
}
// Alt held at the release closes the path (`finish`).
// A straight line stays open, so its release has nothing to close.
if self.draws_path() && !self.alt_smooth && m.alt && !self.straight {
// A straight line stays open, so its release has nothing to close, and before a press Alt
// gives the Pencil a straight line.
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 };
}
match self.id {
@@ -630,9 +653,9 @@ mod tests {
}
}
/// Holding Alt shows the Pencil's and the Paintbrush's closing cursor, before the press and while
/// drawing; a release with Alt still held closes the path, and Alt let go before the release
/// leaves it open.
/// Holding Alt shows the Paintbrush's closing cursor before the press, and the Pencil's and the
/// Paintbrush's while drawing (before a press, Alt gives the Pencil a straight line); a release
/// with Alt still held closes the path, and Alt let go before the release leaves it open.
#[test]
fn alt_shows_the_closing_cursor_and_a_release_with_alt_closes() {
let (d, _) = doc_with_rect();
@@ -646,7 +669,7 @@ mod tests {
{
let mut t = GestureTool::new(id);
assert_eq!(t.cursor(&cx, at, Mods::default()), own, "{id}");
assert_eq!(t.cursor(&cx, at, alt), closing, "{id}");
assert_eq!(t.cursor(&cx, at, alt), if id == "pencil" { own } else { closing }, "{id}");
for alt_at_release in [true, false] {
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 0.0, 0.0));
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 10.0, 5.0).with_mods(alt));
@@ -892,6 +915,106 @@ mod tests {
}
}
/// #3799: Alt held at the press draws a straight Pencil line at any angle from the press point
/// to the pointer, previewed as it is dragged, and left open by a release with Alt still held;
/// with Shift too it keeps Shift's 45° steps. Alt pressed after the press keeps the stroke
/// freehand and closes it at the release, and the Paintbrush keeps following the hand.
#[test]
fn pencil_alt_at_the_press_draws_a_straight_line_at_any_angle() {
let (d, _) = doc_with_rect();
let s = Selection::default();
let p = paint();
let mut cx = cx(&d, &s, &p);
cx.smart_guides = false;
let alt = Mods { alt: true, ..Default::default() };
let pts = |acts: &[Vec<Action>]| -> Vec<Vec<(f64, f64)>> {
acts.iter()
.filter_map(|a| a.iter().find_map(|a| if let Action::Preview(_, v) = a { Some(v["points"].clone()) } else { None }))
.map(|v| v.as_array().unwrap().iter().map(|q| (q[0].as_f64().unwrap(), q[1].as_f64().unwrap())).collect())
.collect()
};
let wavy = [(0.0, 0.0), (30.0, -10.0), (50.0, -60.0), (100.0, -84.0)];
let mut t = GestureTool::new("pencil");
assert_eq!(t.cursor(&cx, Point::new(0.0, 0.0), alt), Cursor::Pencil, "before the press");
let acts = drag_held(&mut t, &cx, &wavy, alt);
let all = pts(&acts);
assert_eq!(all.len(), 4, "{all:?}");
assert!(all.iter().all(|v| v.len() == 2 && v[0] == (0.0, 0.0)), "{all:?}");
assert_eq!(all[1][1], (50.0, -60.0), "the preview follows the pointer at its own angle");
assert_eq!(all[3][1], (100.0, -84.0), "{all:?}");
let last = acts.last().unwrap();
assert!(matches!(&last[0], Action::Preview(_, v) if v["closed"] == false) && last[1] == Action::Commit, "{last:?}");
// While the line is drawn, Alt shows no closing ring.
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 0.0, 0.0).with_mods(alt));
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 40.0, 10.0).with_mods(alt));
assert_eq!(t.cursor(&cx, Point::new(40.0, 10.0), alt), Cursor::Pencil);
t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 40.0, 10.0).with_mods(alt));
// Shift with Alt: 45° steps.
let all = pts(&drag_held(&mut t, &cx, &wavy, Mods { shift: true, alt: true, ..Default::default() }));
let end = *all.last().unwrap().last().unwrap();
assert!(((-end.1).atan2(end.0).to_degrees() - 45.0).abs() < 1e-9, "{end:?}");
// Alt pressed after the press: freehand, closed at the release.
let acts = drag(&mut t, &cx, &wavy, alt);
assert_eq!(pts(&acts).last().unwrap().len(), 4, "{acts:?}");
assert!(matches!(&acts.last().unwrap()[0], Action::Preview(_, v) if v["closed"] == true), "{acts:?}");
// The Paintbrush with Alt held follows the hand and closes.
let mut b = GestureTool::new("paintbrush");
let acts = drag_held(&mut b, &cx, &wavy, alt);
assert_eq!(pts(&acts).last().unwrap().len(), 4, "{acts:?}");
assert!(matches!(&acts.last().unwrap()[0], Action::Preview(_, v) if v["closed"] == true), "{acts:?}");
}
/// #3799: with Alt key toggles to Smooth Tool on, Alt held at the press smooths as before and
/// draws no line.
#[test]
fn pencil_alt_with_alt_to_smooth_on_smooths_instead_of_drawing_a_line() {
let (d, id) = doc_with_rect();
let mut s = Selection::default();
s.set([id]);
let p = paint();
let mut cx = cx(&d, &s, &p);
cx.smart_guides = false;
let alt = Mods { alt: true, ..Default::default() };
let mut t = GestureTool::new("pencil");
t.set_option("altSmooth", &json!(true));
let acts = drag_held(&mut t, &cx, &[(50.0, 150.0), (150.0, 150.0), (250.0, 160.0)], alt);
assert!(acts[..acts.len() - 1].iter().all(|a| a.is_empty()), "{acts:?}");
assert!(matches!(&acts.last().unwrap()[0], Action::Exec(c, v) if c == "path.smoothRegion" && v["points"].as_array().unwrap().len() == 3));
}
/// #3799: Alt's line starts and ends on Smart Guides as a stroke does, and pressed on an end of
/// a selected open path it continues the path from that end to the pointer.
#[test]
fn pencil_alt_line_snaps_its_ends_and_continues_a_selected_path() {
let (mut d, _) = doc_with_rect();
let alt = Mods { alt: true, ..Default::default() };
let p = paint();
{
let s = Selection::default();
let cx = cx(&d, &s, &p);
let mut t = GestureTool::new("pencil");
let acts = drag_held(&mut t, &cx, &[(102.0, 98.0), (151.0, 300.0), (198.0, 203.0)], alt);
let Some(Action::Preview(_, v)) = acts.last().and_then(|a| a.first()) else { panic!("{acts:?}") };
assert_eq!(v["points"], json!([[100.0, 100.0], [200.0, 200.0]]));
}
let l = d.layers[0].id;
let id = d.alloc_id();
let line = vectorcraft_geom::PathData::from_bezpath(&vectorcraft_geom::BezPath::from_vec(vec![
vectorcraft_geom::PathEl::MoveTo(Point::new(300.0, 300.0)),
vectorcraft_geom::PathEl::LineTo(Point::new(400.0, 300.0)),
]));
d.insert(Some(l), 1, vectorcraft_doc::Node::path(id, line, vectorcraft_doc::Appearance::default_art())).unwrap();
let mut s = Selection::default();
s.set([id]);
let mut cx = cx(&d, &s, &p);
cx.smart_guides = false;
let mut t = GestureTool::new("pencil");
let acts = drag_held(&mut t, &cx, &[(405.0, 303.0), (450.0, 340.0), (470.0, 330.0)], alt);
let Some(Action::Preview(_, v)) = acts.last().and_then(|a| a.first()) else { panic!("{acts:?}") };
assert_eq!(v["extend"], json!({"id": id.0, "end": "end"}));
assert_eq!(v["points"], json!([[400.0, 300.0], [470.0, 330.0]]));
}
#[test]
fn pencil_alt_closes_and_continues_selected_path() {
let (d, id) = doc_with_rect();
@@ -1024,9 +1147,10 @@ mod tests {
// When altSmooth is disabled:
t.set_option("altSmooth", &json!(false));
assert_eq!(t.options()["altSmooth"], json!(false));
assert_eq!(t.cursor(&cx, Point::new(50.0, 50.0), alt_mod), Cursor::PencilClose);
// Before a press, Alt gives a straight line rather than a closed path.
assert_eq!(t.cursor(&cx, Point::new(50.0, 50.0), alt_mod), Cursor::Pencil);
// Alt-drag draws a closed pencil path.
// A drag released with Alt held draws a closed pencil path.
let empty_sel = Selection::default();
let cx_empty = crate::testutil::cx(&d, &empty_sel, &p);
let acts = drag(&mut t, &cx_empty, &[(0.0, 0.0), (10.0, 5.0), (20.0, 0.0)], alt_mod);
+2 -2
View File
@@ -574,8 +574,8 @@ pub enum Cursor {
MagicWand,
/// The Pencil tool.
Pencil,
/// The Pencil tool with Alt held while Alt key toggles to Smooth Tool is off: releasing the
/// mouse button with Alt still held closes the path. A crosshair with a ring.
/// The Pencil tool with Alt held while drawing and Alt key toggles to Smooth Tool off: releasing
/// the mouse button with Alt still held closes the path. A crosshair with a ring.
PencilClose,
/// The Paintbrush tool.
Paintbrush,
+2 -2
View File
@@ -637,8 +637,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- **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).
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.
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). Option/Alt's unconstrained straight line and the straight-line cursor remain: Alt still closes the path or lends the Smooth tool.
While Alt is held, the Paintbrush (before a press and while drawing) and the Pencil with Alt key toggles to Smooth Tool off (while drawing) show a closing cursor (a crosshair with a ring); releasing the mouse button with Alt still held closes the path.
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). The straight-line cursor remains.
- **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).
- **Cutting:** Eraser, Scissors, Knife, Mirror & Cut, Line Cut and Rectangle Cut (real geometry, compound paths keep their holes).
- **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).
+1 -1
View File
@@ -91,7 +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 (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) |
| 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) |
| Average and Join in one step, with a corner or smooth choice for the joined anchor | partial | `path.join` accepts `to: corner|smooth` on the merged anchor; Average and Join are still separate commands; [gap 1](gaps.md) |