Pen + Alt converts anchors of paths in a selected group
Fixes #1273. A selected group or compound path shows its paths' anchors with the Pen, but Pen + Alt only looked at paths selected directly, so an Alt-click there started a new path instead of converting the anchor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ba4a870750
commit
5cbb7989f9
|
|
@ -1035,3 +1035,29 @@ fn join_takes_the_paths_and_the_ends_to_join() {
|
|||
assert!(s.execute("path.join", &bad).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pen_with_alt_converts_the_anchors_of_a_path_in_a_selected_group() {
|
||||
// #1273: a selected group (or compound path) shows its paths' anchors, so Alt with the Pen
|
||||
// converts them as it does a selected path's, rather than starting a new path there.
|
||||
let alt = Mods { alt: true, ..Mods::default() };
|
||||
let v = view();
|
||||
for wrap in ["object.group", "object.compoundPath.make"] {
|
||||
let mut s = session();
|
||||
let anchors = json!([{"x": 100, "y": 200}, {"x": 200, "y": 100, "in": [150, 100], "out": [250, 100]}, {"x": 300, "y": 200}]);
|
||||
let id = NodeId(s.execute("path.create", &json!({"anchors": anchors})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute(wrap, &json!({})).unwrap();
|
||||
assert!(!s.doc().unwrap().selection.objects.contains(&id), "{wrap}: the wrapper is selected, not the path");
|
||||
s.select_tool("pen", v).unwrap();
|
||||
// Alt-click the smooth anchor: a corner, in one step, and no new path.
|
||||
assert_eq!(gesture(&mut s, &[(200.0, 100.0)], alt), 1, "{wrap}");
|
||||
let a = path(&s, id).subpaths[0].anchors[1];
|
||||
assert!(!a.has_in() && !a.has_out(), "{wrap}: a corner: {a:?}");
|
||||
assert_eq!(paths(&s).len(), 1, "{wrap}: no new path");
|
||||
// Alt-drag it: smooth again.
|
||||
assert_eq!(gesture(&mut s, &[(200.0, 100.0), (220.0, 100.0), (240.0, 110.0)], alt), 1, "{wrap}");
|
||||
let a = path(&s, id).subpaths[0].anchors[1];
|
||||
assert_eq!((a.h_in, a.h_out), (Point::new(160.0, 90.0), Point::new(240.0, 110.0)), "{wrap}");
|
||||
assert_eq!(paths(&s).len(), 1, "{wrap}: no new path");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,6 +103,29 @@ pub(crate) fn editable_paths<'a>(cx: &'a ToolContext) -> impl Iterator<Item = No
|
|||
.filter(|&id| cx.doc.is_editable(id) && cx.doc.node(id).is_some_and(|n| matches!(n.kind, NodeKind::Path { guide: false, .. })))
|
||||
}
|
||||
|
||||
/// The paths whose anchors show with the Pen: the selected ones, and those inside selected groups
|
||||
/// and compound paths (not guides, nor locked or hidden ones). Pen + Alt converts their anchors.
|
||||
pub(crate) fn shown_paths(cx: &ToolContext) -> Vec<NodeId> {
|
||||
fn inside(n: &vectorcraft_doc::Node, v: &mut Vec<NodeId>) {
|
||||
match &n.kind {
|
||||
NodeKind::Path { guide: false, .. } => v.push(n.id),
|
||||
// A Shape Builder group's first child is its shaper, not art.
|
||||
NodeKind::Group { children, .. } | NodeKind::Compound { children, .. } => {
|
||||
for c in children.iter().skip(usize::from(n.shaper.is_some())) {
|
||||
inside(c, v);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut v = vec![];
|
||||
for n in cx.selection.objects.iter().filter_map(|id| cx.doc.node(*id)) {
|
||||
inside(n, &mut v);
|
||||
}
|
||||
v.retain(|id| cx.doc.is_editable(*id));
|
||||
v
|
||||
}
|
||||
|
||||
/// Nearest anchor within `tol`: (path, subpath, anchor).
|
||||
pub(crate) fn hit_anchor(cx: &ToolContext, p: Point, tol: f64) -> Option<(NodeId, usize, usize)> {
|
||||
anchor_in(cx, candidate_paths(cx), p, tol)
|
||||
|
|
|
|||
|
|
@ -107,12 +107,14 @@ fn last_anchor(cx: &ToolContext, id: NodeId) -> Option<(usize, usize)> {
|
|||
Some((si, ai))
|
||||
}
|
||||
|
||||
/// Alt held over a handle end, an anchor or a segment of a selected path (or of one inside a
|
||||
/// selected group or compound path, whose anchors show too): the Anchor Point tool's gesture,
|
||||
/// within its tolerance.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Endpoint {
|
||||
First,
|
||||
Last,
|
||||
}
|
||||
|
||||
/// Pick the closest eligible endpoint at `p`. The caller supplies the historical
|
||||
/// tie choice: closing prefers the first end, resuming prefers the last. A one-anchor path has
|
||||
/// no eligible closing end.
|
||||
|
|
@ -129,15 +131,14 @@ fn pick_endpoint(first: Option<Point>, last: Point, p: Point, tol: f64, tie: End
|
|||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Alt held over a handle end, an anchor or a segment of a selected path: the Anchor Point tool's
|
||||
/// gesture, within its tolerance.
|
||||
fn alt_converts(cx: &ToolContext, p: Point, m: Mods) -> bool {
|
||||
let point = cx.point_tol();
|
||||
m.alt
|
||||
&& (hit_handle(cx, p, point).is_some()
|
||||
|| crate::draw2::anchor_in(cx, editable_paths(cx), p, point).is_some()
|
||||
|| crate::draw2::segment_in(cx, editable_paths(cx), p, cx.pick_tol()).is_some())
|
||||
|| crate::draw2::anchor_in(cx, crate::draw2::shown_paths(cx), p, point).is_some()
|
||||
|| crate::draw2::segment_in(cx, crate::draw2::shown_paths(cx), p, cx.pick_tol()).is_some())
|
||||
}
|
||||
|
||||
/// Preview the outgoing handle of anchor `ai` of subpath `si` at `h`, the incoming one left alone.
|
||||
|
|
|
|||
Loading…
Reference in New Issue