Merge pull request #6718 from dotCooCoo/fix/issue-6634

Fix #6634: Pen closing drag no longer gives the first anchor symmetric handles
This commit is contained in:
BFlat
2026-11-12 09:08:12 +07:00
committed by GitHub
10 changed files with 309 additions and 61 deletions
+51
View File
@@ -285,3 +285,54 @@ fn the_tool_draws_a_path_one_step_per_anchor_and_closes_it() {
assert_eq!(path(&s, id).subpaths[0].anchors.len(), 3);
assert_eq!(steps(&s), n + 8);
}
/// #6634: the tool's closing drag on the first anchor sets that anchor's incoming handle to the
/// mirror of the pointer and does not give it symmetric handles: a corner keeps its outgoing
/// handle, a smooth anchor turns it to stay in line at its own length, and Alt pressed mid-drag
/// keeps it where the drag had turned it.
#[test]
fn the_tools_closing_drag_does_not_make_the_first_anchor_symmetric() {
// An open path whose first anchor, at (50, 50), has the handles `first`, drawn on from its last
// anchor; the closing gesture is then up to the caller.
let draw = |first: Value| {
let mut s = session();
let mut a = json!({"x": 50, "y": 50});
a.as_object_mut().unwrap().extend(first.as_object().unwrap().clone());
let anchors = json!([a, {"x": 150, "y": 50}, {"x": 150, "y": 150}]);
let id = NodeId(s.execute("path.create", &json!({"anchors": anchors})).unwrap()["id"].as_u64().unwrap());
s.select_tool("smartPen", view()).unwrap();
gesture(&mut s, &[(150.0, 150.0)], Mods::default());
(s, id)
};
let first = |s: &Session, id: NodeId| {
let sp = &path(s, id).subpaths[0];
assert!(sp.closed, "{sp:?}");
sp.anchors[0]
};
let near = |a: Point, b: Point| a.distance(b) < 1e-6;
// A corner: the first segment keeps its shape.
let (mut s, id) = draw(json!({"out": [80, 30]}));
gesture(&mut s, &[(50.0, 50.0), (50.0, 100.0)], Mods::default());
let a = first(&s, id);
assert_eq!((a.h_in, a.h_out, a.kind), (Point::new(50.0, 0.0), Point::new(80.0, 30.0), AnchorKind::Corner), "{a:?}");
// A smooth anchor: the outgoing handle turns opposite the incoming one, keeping its length 30.
let (mut s, id) = draw(json!({"in": [20, 50], "out": [80, 50]}));
assert_eq!(path(&s, id).subpaths[0].anchors[0].kind, AnchorKind::Smooth);
gesture(&mut s, &[(50.0, 50.0), (50.0, 100.0)], Mods::default());
let a = first(&s, id);
assert_eq!((a.h_in, a.kind), (Point::new(50.0, 0.0), AnchorKind::Smooth), "{a:?}");
assert!(near(a.h_out, Point::new(50.0, 80.0)), "{a:?}");
// Alt pressed mid-drag: a corner, its outgoing handle where the drag had turned it.
let (mut s, id) = draw(json!({"in": [20, 50], "out": [80, 50]}));
let alt = Mods { alt: true, ..Mods::default() };
s.pointer(&PointerEvent::new(PointerKind::Down, 50.0, 50.0), view()).unwrap();
s.pointer(&PointerEvent::new(PointerKind::Drag, 50.0, 100.0), view()).unwrap();
s.pointer(&PointerEvent::new(PointerKind::Drag, 80.0, 100.0).with_mods(alt), view()).unwrap();
s.pointer(&PointerEvent::new(PointerKind::Up, 80.0, 100.0).with_mods(alt), view()).unwrap();
let a = first(&s, id);
assert_eq!((a.h_in, a.kind), (Point::new(20.0, 0.0), AnchorKind::Corner), "{a:?}");
assert!(near(a.h_out, Point::new(50.0, 80.0)), "{a:?}");
}
+16 -10
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), independent?: bool}",
"{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)}",
has_doc,
close
),
@@ -202,14 +202,18 @@ fn append_anchor(s: &mut Session, p: &Value) -> Result<Value> {
ok()
}
/// Close the active open subpath of `id`, optionally setting the incoming handle of the first anchor.
/// 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.
fn close(s: &mut Session, p: &Value) -> Result<Value> {
let id = id_param(p, "id").ok_or_else(|| bad("path.close", "missing id"))?;
let subpath = p.get("subpath").and_then(Value::as_u64).map(|u| u as usize);
let h_in = match p.get("in").filter(|h| !h.is_null()) {
None => None,
Some(h) => Some(finite_numbers(h).map(|[x, y]| Point::new(x, y)).ok_or_else(|| bad("path.close", "`in` must be [x, y]"))?),
let handle = |key: &str| match p.get(key).filter(|h| !h.is_null()) {
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 h_in = handle("in")?;
let h_out = handle("out")?;
let independent = switch_or(p, "independent", false, "path.close")?;
s.edit("Close Path", |d, sel| {
let path = path_mut(d, id)?;
@@ -218,11 +222,13 @@ fn close(s: &mut Session, p: &Value) -> Result<Value> {
None => path.subpaths.iter_mut().rev().find(|s| !s.closed).ok_or_else(|| EngineError::Other("path is already closed".into()))?,
};
sp.closed = true;
if let (Some(h), Some(first)) = (h_in, sp.anchors.first_mut()) {
first.h_in = h;
if !independent {
first.h_out = first.p - (h - first.p);
first.kind = AnchorKind::Smooth;
if let Some(first) = sp.anchors.first_mut() {
if let Some(h) = h_in {
first.set_handle(false, h, independent);
}
if let Some(h) = h_out {
first.h_out = h;
first.kind = AnchorKind::Corner;
}
}
let top = compound_or_self(d, id);