From b089b7e948f96ef7ad1526bf766e97fa064cb6f2 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sun, 25 Oct 2026 22:28:57 +0000 Subject: [PATCH] Join the selected end points of two paths with Ctrl+J Fixes #4163 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/path.rs | 23 +++++++++++++++++++++-- crates/engine/src/tests_draw2.rs | 14 ++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/path.rs b/crates/engine/src/cmd/path.rs index f55378a34..4de797583 100644 --- a/crates/engine/src/cmd/path.rs +++ b/crates/engine/src/cmd/path.rs @@ -467,6 +467,9 @@ fn join(s: &mut Session, p: &Value) -> Result { if a == b { return Err(bad(C, "join two different paths")); } + let st = s.doc()?; + let picked_a = st.selection.anchors.get(&a).cloned().unwrap_or_default(); + let picked_b = st.selection.anchors.get(&b).cloned().unwrap_or_default(); s.edit("Join", |d, sel| { let pb = d.node(b).and_then(|n| n.path_data()).cloned().ok_or(EngineError::NoNode(b))?; let pa = path_mut(d, a)?; @@ -481,8 +484,24 @@ fn join(s: &mut Session, p: &Value) -> Result { // The ends asked for, else the closest pair. let cands = [(xl.distance(yf), false, false), (xl.distance(yl), false, true), (xf.distance(yf), true, false), (xf.distance(yl), true, true)]; - let (rx, ry) = - ends.unwrap_or_else(|| cands.into_iter().min_by(|p, q| p.0.total_cmp(&q.0)).map_or((false, false), |(_, rx, ry)| (rx, ry))); + // Direct-selected end points decide which ends meet, before the closest pair does. + let end_of = |picked: &std::collections::BTreeSet<(usize, usize)>, si: usize, len: usize| { + let last_ix = len.checked_sub(1)?; + let first = picked.contains(&(si, 0)); + let last = picked.contains(&(si, last_ix)); + match (first, last) { + (true, false) => Some(true), + (false, true) => Some(false), + _ => None, + } + }; + let by_selection = match (end_of(&picked_a, sa, x.anchors.len()), end_of(&picked_b, sb, y.anchors.len())) { + (Some(fa), Some(fb)) => Some((fa, !fb)), + _ => None, + }; + let (rx, ry) = ends + .or(by_selection) + .unwrap_or_else(|| cands.into_iter().min_by(|p, q| p.0.total_cmp(&q.0)).map_or((false, false), |(_, rx, ry)| (rx, ry))); if rx { x.reverse(); } diff --git a/crates/engine/src/tests_draw2.rs b/crates/engine/src/tests_draw2.rs index c328b3f15..f31368e96 100644 --- a/crates/engine/src/tests_draw2.rs +++ b/crates/engine/src/tests_draw2.rs @@ -2188,6 +2188,20 @@ fn join_takes_the_paths_and_the_ends_to_join() { } } +/// Without `ends`, the end points picked with the direct selection are the ones joined. +#[test] +fn join_uses_the_selected_end_points() { + let mut s = session(); + let a = line(&mut s, 0.0, 0.0, 100.0, 0.0); + let b = line(&mut s, 110.0, 0.0, 200.0, 0.0); + s.execute("select.all", &json!({})).unwrap(); + s.execute("select.anchors", &json!({"id": a.0, "anchors": [[0, 0]], "mode": "add"})).unwrap(); + s.execute("select.anchors", &json!({"id": b.0, "anchors": [[0, 1]], "mode": "add"})).unwrap(); + s.execute("path.join", &json!({})).unwrap(); + let pts: Vec<(f64, f64)> = path(&s, a).subpaths[0].anchors.iter().map(|x| (x.p.x, x.p.y)).collect(); + assert_eq!(pts, [(100.0, 0.0), (0.0, 0.0), (200.0, 0.0), (110.0, 0.0)]); +} + /// `path.join {to}` makes the joined anchor a corner or smooth point. #[test] fn join_to_corner_or_smooth_sets_the_merged_anchor() {