Merge pull request #6528 from dotCooCoo/fix/insert-anchor-keeps-selection

Paths: Add Anchor Point keeps the same anchors and segments selected
This commit is contained in:
BFlat
2026-11-11 14:48:16 +07:00
committed by GitHub
4 changed files with 91 additions and 1 deletions
+10 -1
View File
@@ -135,7 +135,15 @@ pub fn specs() -> Vec<CommandSpec> {
has_doc,
reshape
),
cmd!("path.insertAnchor", "Add Anchor Point", [], None, "{id, subpath?, segment, t: 0..1}", has_doc, insert_anchor),
cmd!(
"path.insertAnchor",
"Add Anchor Point",
[],
None,
"{id, subpath?, segment, t: 0..1} add an anchor at t on the segment → {anchor}; the direct-selected anchors and selected segments keep the same points (a selected segment it splits stays selected as its two halves), and a path that isn't selected becomes the selection with the new anchor direct-selected",
has_doc,
insert_anchor
),
cmd!(
"path.cutAtAnchors",
"Cut Path at Selected Anchor Points",
@@ -1669,6 +1677,7 @@ fn insert_anchor(s: &mut Session, p: &Value) -> Result<Value> {
return Err(EngineError::Other("no such segment".into()));
}
let idx = sp.insert_anchor(seg, t);
renumber_after_insert(sel, id, si, idx);
// A path that wasn't selected becomes the selection, its new anchor direct-selected, so
// the point added shows and can be edited at once (#4237).
if !sel.contains(id) {
+76
View File
@@ -2380,6 +2380,82 @@ fn add_anchor_point_selects_the_path_it_was_added_to() {
assert!(selected_anchors(&s, kept).is_empty(), "a path selected as a whole stays so");
}
fn path_from(s: &mut Session, d: &str) -> NodeId {
NodeId(s.execute("path.create", &json!({ "d": d })).unwrap()["id"].as_u64().unwrap())
}
fn selected_segments(s: &Session, id: NodeId) -> Vec<(usize, usize)> {
s.doc().unwrap().selection.segments.get(&id).map(|set| set.iter().copied().collect()).unwrap_or_default()
}
fn selected_points(s: &Session, id: NodeId) -> Vec<Point> {
let pd = path(s, id);
selected_anchors(s, id).iter().filter_map(|&(si, ai)| pd.subpaths.get(si)?.anchors.get(ai)).map(|a| a.p).collect()
}
#[test]
fn add_anchor_point_keeps_the_same_anchors_selected() {
// The direct-selected anchors after the one added are numbered one higher, so the selection
// stays on the same points; one undo step restores the path and the selection.
let mut s = session();
let id = path_from(&mut s, "M0 0 L70 0 L140 0 L210 0 L280 0 L350 0 L420 0");
s.execute("select.anchors", &json!({"id": id.0, "anchors": [[0, 1], [0, 5]], "mode": "set"})).unwrap();
let before = selected_points(&s, id);
let r = s.execute("path.insertAnchor", &json!({"id": id.0, "segment": 2, "t": 0.5})).unwrap();
assert_eq!(r["anchor"], 3);
assert_eq!(selected_anchors(&s, id), [(0, 1), (0, 6)], "the anchor at 350 is now number 6");
assert_eq!(selected_points(&s, id), before);
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(path(&s, id).subpaths[0].anchors.len(), 7);
assert_eq!(selected_anchors(&s, id), [(0, 1), (0, 5)]);
}
#[test]
fn add_anchor_point_on_a_selected_segment_keeps_both_halves_and_the_new_anchor_selected() {
let mut s = session();
let id = path_from(&mut s, "M0 0 L70 0 L140 0 L210 0 L280 0 L350 0 L420 0");
s.execute("select.segments", &json!({"id": id.0, "segments": [[0, 2], [0, 4]], "mode": "set"})).unwrap();
s.execute("path.insertAnchor", &json!({"id": id.0, "segment": 2, "t": 0.5})).unwrap();
assert_eq!(selected_segments(&s, id), [(0, 2), (0, 3), (0, 5)]);
assert_eq!(selected_anchors(&s, id), [(0, 2), (0, 3), (0, 4), (0, 5), (0, 6)], "the new anchor is selected with the segment's ends");
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(path(&s, id).subpaths[0].anchors.len(), 7);
assert_eq!(selected_segments(&s, id), [(0, 2), (0, 4)]);
}
#[test]
fn add_anchor_point_on_the_closing_segment_keeps_it_selected_as_two_halves() {
let mut s = session();
let id = path_from(&mut s, "M0 0 L100 0 L100 100 L0 100 Z");
s.execute("select.segments", &json!({"id": id.0, "segments": [[0, 3]], "mode": "set"})).unwrap();
let r = s.execute("path.insertAnchor", &json!({"id": id.0, "segment": 3, "t": 0.5})).unwrap();
assert_eq!(r["anchor"], 4);
assert_eq!(selected_segments(&s, id), [(0, 3), (0, 4)]);
assert_eq!(selected_anchors(&s, id), [(0, 0), (0, 3), (0, 4)]);
assert_eq!(selected_points(&s, id), [Point::new(0.0, 0.0), Point::new(0.0, 100.0), Point::new(0.0, 50.0)]);
}
#[test]
fn add_anchor_point_renumbers_only_the_subpath_it_was_added_to() {
let mut s = session();
let id = path_from(&mut s, "M0 0 L100 0 L200 0 M0 100 L100 100 L200 100");
s.execute("select.anchors", &json!({"id": id.0, "anchors": [[0, 2], [1, 1], [1, 2]], "mode": "set"})).unwrap();
let before = selected_points(&s, id);
s.execute("path.insertAnchor", &json!({"id": id.0, "subpath": 1, "segment": 0, "t": 0.5})).unwrap();
assert_eq!(selected_anchors(&s, id), [(0, 2), (1, 2), (1, 3)]);
assert_eq!(selected_points(&s, id), before);
}
#[test]
fn add_anchor_point_with_a_selected_anchor_index_past_the_end_does_not_panic() {
// `select.anchors` takes any index, so a selected one can name no anchor.
let mut s = session();
let id = path_from(&mut s, "M0 0 L70 0 L140 0 L210 0");
s.execute("select.anchors", &json!({"id": id.0, "anchors": [[0, 1], [0, usize::MAX]], "mode": "set"})).unwrap();
s.execute("path.insertAnchor", &json!({"id": id.0, "segment": 0, "t": 0.5})).unwrap();
assert_eq!(selected_anchors(&s, id), [(0, 2), (0, usize::MAX)]);
}
fn selected_anchors(s: &Session, id: NodeId) -> Vec<(usize, usize)> {
s.doc().unwrap().selection.anchors.get(&id).map(|set| set.iter().copied().collect()).unwrap_or_default()
}
+4
View File
@@ -3759,6 +3759,10 @@ the canvas (coordinates out of range)".
`path.setHandle`, `path.moveAnchor` and `path.insertAnchor` edit subpath 0 when `subpath` is absent or null, and refuse a `subpath` that
is not a whole number of 0 or more, such as 1.5, -1 or "1" ("missing integer `subpath`"); `path.split` refuses such
an `anchor` or `segment` in the same way. The document does not change and no undo step is recorded.
`path.insertAnchor` keeps the direct-selected anchors and selected segments of the path on the same points: those
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.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`.
+1
View File
@@ -946,6 +946,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
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).
With Direct Selection, the Pen and the anchor tools, a group, compound path or clip group selected whole shows the anchors of its paths filled, as a path selected whole does; the direction handles still show only on a path that is selected itself. Public references: [Adjust anchor point, handle, and bounding box display size](https://web.archive.org/web/20251114182206/https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/learn-drawing-basics/adjust-anchor-point-handle-and-bounding-box-display-size.html) and [Drawing basics](https://web.archive.org/web/20231130064834/https://helpx.adobe.com/illustrator/using/drawing-basics.html) (archived copies).
When the Add Anchor Point tool, the Pen's Auto Add, the Smart Pen's Alt-click on a segment or Alt-click with the Scissors adds an anchor to a path with direct-selected anchors or segments (`path.insertAnchor`), the same points stay selected: the anchors and segments after the new anchor are numbered one higher, and a selected segment that the new anchor splits stays selected as its two halves.
`path.curvatureEdit`, `path.removeAnchor`, `path.convertAnchor`, `path.reshapeSegment`, `path.split`, `path.setHandle`, `path.moveAnchor` and `path.insertAnchor` refuse a `subpath` that is not a whole number of 0 or more, and `path.split` refuses such an `anchor` or `segment`; without a `subpath` they edit subpath 0.
`path.reshapeSegment` refuses a delta of 1e9 pt or longer, as `object.blend.spine.reshapeSegment` does, and `path.convertAnchor` and `path.freehand` refuse a point whose x or y is 1e9 pt or more in size.
`path.reshapeSegment`, `object.blend.spine.reshapeSegment`, `path.convertAnchor`, `path.curvatureEdit`, `path.insertAnchor`, `path.moveAnchors`, `path.reshape`, `path.close` and the Pencil, Curvature, Knife, Eraser, Path Eraser, Smooth, Join and Blob Brush commands refuse a number, point or handle given as text, such as "10", and leave the document unchanged; `path.convertAnchor` refuses an `x` without a `y`, or a `y` without an `x`.