diff --git a/crates/engine/src/cmd/sketch_create.rs b/crates/engine/src/cmd/sketch_create.rs index f82396cb..c4eebc0d 100644 --- a/crates/engine/src/cmd/sketch_create.rs +++ b/crates/engine/src/cmd/sketch_create.rs @@ -659,15 +659,26 @@ fn arc_tangent(s: &mut Session, p: &Value) -> Result { let end = req_vec2(cmd, p, "end")?; let (out, info) = edit(s, p, cmd, false, |sk, _| { let st = req_parg(sk, p, "start", cmd)?; - let pi = st.idx().ok_or_else(|| bad(cmd, "`start` must be the end point of a line or arc"))?; - let ci = match str_(p, "curve") { - Some(id) => sk.curve_index(id).ok_or_else(|| bad(cmd, format!("unknown curve `{id}`")))?, - None => sk - .curves + let picked = st.idx().ok_or_else(|| bad(cmd, "`start` must be the end point of a line or arc"))?; + // The picked point, then any other point at the same place: a trimmed arc's end is its own + // point, lying on the end of the curve that trimmed it, which is the one a click picks (#1485). + let at = st.pos(); + let tol = 1e-6 * at.len().max(1.0); + let candidates: Vec = + std::iter::once(picked).chain((0..sk.points.len()).filter(|&q| q != picked && sk.point(q).is_some_and(|x| x.dist(at) <= tol))).collect(); + let (ci, pi) = match str_(p, "curve") { + Some(id) => { + let ci = sk.curve_index(id).ok_or_else(|| bad(cmd, format!("unknown curve `{id}`")))?; + let pi = candidates + .iter() + .copied() + .find(|&q| leaving_dir(sk, ci, q).is_some()) + .ok_or_else(|| bad(cmd, "`start` is not an end of that curve"))?; + (ci, pi) + } + None => candidates .iter() - .enumerate() - .find(|(i, c)| !c.construction && leaving_dir(sk, *i, pi).is_some()) - .map(|(i, _)| i) + .find_map(|&q| sk.curves.iter().enumerate().find(|(i, c)| !c.construction && leaving_dir(sk, *i, q).is_some()).map(|(i, _)| (i, q))) .ok_or_else(|| bad(cmd, "no line or arc ends at `start`"))?, }; let t = leaving_dir(sk, ci, pi).ok_or_else(|| bad(cmd, "`start` is not an end of that curve"))?; diff --git a/crates/engine/src/cmd/sketch_modify_tests.rs b/crates/engine/src/cmd/sketch_modify_tests.rs index ac552669..805c8a19 100644 --- a/crates/engine/src/cmd/sketch_modify_tests.rs +++ b/crates/engine/src/cmd/sketch_modify_tests.rs @@ -310,6 +310,33 @@ fn offset_chain_mirror_patterns_move_scale() { assert!(sk.center(sk.curve_index(&cp).unwrap()).unwrap().dist(Vec2::new(-110.0, 0.0)) < 1e-7); } +/// #1485: after a circle is trimmed between two construction lines, clicking the trimmed end +/// picks the construction line's end lying there, not the arc's own; the tangent arc still starts +/// from the trimmed arc, tangent to it. +#[test] +fn a_tangent_arc_starts_from_a_trimmed_end() { + let mut s = new_sketch(); + let c = ids(&run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": 10}))["curves"])[0].clone(); + run(&mut s, "sketch.line", json!({"points": [[0, 0], [10, 0]], "ids": ["guide_a"]})); + run(&mut s, "sketch.line", json!({"points": [[0, 0], [0, 10]], "ids": ["guide_b"]})); + run(&mut s, "sketch.construction", json!({"curves": ["guide_a", "guide_b"]})); + run(&mut s, "sketch.trim", json!({"curve": c, "at": [7.07, 7.07]})); + let sk = sketch(&s); + let picked = sk.resolve_point("guide_a.end").unwrap(); + assert!(sk.point(picked).unwrap().dist(Vec2::new(10.0, 0.0)) < 1e-6); + let trimmed = sk.curves.iter().position(|cu| !cu.construction && matches!(cu.kind, CurveKind::Arc { .. })).expect("the trimmed arc"); + let r = run(&mut s, "sketch.arc.tangent", json!({"start": "guide_a.end", "end": [14, 6]})); + let sk = sketch(&s); + let a = sk.curve_index(&ids(&r["curves"])[0]).unwrap(); + let a_ends: Vec = sk.curves[a].kind.point_ids(); + let t_ends: Vec = sk.curves[trimmed].kind.point_ids(); + assert!(a_ends.iter().any(|q| t_ends.contains(q) && *q != picked), "the new arc shares the trimmed arc's own end"); + assert!( + sk.constraints.iter().any(|k| matches!(k.kind, ConstraintKind::Tangent { a: x, b: y } if (x, y) == (trimmed, a) || (x, y) == (a, trimmed))), + "and is tangent to it" + ); +} + #[test] fn new_creation_tools() { let mut s = new_sketch();