mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 12:21:54 +00:00
Part of #6361: Cut takes direct-selected anchors out of the path
With anchors picked with Direct Selection, Edit > Cut removed only the segments between them, so the picked anchors stayed in the path. Cut now removes picked anchors and the segments attached to them from a path or a compound path's member, as Clear does. Picked segments still lose only those segments. When a path has picked segments, Copy and Cut leave out anchors picked on its other contours, as Clear does. Docs: docs/target-app-parity.md.
This commit is contained in:
@@ -115,7 +115,8 @@ pub(crate) fn subpath_selected_segments(
|
||||
let n = sp.anchors.len();
|
||||
let mut selected_segs = BTreeSet::new();
|
||||
|
||||
let has_explicit_segs = seg_refs.is_some_and(|segments| segments.iter().any(|&(s, _)| s == si));
|
||||
// Segments picked anywhere on the path leave anchors out of it, as Clear does.
|
||||
let has_explicit_segs = seg_refs.is_some_and(|segments| !segments.is_empty());
|
||||
if has_explicit_segs {
|
||||
if let Some(segments) = seg_refs {
|
||||
for &(s, seg) in segments {
|
||||
|
||||
@@ -1063,8 +1063,12 @@ fn cut_direct_selection(s: &mut Session, roots: &[NodeId]) -> Result<Value> {
|
||||
let cuts = super::clipboard::subpath_selected_segments(&sp, si, seg_refs, anchor_refs);
|
||||
if cuts.is_empty() {
|
||||
subpaths.push(sp);
|
||||
} else {
|
||||
} else if seg_refs.is_some_and(|segments| !segments.is_empty()) {
|
||||
subpaths.extend(super::path::without_segments(sp, &cuts));
|
||||
} else {
|
||||
// Picked anchors leave the path with their connections, as Clear removes them.
|
||||
let gone: BTreeSet<usize> = anchor_refs.into_iter().flatten().filter_map(|&(s, a)| (s == si).then_some(a)).collect();
|
||||
subpaths.extend(super::path::without_anchors(sp, &gone));
|
||||
}
|
||||
}
|
||||
subpaths.retain(|sp| sp.anchors.len() >= 2);
|
||||
|
||||
@@ -1329,7 +1329,7 @@ pub(super) fn replace_deleted_pieces(d: &mut Document, beside: &mut Beside, orig
|
||||
|
||||
/// Remove anchors and their incident segments, keeping each surviving stretch as an open path.
|
||||
/// Only a closed path can have a surviving stretch that wraps past the last anchor.
|
||||
fn without_anchors(sp: SubPath, removed: &BTreeSet<usize>) -> Vec<SubPath> {
|
||||
pub(super) fn without_anchors(sp: SubPath, removed: &BTreeSet<usize>) -> Vec<SubPath> {
|
||||
let n = sp.anchors.len();
|
||||
let gone: BTreeSet<usize> = removed.iter().copied().filter(|&i| i < n).collect();
|
||||
if gone.is_empty() {
|
||||
|
||||
@@ -2698,19 +2698,15 @@ fn pen_cmd_selected_anchors_are_what_copy_and_cut_take() {
|
||||
assert_eq!(s.doc().unwrap().selection.anchors[&id], [(0, 1), (0, 2)].into());
|
||||
s.execute("edit.copy", &json!({})).unwrap();
|
||||
assert_eq!(pasted(&mut s), [2]);
|
||||
// Cut takes the segment between them. The two surviving runs of an ordinary path
|
||||
// become independent objects, and this path keeps the first run (#3380).
|
||||
// Cut takes the two anchors out of the path, as Clear does (#6361): each side keeps one
|
||||
// anchor, which makes no path, so nothing is left.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let original = path(&s, id);
|
||||
let picked = s.doc().unwrap().selection.clone();
|
||||
s.execute("edit.cut", &json!({})).unwrap();
|
||||
let left = paths(&s);
|
||||
assert_eq!(left.len(), 2);
|
||||
assert_eq!(left[0].0, id);
|
||||
assert!(left.iter().all(|(_, p)| p.subpaths.len() == 1 && !p.subpaths[0].closed));
|
||||
assert_eq!(left[0].1.subpaths[0].anchors, original.subpaths[0].anchors[..2]);
|
||||
assert_eq!(left[1].1.subpaths[0].anchors, original.subpaths[0].anchors[2..]);
|
||||
assert_eq!(s.doc().unwrap().selection.objects, left.iter().map(|(id, _)| *id).collect::<Vec<_>>());
|
||||
assert!(left.is_empty());
|
||||
assert!(s.doc().unwrap().selection.objects.is_empty());
|
||||
assert_eq!(s.clipboard.nodes.len(), 1);
|
||||
assert_eq!(s.clipboard.nodes[0].path_data().unwrap().subpaths[0].anchors, original.subpaths[0].anchors[1..3]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
|
||||
@@ -192,7 +192,7 @@ fn pen_tool_drawing_copies_whole_path_not_single_anchor() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cut_adjacent_anchors_clipboard_plus_remaining_equals_original() {
|
||||
fn cut_adjacent_anchors_takes_them_out_of_the_source() {
|
||||
let mut s = Session::new();
|
||||
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
||||
let r = rect(&mut s, 10.0, 10.0, 100.0, 50.0);
|
||||
@@ -207,15 +207,93 @@ fn cut_adjacent_anchors_clipboard_plus_remaining_equals_original() {
|
||||
assert_eq!(clip_path.subpaths[0].segment_count(), 1);
|
||||
assert_eq!(clip_path.subpaths[0].anchors.len(), 2);
|
||||
|
||||
// Remaining source path must have the remaining 3 segments (segments 0, 2, 3), not 1.
|
||||
// The source keeps anchors 3 and 0 and the one segment between them.
|
||||
let src_node = s.doc().unwrap().doc.node(r).unwrap();
|
||||
let src_path = src_node.path_data().unwrap();
|
||||
assert_eq!(src_path.subpaths[0].segment_count(), 3);
|
||||
assert_eq!(src_path.subpaths[0].anchors.len(), 4);
|
||||
assert_eq!(src_path.subpaths[0].segment_count(), 1);
|
||||
assert_eq!(src_path.subpaths[0].anchors.len(), 2);
|
||||
assert!(!src_path.subpaths[0].closed);
|
||||
}
|
||||
|
||||
// Sum of segments in clipboard + remaining = 1 + 3 = 4 (the original 4 segments).
|
||||
assert_eq!(clip_path.subpaths[0].segment_count() + src_path.subpaths[0].segment_count(), 4);
|
||||
/// Every contour left in the document after `command` runs on anchors `picked` of a four-anchor
|
||||
/// open path, as `[x, y]` lists. With `compound`, the path is a member of a compound path.
|
||||
fn contours_after(command: &str, picked: Value, compound: bool) -> Vec<Vec<[f64; 2]>> {
|
||||
let mut s = Session::new();
|
||||
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
||||
let id = NodeId(run(&mut s, "path.create", json!({"anchors": [{"x": 10, "y": 20}]}))["id"].as_u64().unwrap());
|
||||
run(&mut s, "path.appendAnchor", json!({"id": id.0, "x": 60, "y": 20}));
|
||||
run(&mut s, "path.appendAnchor", json!({"id": id.0, "x": 100, "y": 80}));
|
||||
run(&mut s, "path.appendAnchor", json!({"id": id.0, "x": 150, "y": 80}));
|
||||
if compound {
|
||||
let other = rect(&mut s, 10.0, 100.0, 40.0, 40.0);
|
||||
run(&mut s, "select.set", json!({"ids": [id.0, other.0]}));
|
||||
run(&mut s, "object.compoundPath.make", json!({}));
|
||||
}
|
||||
run(&mut s, "select.anchors", json!({"id": id.0, "anchors": picked, "mode": "set"}));
|
||||
run(&mut s, command, json!({}));
|
||||
check_session(&s).unwrap();
|
||||
contours(&s)
|
||||
}
|
||||
|
||||
/// Every contour in the document, as `[x, y]` lists.
|
||||
fn contours(s: &Session) -> Vec<Vec<[f64; 2]>> {
|
||||
let mut left = vec![];
|
||||
for layer in &s.doc().unwrap().doc.layers {
|
||||
layer.walk(&mut |n| {
|
||||
if let Some(path) = n.path_data() {
|
||||
left.extend(path.subpaths.iter().map(|sp| sp.anchors.iter().map(|a| [a.p.x, a.p.y]).collect::<Vec<_>>()));
|
||||
}
|
||||
});
|
||||
}
|
||||
left
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cut_end_anchors_leaves_the_source_as_clear_does() {
|
||||
for compound in [false, true] {
|
||||
let picked = || json!([[0, 0], [0, 1]]);
|
||||
let left = contours_after("edit.cut", picked(), compound);
|
||||
assert_eq!(left, contours_after("edit.clear", picked(), compound), "compound: {compound}");
|
||||
assert_eq!(left[0], [[100.0, 80.0], [150.0, 80.0]], "compound: {compound}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cut_interior_anchors_leaves_the_source_as_clear_does() {
|
||||
let picked = || json!([[0, 1], [0, 2]]);
|
||||
let left = contours_after("edit.cut", picked(), true);
|
||||
assert_eq!(left, contours_after("edit.clear", picked(), true));
|
||||
assert!(left.iter().flatten().all(|p| p[0] != 60.0 && p[0] != 100.0), "the picked anchors are gone: {left:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cut_one_end_anchor_of_a_compound_member_shortens_it() {
|
||||
let left = contours_after("edit.cut", json!([[0, 3]]), true);
|
||||
assert_eq!(left[0], [[10.0, 20.0], [60.0, 20.0], [100.0, 80.0]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cut_of_an_anchor_with_a_segment_on_another_contour_leaves_the_source_as_clear_does() {
|
||||
let after = |command: &str| {
|
||||
let mut s = Session::new();
|
||||
run(&mut s, "file.new", json!({"width": 400, "height": 400}));
|
||||
let id = NodeId(run(&mut s, "path.create", json!({"anchors": [{"x": 10, "y": 20}]}))["id"].as_u64().unwrap());
|
||||
let contour = |y: i32| json!({"anchors": [{"x": 10, "y": y}, {"x": 60, "y": y}, {"x": 110, "y": y}], "closed": false});
|
||||
run(&mut s, "path.setAnchors", json!({"id": id.0, "subpaths": [contour(20), contour(120)]}));
|
||||
run(&mut s, "select.anchors", json!({"id": id.0, "anchors": [[0, 0]], "mode": "set"}));
|
||||
run(&mut s, "select.segments", json!({"id": id.0, "segments": [[1, 1]], "mode": "add"}));
|
||||
run(&mut s, command, json!({}));
|
||||
check_session(&s).unwrap();
|
||||
s
|
||||
};
|
||||
let cut = after("edit.cut");
|
||||
assert_eq!(contours(&cut), contours(&after("edit.clear")));
|
||||
assert!(contours(&cut).contains(&vec![[10.0, 20.0], [60.0, 20.0], [110.0, 20.0]]), "the picked anchor stays: {:?}", contours(&cut));
|
||||
// The clipboard holds what left the source: the picked segment alone.
|
||||
assert_eq!(cut.clipboard.nodes.len(), 1);
|
||||
let clip = cut.clipboard.nodes[0].path_data().unwrap();
|
||||
assert_eq!(clip.subpaths.len(), 1);
|
||||
assert_eq!(clip.subpaths[0].anchors.iter().map(|a| [a.p.x, a.p.y]).collect::<Vec<_>>(), [[60.0, 120.0], [110.0, 120.0]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -578,6 +578,7 @@ since 2026-10-01.
|
||||
|
||||
- **Clipping paths on their outline (G1):** the selection tools (Selection, Direct Selection, Group Selection), Object Highlighting and a right-click take a clipping path on its outline before the art it clips; Select Behind lists the same; the eyedropper, the tools that act on what is drawn and the selection tools' double-clicks, loaded text and guide release take the art. Doc and tool regression tests; score unchanged.
|
||||
- **Copy and Cut of anchors picked with a lent Direct Selection (#6123):** with the Pen or the Curvature tool, Cmd/Ctrl held lends Direct Selection for the gesture; the anchors and segments it picks are what Edit › Copy and Cut take once the key is let go, as with Direct Selection chosen, while the last anchor the Pen leaves selected as it draws still makes Copy take the whole path (#2122). A Cmd gesture that leaves the selection as it was (a Shift marquee over nothing) picks nothing. Regression test: `tests::draw2::pen_cmd_selected_anchors_are_what_copy_and_cut_take`.
|
||||
- **Cut of picked anchors (#6361):** Edit › Cut removes direct-selected anchors from their path and from a compound path's member, with the segments attached to them, as Clear does. Picked segments still lose only those segments. Regression tests: `cut_*_anchors_*` in `crates/engine/tests/it/copy_direct_selection.rs`.
|
||||
- **Escape in Selection tool drags:** Escape during a Selection tool move or Alt-drag copy, a bounding-box scale or rotation, a marquee, or a corner widget, pie widget or bracket drag cancels it: the art returns to its state at the press, no copy is left, no undo step is added, the release does nothing, and the tool stays chosen. This is VectorCraft's own behavior. Regression tests: `escape_cancels_*` in `select/tests.rs` and `pie.rs`, and `escape_cancels_a_selection_tool_move_copy_scale_or_rotation` in `tests/selectescape.rs`.
|
||||
- **Global Edit: Shift-click leaves a match out:** while Global Edit is on, a Shift-click with the Selection tool on a highlighted match, or on an object inside a matched group, leaves that match out (`select.globalEditExclude`): it is no longer highlighted and later edits leave it as it is; the object the edit started from stays, and Global Edit ends when fewer than two matches are left ([Update assets quickly with Global Edit](https://helpx.adobe.com/ph_fil/illustrator/how-to/global-editing-sensei.html)). Regression tests: `a_shift_click_*`, `an_object_left_out_*`, `a_group_left_out_*` and `leaving_out_all_but_one_*` in `crates/engine/src/tests/globaledit.rs`.
|
||||
- **Global Edit Options in the Control bar:** while Select › Start Global Edit can run, the Control bar shows a Global Edit Options dropdown that opens the same options as the Properties panel's (Match: Appearance, Size; On Artboards; Include Objects On Canvas; Result(s)); both set the same options. The dropdown is off while Global Edit is on; it sits after Recolor Artwork, ahead of the X/Y/W/H fields ([Edit similar objects globally](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/edit-similar-objects.html)). Regression test: `the_control_bar_shows_the_global_edit_options` in `crates/ui-egui/src/chrome/tests.rs`.
|
||||
|
||||
Reference in New Issue
Block a user