From 85af04a83a4d84791b32109d75cdb9925ff77921 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sun, 18 Oct 2026 21:19:13 +0200 Subject: [PATCH] Clear dangling handles when deleting anchors from a closed path Found while testing crates/engine/src/cmd/path.rs:632 (without_anchors, closed-path branch uses keep instead of open_run) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/path.rs | 3 +-- crates/engine/src/tests_draw2.rs | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/path.rs b/crates/engine/src/cmd/path.rs index 97b83838c..0f47265f4 100644 --- a/crates/engine/src/cmd/path.rs +++ b/crates/engine/src/cmd/path.rs @@ -602,7 +602,6 @@ fn without_anchors(sp: SubPath, removed: &BTreeSet) -> Vec { if gone.is_empty() { return vec![sp]; } - let keep = |anchors: Vec| (anchors.len() >= 2).then(|| SubPath::new(anchors, false)); let mut runs: Vec> = vec![]; let mut run = vec![]; for (i, anchor) in sp.anchors.into_iter().enumerate() { @@ -630,7 +629,7 @@ fn without_anchors(sp: SubPath, removed: &BTreeSet) -> Vec { } else if !run.is_empty() { runs.push(run); } - runs.into_iter().filter_map(keep).collect() + runs.into_iter().filter_map(open_run).collect() } /// Remove connections, splitting open/closed contours into their surviving open runs. diff --git a/crates/engine/src/tests_draw2.rs b/crates/engine/src/tests_draw2.rs index 88c9c27bf..ff970fc17 100644 --- a/crates/engine/src/tests_draw2.rs +++ b/crates/engine/src/tests_draw2.rs @@ -1968,6 +1968,26 @@ fn open_anchor_delete_preserves_curves_appearance_and_history() { assert_eq!(s.doc().unwrap().doc, after); } +#[test] +fn closed_anchor_delete_then_join_closes_with_a_line() { + let mut s = session(); + let data = "M100 100C120 60 180 60 200 100C220 140 280 140 300 100C320 60 380 60 400 100C420 140 480 140 500 100C500 200 100 200 100 100Z"; + let id = NodeId(s.execute("path.create", &json!({"d": data})).unwrap()["id"].as_u64().unwrap()); + s.execute("select.anchors", &json!({"id": id.0, "anchors": [[0, 2]]})).unwrap(); + s.execute("path.deleteAnchors", &json!({})).unwrap(); + let result = path(&s, id); + assert_eq!(result.subpaths.len(), 1); + let sp = &result.subpaths[0]; + assert!(!sp.closed); + assert!(!sp.anchors.first().unwrap().has_in() && !sp.anchors.last().unwrap().has_out(), "dangling handles are cleared"); + s.execute("path.join", &json!({"ids": [id.0]})).unwrap(); + let joined = path(&s, id); + let sp = &joined.subpaths[0]; + assert!(sp.closed); + let closing = sp.segment(sp.segment_count() - 1); + assert!(closing.p1 == closing.p0 && closing.p2 == closing.p3, "closing segment is a line: {closing:?}"); +} + #[test] fn open_anchor_delete_does_not_change_explicit_remove_anchors() { for command in ["path.removeAnchors", "path.removeAnchor"] {