Merge pull request #7029 from dotCooCoo/fix/followup-6718

Pen: Space held during a closing drag moves the first anchor
This commit is contained in:
BFlat
2026-11-13 11:26:39 +07:00
committed by GitHub
7 changed files with 93 additions and 21 deletions
+6 -1
View File
@@ -32,7 +32,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Close Path",
[],
None,
"{id, subpath?: usize (the open subpath to close, 0 included; default: the last open one), in?: [x,y] (handle into the first anchor, placed as path.setHandle places it: a smooth first anchor keeps its outgoing handle in line at its own length, a corner keeps it where it is), out?: [x,y] (the first anchor's outgoing handle, placed after `in`; the anchor becomes a corner), independent?: bool (`in` makes the first anchor a corner and leaves its outgoing handle)}",
"{id, subpath?: usize (the open subpath to close, 0 included; default: the last open one), at?: [x,y] (move the first anchor there, its handles with it, before `in` and `out` are placed), in?: [x,y] (handle into the first anchor, placed as path.setHandle places it: a smooth first anchor keeps its outgoing handle in line at its own length, a corner keeps it where it is), out?: [x,y] (the first anchor's outgoing handle, placed after `in`; the anchor becomes a corner), independent?: bool (`in` makes the first anchor a corner and leaves its outgoing handle)}",
has_doc,
close
),
@@ -205,6 +205,7 @@ fn append_anchor(s: &mut Session, p: &Value) -> Result<Value> {
/// Close the active open subpath of `id`, optionally setting the handles of the first anchor. The
/// incoming handle moves as Move Direction Handle moves it: the outgoing handle turns with it only
/// on a smooth anchor, so the first segment of a path that starts at a corner keeps its shape.
/// `at` first moves the anchor, its handles with it (the Pen's closing drag with Space held).
fn close(s: &mut Session, p: &Value) -> Result<Value> {
let id = object_param(p, "id", "path.close")?.ok_or_else(|| bad("path.close", "missing id"))?;
let subpath = p.get("subpath").and_then(Value::as_u64).map(|u| u as usize);
@@ -212,6 +213,7 @@ fn close(s: &mut Session, p: &Value) -> Result<Value> {
None => Ok(None),
Some(h) => finite_numbers(h).map(|[x, y]| Some(Point::new(x, y))).ok_or_else(|| bad("path.close", format!("`{key}` must be [x, y]"))),
};
let at = handle("at")?;
let h_in = handle("in")?;
let h_out = handle("out")?;
let independent = switch_or(p, "independent", false, "path.close")?;
@@ -223,6 +225,9 @@ fn close(s: &mut Session, p: &Value) -> Result<Value> {
};
sp.closed = true;
if let Some(first) = sp.anchors.first_mut() {
if let Some(to) = at {
first.translate(to - first.p);
}
if let Some(h) = h_in {
first.set_handle(false, h, independent);
}
+8
View File
@@ -3278,6 +3278,14 @@ fn close_path_places_the_first_anchors_handles() {
s.execute("path.close", &json!({"id": id.0, "in": [60.0, 140.0], "out": [120.0, 60.0]})).unwrap();
let b = path(&s, id).subpaths[0].anchors[0];
assert_eq!((b.h_in, b.h_out, b.kind), (Point::new(60.0, 140.0), Point::new(120.0, 60.0), AnchorKind::Corner));
// `at` moves the anchor, its handles with it, before `in` is placed.
s.execute("edit.undo", &json!({})).unwrap();
refuses_with(&mut s, "path.close", &json!({"id": id.0, "at": "120, 90"}), "`at` must be [x, y]");
s.execute("path.close", &json!({"id": id.0, "at": [120.0, 90.0], "in": [60.0, 140.0]})).unwrap();
let b = path(&s, id).subpaths[0].anchors[0];
assert_eq!((b.p, b.h_in, b.h_out), (Point::new(120.0, 90.0), Point::new(60.0, 140.0), Point::new(170.0, 90.0)), "{b:?}");
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(path(&s, id).subpaths[0].anchors[0], a, "one undo step");
}
#[test]
+1 -1
View File
@@ -368,7 +368,7 @@ impl SmartPenTool {
}
Target::Close { id, si, first, mut anchor } => {
let out = Self::handle_to(cx, first, ev.pos, first, shift, false);
let act = close_drag(id, Some(si), first - (out - first), alt, anchor.as_mut());
let act = close_drag(id, Some(si), None, first - (out - first), alt, anchor.as_mut());
pr.what = Target::Close { id, si, first, anchor };
("", act)
}
+35 -15
View File
@@ -7,7 +7,8 @@
//! incoming handle mirrors the pointer (a click leaves it retracted and the anchor a corner, so the
//! closing segment comes in straight). Its outgoing handle stays where it is on a corner, so the first segment keeps its
//! shape, and on a smooth anchor turns to stay in line at its own length; Alt makes the anchor a
//! corner, leaving the outgoing handle where it was when Alt went down. Clicking the
//! corner, leaving the outgoing handle where it was when Alt went down; Space held moves the
//! anchor, handles and all. Clicking the
//! last one retracts its outgoing handle, so the next segment leaves it as a corner; dragging from
//! it pulls a new one out on its own.
//! Alt held over a handle end, an anchor or a segment of a selected path (the one being drawn too)
@@ -76,13 +77,15 @@ const DRAG_THRESHOLD: f64 = 3.0;
/// An anchor being placed, as the press decided: where it is, the path it goes on (none: the
/// first anchor of a new path) and whether it closes that path (the press was on its first
/// anchor). The drag only shapes its handles, whatever the path looks like meanwhile: a closing
/// preview leaves the path closed, so no longer one the pen draws on.
/// preview leaves the path closed, so no longer one the pen draws on. `moved`: Space held has
/// moved the anchor from where the press found it.
#[derive(Clone, Copy)]
struct Place {
at: Point,
path: Option<NodeId>,
subpath: Option<usize>,
closing: bool,
moved: bool,
}
/// The open path the pen is extending: the single selected open path, or the single open path of a
@@ -329,7 +332,7 @@ impl Tool for PenTool {
params["subpaths"] = json!([si, other_si]);
return vec![Action::Exec("path.join".into(), params)];
}
self.drag = Some(Place { at: p, path: Some(id), subpath: Some(si), closing: false });
self.drag = Some(Place { at: p, path: Some(id), subpath: Some(si), closing: false, moved: false });
self.in_h = p;
let params = json!({"id": id.0, "x": p.x, "y": p.y, "subpath": si});
return vec![Action::Begin("Pen".into()), Action::Preview("path.appendAnchor".into(), params)];
@@ -366,7 +369,7 @@ impl Tool for PenTool {
return vec![act];
}
(self.drawing, self.path, self.subpath) = (true, None, None);
self.drag = Some(Place { at: p, path: None, subpath: None, closing: false });
self.drag = Some(Place { at: p, path: None, subpath: None, closing: false, moved: false });
vec![Action::Begin("Pen".into()), Action::Preview("path.create".into(), json!({"anchors": [{"x": p.x, "y": p.y}]}))]
}
PointerKind::Drag => {
@@ -390,13 +393,19 @@ impl Tool for PenTool {
}
return vec![set_out_handle(id, si, ai, h)];
}
let Some(Place { at: mut a, path, subpath, closing }) = self.drag else { return vec![] };
// Space held moves the anchor being placed, handles and all.
if ev.mods.space && !closing {
let Some(Place { at: mut a, path, subpath, closing, mut moved }) = self.drag else { return vec![] };
// Space held moves the anchor being placed, handles and all; while closing, the
// first anchor, with the outgoing handle a closing drag keeps.
if ev.mods.space {
let d = ev.pos - self.last;
a += d;
self.in_h += d;
self.drag = Some(Place { at: a, path, subpath, closing });
if closing && let Some((anchor, out)) = &mut self.first {
anchor.translate(d);
*out += d;
}
moved |= d != Vec2::ZERO;
self.drag = Some(Place { at: a, path, subpath, closing, moved });
}
self.last = ev.pos;
let mut out_h = ev.pos;
@@ -413,7 +422,7 @@ impl Tool for PenTool {
)];
};
if closing {
return vec![close_drag(id, subpath, a - (out_h - a), alt, self.first.as_mut())];
return vec![close_drag(id, subpath, moved.then_some(a), a - (out_h - a), alt, self.first.as_mut())];
}
// Alt pressed mid-drag keeps the curve already shaped into the anchor: only the
// outgoing handle follows. Held from the start, the incoming handle stays retracted.
@@ -583,7 +592,7 @@ impl PenTool {
/// anchor a corner with its incoming handle retracted until a drag pulls it out, as a click on
/// the last anchor retracts its outgoing one ([`set_out_handle`]).
fn begin_close(&mut self, cx: &ToolContext, id: NodeId, si: usize, at: Point) -> Vec<Action> {
self.drag = Some(Place { at, path: Some(id), subpath: Some(si), closing: true });
self.drag = Some(Place { at, path: Some(id), subpath: Some(si), closing: true, moved: false });
self.first = first_anchor(cx, id, si).map(|a| (a, a.h_out));
let params = json!({"id": id.0, "subpath": si, "in": [at.x, at.y], "independent": true});
vec![Action::Begin("Close Path".into()), Action::Preview("path.close".into(), params)]
@@ -609,12 +618,23 @@ pub(crate) fn first_anchor(cx: &ToolContext, id: NodeId, si: usize) -> Option<An
cx.doc.node(id)?.path_data()?.subpaths.get(si)?.anchors.first().copied()
}
/// The preview of a closing drag that puts the first anchor's incoming handle at `in_h`. `first`
/// holds that anchor as it was when the press began closing the path, and its outgoing handle as
/// the drag last left it: a smooth anchor turns that handle to stay in line, and Alt (`alt`), which
/// makes the anchor a corner, keeps it there.
pub(crate) fn close_drag(id: NodeId, subpath: Option<usize>, in_h: Point, alt: bool, first: Option<&mut (Anchor, Point)>) -> Action {
/// The preview of a closing drag that puts the first anchor's incoming handle at `in_h`, the anchor
/// first moved to `at` when given (Space held). `first` holds that anchor as it was when the press
/// began closing the path (moved with it), and its outgoing handle as the drag last left it: a
/// smooth anchor turns that handle to stay in line, and Alt (`alt`), which makes the anchor a
/// corner, keeps it there.
pub(crate) fn close_drag(
id: NodeId,
subpath: Option<usize>,
at: Option<Point>,
in_h: Point,
alt: bool,
first: Option<&mut (Anchor, Point)>,
) -> Action {
let mut params = json!({"id": id.0, "in": [in_h.x, in_h.y], "independent": alt});
if let Some(at) = at {
params["at"] = json!([at.x, at.y]);
}
if let Some((anchor, out)) = first {
if alt {
params["out"] = json!([out.x, out.y]);
+37
View File
@@ -628,6 +628,43 @@ fn alt_mid_close_keeps_the_turned_outgoing_handle() {
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Up, 130.0, 200.0)), vec![Action::Commit]);
}
/// Space held during a closing drag moves the first anchor, handles and all, by as far as the
/// pointer moves; released, the drag shapes the handles round the anchor's new place.
#[test]
fn space_moves_the_closing_anchor() {
use vectorcraft_geom::{Anchor, SubPath};
let anchors = vec![Anchor::smooth(Point::new(100.0, 300.0), Point::new(150.0, 300.0)), Anchor::corner(Point::new(200.0, 200.0))];
let (mut d, id, s) = drawing(SubPath { anchors, closed: false });
let p = paint();
let mut t = PenTool { drawing: true, ..PenTool::default() };
assert_eq!(t.pointer(&cx(&d, &s, &p), &PointerEvent::new(PointerKind::Down, 100.0, 300.0)), close_press(id, Point::new(100.0, 300.0)));
d.node_mut(id).unwrap().path_data_mut().unwrap().subpaths[0].closed = true;
let c = cx(&d, &s, &p);
let close = |v: serde_json::Value| vec![Action::Preview("path.close".into(), v)];
assert_eq!(
t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 100.0, 200.0)),
close(json!({"id": id.0, "in": [100.0, 400.0], "independent": false, "subpath": 0}))
);
let space = Mods { space: true, ..Mods::default() };
assert_eq!(
t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 130.0, 180.0).with_mods(space)),
close(json!({"id": id.0, "at": [130.0, 280.0], "in": [130.0, 380.0], "independent": false, "subpath": 0})),
"moved, handles and all"
);
// Space released: the drag shapes the handles round the anchor's new place.
assert_eq!(
t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 130.0, 230.0)),
close(json!({"id": id.0, "at": [130.0, 280.0], "in": [130.0, 330.0], "independent": false, "subpath": 0}))
);
// Alt keeps the outgoing handle where the drag had turned it, moved with the anchor.
let alt = Mods { alt: true, ..Mods::default() };
assert_eq!(
t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 160.0, 230.0).with_mods(alt)),
close(json!({"id": id.0, "at": [130.0, 280.0], "in": [100.0, 330.0], "out": [130.0, 230.0], "independent": true, "subpath": 0}))
);
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Up, 160.0, 230.0)), vec![Action::Commit]);
}
#[test]
fn a_one_anchor_path_does_not_close_on_itself() {
let (d, id, s) = drawing(vectorcraft_geom::SubPath::polyline(&[Point::new(10.0, 300.0)], false));
+5 -3
View File
@@ -4698,13 +4698,15 @@ an `anchor` or `segment` in the same way. The document does not change and no un
after the new anchor on its subpath are numbered one higher, and a selected segment that the new anchor splits stays
selected as its two halves, with the new anchor as an end of both that moves with them. Undo restores the path and
the selection in one step.
`path.close {id, subpath?, in?, out?, independent?}` closes an open subpath (by default the last open one). `in` places
`path.close {id, subpath?, at?, in?, out?, independent?}` closes an open subpath (by default the last open one). `at`
moves the first anchor to `[x, y]`, its handles with it, before `in` and `out` are placed. `in` places
the first anchor's incoming handle as `path.setHandle` places a handle: a smooth anchor turns its outgoing handle to
stay in line and keeps that handle's length, and a corner keeps its outgoing handle where it is. With
`independent: true` the anchor becomes a corner first, so its outgoing handle does not move. `out`, placed after `in`,
is the first anchor's outgoing handle, and makes the anchor a corner. A Pen press on the first anchor sends `in` at
the anchor with `independent: true`. The Pen's and the Smart Pen's closing drags send the mirror of the pointer as
`in`, and once Alt is held they also send `out` at the outgoing handle's position when Alt went down.
`in`, and once Alt is held they also send `out` at the outgoing handle's position when Alt went down. Once Space held
during the Pen's closing drag has moved the first anchor, the drag also sends `at` at the anchor's new position.
`path.reshape {id, x, y, dx, dy, tol?, focal?, radius?}` (a Reshape tool drag) grabs the path at `x`, `y`, adding an
anchor there unless one lies within `tol` (3 by default), and moves that point by `dx`, `dy`.
@@ -4753,7 +4755,7 @@ an `x` without a `y`, or a `y` without an `x`, with "x and y must be given toget
`path.blob` refuse a point in `points` whose x or y is not a number, such as `["10", 10]` or a point written as a
string, with "x and y must both be numbers". All of them except `path.curvature` refuse a pen pressure, tilt or
rotation that is neither a number nor null with `` `pressure` must be a number, not "0.5" ``, naming the key
(`pressure`, `tiltX`, `tiltY` or `rotation`). `path.close` refuses an `in` or `out` that is not `[x, y]`
(`pressure`, `tiltX`, `tiltY` or `rotation`). `path.close` refuses an `at`, `in` or `out` that is not `[x, y]`
with `` `in` must be [x, y] ``, naming the key.
These commands refuse a string, a number or another value that is not true or false with
+1 -1
View File
@@ -1025,7 +1025,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
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).
Transform Each opens on the scale, move, rotate, reflect, Random, reference point, Transform Objects and Preview it was last confirmed with (kept with the UI state; Copy never is), and its Copy button, left of Cancel, transforms a copy of each object as the [Scale multiple objects](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/scale-multiple-objects.html) help page has it; the Copy checkbox is gone (#1732; the remembered values follow a maintainer comment on the issue, not the help page). Its Scale section has the Relative and Absolute radio buttons the same page describes: percentages of each object's size, or the width and height it becomes (`object.transformEach` `relative`, which Transform Again repeats); Absolute measures each object's own box, and the scales act along its axes about that box's reference point, so a turned shape keeps its shape and objects of different sizes come out alike; the fields show the width and height the selected objects share and read "Mixed" when they differ or a side is 0 (a line's), each object measured by its own box, rotated with it, as the Transform panel and the Control bar show it (sizes they show alike count as the same), and a size typed into them sets every object to it (a side left Mixed keeps its size), our choices where the page leaves them open; a live Repeat scales its spacing by the geometric mean of the two factors, so its Absolute size is approximate. The link beside the two scales keeps them in proportion, as the shape dialogs' Width and Height link does (`__constrain`, kept with the dialog's values; where it sits is our choice, the public pages don't establish it). The panel lists Scale, Move and Rotate, then the Options in two columns (Transform Objects, Transform Patterns, Scale Strokes & Effects, Scale Corners; Reflect X, Reflect Y, Random), the Reference Point, and Preview at the left of the button row, level with Copy, Cancel and OK.
Transform Again uses the Copy, Transform Patterns, Scale Strokes & Effects and Scale Corners choices of the transform it repeats; after a bounding-box handle drag on area type, it resizes the type area again and the type keeps its size (`object.transformAgain`). With anchors selected with Direct Selection, Move, Rotate, Scale, Reflect, Shear and Transform Again (the commands, their dialogs and the four tools' drags) transform only those anchors, their handles with them, about the centre of the box around them unless a point was clicked; a copy still copies the whole object (#1408; behaviour reference: [adjust path segments](https://helpx.adobe.com/illustrator/using/adjust-path-segments.html); the default reference point and the copy are our choices). The Move, Rotate, Scale, Reflect and Shear dialogs have a Preview checkbox, off when they open: while it is on the canvas follows the values, Cancel rolls it back and OK is still one undo step; Up/Down step the Rotate and Shear angles and the Scale percentages by 1, Shift by 10 (#1412). The five dialogs and Transform Each have Transform Objects beside Transform Patterns: off, the art stays and only its pattern tiles transform (`objects` on the transform commands), and Transform Again repeats the choice (#1448; behaviour reference: [scale objects](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/scale-objects.html); the checkboxes are always enabled and the Scale page's pair is applied to all of them, our choices). Reflect's axis is Horizontal, Vertical or an angle (#1460; [reflect objects along an axis](https://helpx.adobe.com/illustrator/desktop/manage-objects/arrange-objects/reflect-objects-along-an-axis.html)).
- **Pen:** Alt pressed in the middle of dragging out an anchor splits its handles and keeps the curve already shaped into it (only the outgoing handle follows the pointer); Space held while dragging out an anchor moves it, handles and all. A press on the first anchor of the path being drawn closes it whatever the drag then does: the drag shapes the closing curve and the release closes the path wherever it ends (#501). The first anchor's incoming handle mirrors the pointer, and a click leaves it retracted and the anchor a corner. Its outgoing handle stays where it is on a corner, so the first segment keeps its shape, and on a smooth anchor it turns to stay in line at its own length; Alt makes the anchor a corner and leaves the outgoing handle where it was when Alt went down. The Smart Pen's closing drag does the same (fixes #6634; `path.close`'s `in` and `out`; behavior reference: [draw with the Pen, Curvature, or Pencil tool](https://web.archive.org/web/20220614224656/https://helpx.adobe.com/illustrator/using/drawing-pen-curvature-or-pencil.html), where a click or drag on the first anchor closes the path and Alt breaks the pairing of the closing anchor's handles; which handles the drag sets are our choices). Moving the closing anchor with Space held, which that page also describes, is still missing. Clicking the last anchor of the path being drawn retracts its outgoing handle (convert cursor), and dragging from it pulls out a new one on its own. Auto Add/Delete: over a selected path, a click on a segment adds an anchor and a click on an anchor deletes it (add and delete cursors); Shift held, or Preferences › General › Disable Auto Add/Delete, starts a new path instead. Alt held over a handle end or an anchor of a selected path (the one being drawn too) works as the Anchor Point tool, with its convert cursor: dragging a handle moves it alone, clicking a smooth anchor makes it a corner, dragging an anchor pulls out new symmetric handles, dragging a segment reshapes it; drawing then goes on. Cmd (Ctrl) held drags with the selection tool chosen last, Direct Selection until one is (handles, anchors and segments of the path being drawn or another), and the Pen then goes on drawing its path while that stays selected; the pointer shows that tool's while Cmd is held. The same holds for agents (`pointer_gesture` with `mods.cmd`), as the borrowing now lives in the engine (#494). A drag's preview (the Pen's, the Anchor Point tool's, Direct Selection's handles and anchors) follows every position the pointer is seen at (#834): the canvas sends the tool a drag whenever the pointer's document position differs from the last one it sent, not only when egui's frame delta is non-zero (zero when a compositor coalesces motion and in the frame the pointer comes back into the window after leaving it), and a move that arrives in the release's own frame shapes the preview before the release commits it, once the press has dragged. With the Pen, a press that moves under 3 screen pixels is a click (a corner without handles), and a Precise Cursors command toggles the existing preference and can take a shortcut (#1663).
- **Pen:** Alt pressed in the middle of dragging out an anchor splits its handles and keeps the curve already shaped into it (only the outgoing handle follows the pointer); Space held while dragging out an anchor moves it, handles and all. A press on the first anchor of the path being drawn closes it whatever the drag then does: the drag shapes the closing curve and the release closes the path wherever it ends (#501). The first anchor's incoming handle mirrors the pointer, and a click leaves it retracted and the anchor a corner. Its outgoing handle stays where it is on a corner, so the first segment keeps its shape, and on a smooth anchor it turns to stay in line at its own length; Alt makes the anchor a corner and leaves the outgoing handle where it was when Alt went down. The Smart Pen's closing drag does the same (fixes #6634; `path.close`'s `in` and `out`; behavior reference: [draw with the Pen, Curvature, or Pencil tool](https://web.archive.org/web/20220614224656/https://helpx.adobe.com/illustrator/using/drawing-pen-curvature-or-pencil.html), where a click or drag on the first anchor closes the path and Alt breaks the pairing of the closing anchor's handles; which handles the drag sets are our choices). Space held during the Pen's closing drag moves the first anchor, handles and all, as that page also describes (`path.close`'s `at`). Clicking the last anchor of the path being drawn retracts its outgoing handle (convert cursor), and dragging from it pulls out a new one on its own. Auto Add/Delete: over a selected path, a click on a segment adds an anchor and a click on an anchor deletes it (add and delete cursors); Shift held, or Preferences › General › Disable Auto Add/Delete, starts a new path instead. Alt held over a handle end or an anchor of a selected path (the one being drawn too) works as the Anchor Point tool, with its convert cursor: dragging a handle moves it alone, clicking a smooth anchor makes it a corner, dragging an anchor pulls out new symmetric handles, dragging a segment reshapes it; drawing then goes on. Cmd (Ctrl) held drags with the selection tool chosen last, Direct Selection until one is (handles, anchors and segments of the path being drawn or another), and the Pen then goes on drawing its path while that stays selected; the pointer shows that tool's while Cmd is held. The same holds for agents (`pointer_gesture` with `mods.cmd`), as the borrowing now lives in the engine (#494). A drag's preview (the Pen's, the Anchor Point tool's, Direct Selection's handles and anchors) follows every position the pointer is seen at (#834): the canvas sends the tool a drag whenever the pointer's document position differs from the last one it sent, not only when egui's frame delta is non-zero (zero when a compositor coalesces motion and in the frame the pointer comes back into the window after leaving it), and a move that arrives in the release's own frame shapes the preview before the release commits it, once the press has dragged. With the Pen, a press that moves under 3 screen pixels is a click (a corner without handles), and a Precise Cursors command toggles the existing preference and can take a shortcut (#1663).
- **Direct Selection (#1278):** clicking a segment picks its connection, with hollow endpoint anchors and only that curve's direction handles. Delete leaves the neighboring curves and appearance intact, opens/splits contours in one undo step, and another Delete removes the remaining selected path. A whole-path drag and deliberate anchor selection keep their existing behavior; egui frames and native control/art checked, OS mouse/visual QA pending. Where anchors of overlapping objects coincide, a click or drag takes the anchor of the selected object, also when another object lies over it (#1509, fixes #1508). Delete removes every selected anchor of a closed path (it used to remove the wrong ones), removes objects selected whole along with the selected anchors of others, and keeps the remainder selected so a second Delete removes it (#1674). In isolation mode Direct Selection and Group Selection pick only inside the isolated object (#1666). Escape cancels a Direct Selection drag (#1803, #1743). Double-clicking type (including type nested inside a group) switches to the Type tool and puts the caret at the clicked character.
With Direct Selection and Group Selection, a drag that starts on an object while the selection also holds direct-selected anchors or segments moves those anchors and the objects selected whole in one undo step (`path.moveAnchors`), as a drag of a selected anchor does, and the other anchors of a partly selected path stay in place. While Alt is held during the drag, it copies the selected objects instead (`object.transform`). When direct-selected anchors are moved or transformed together with objects selected whole (such a drag, an anchor drag, an arrow key, or Move, Rotate, Scale, Reflect and Shear without a copy), a path inside a selected group is transformed once, with the group.
A path segment with a control point whose x or y is infinite or larger in size than `f64::MAX / 16` is measured by its control points, so its bounds hold the whole curve and the canvas check refuses an edit that leaves such a path selected (`path.create` included).