mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 18:54:55 +00:00
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:
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user