diff --git a/crates/engine/src/cmd/sketch_modify.rs b/crates/engine/src/cmd/sketch_modify.rs index 2b56d1a5..b0e06c49 100644 --- a/crates/engine/src/cmd/sketch_modify.rs +++ b/crates/engine/src/cmd/sketch_modify.rs @@ -479,6 +479,55 @@ fn uses(c: &Curve, p: usize) -> bool { // --------------------------------------------------------------------------------------------- // Corners: fillet and chamfer +/// A corner drawn as two curves whose ends are separate points held together by a Coincident +/// constraint (a line drawn to where an arc starts, #1208) becomes one shared point, so it can be +/// rounded or cut like a corner drawn through one point. Only a partner held by nothing but that +/// constraint is welded; anything else is left as it is. +fn weld_corner(sk: &mut Sketch, p: &Value) { + let ends = |sk: &Sketch, c: usize| match sk.curves.get(c).map(|c| &c.kind) { + Some(CurveKind::Line { a, b }) | Some(CurveKind::Arc { a, b, .. }) => Some([*a, *b]), + _ => None, + }; + let q = if let Some(r) = str_(p, "point") { + sk.resolve_point(r) + } else { + let curve = |k: &str| str_(p, k).and_then(|i| sk.curve_index(i)).and_then(|c| ends(sk, c)); + let (Some(ea), Some(eb)) = (curve("a"), curve("b")) else { return }; + if ea.iter().any(|x| eb.contains(x)) { + return; + } + // The end of `a` held coincident with an end of `b`. + ea.into_iter().find(|x| { + sk.constraints + .iter() + .any(|k| matches!(k.kind, ConstraintKind::Coincident { p, q } if (p == *x && eb.contains(&q)) || (q == *x && eb.contains(&p)))) + }) + }; + let Some(mut q) = q else { return }; + // Each pass welds one partner; a pass removes a constraint, so this ends. + for _ in 0..sk.constraints.len() { + let partner = sk.constraints.iter().enumerate().find_map(|(i, k)| { + let o = match k.kind { + ConstraintKind::Coincident { p, q: r } if p == q && r != q => r, + ConstraintKind::Coincident { p, q: r } if r == q && p != q => p, + _ => return None, + }; + let held_elsewhere = sk.constraints.iter().enumerate().any(|(j, k)| j != i && k.kind.refers_to_point(o)); + let linked = (0..sk.curves.len()).any(|c| sk.is_linked_curve(c) && sk.curves.get(c).is_some_and(|x| x.kind.uses(o))); + (o != 0 && !held_elsewhere && !linked).then_some((i, o)) + }); + let Some((i, o)) = partner else { return }; + for c in sk.curves.iter_mut() { + c.kind.map_points(&|x| if x == o { q } else { x }); + } + sk.constraints.remove(i); + sk.remove_points(&[o]); + if o < q { + q -= 1; + } + } +} + /// A corner of two lines: (line a, line b, corner point, far end of a, far end of b). fn corner(sk: &Sketch, p: &Value, cmd: &str) -> Result<(usize, usize, usize, usize, usize)> { let ends = |l: usize| match sk.curves.get(l).map(|c| &c.kind) { @@ -653,6 +702,7 @@ fn kept_json(kept: Vec) -> (Vec, Vec) { /// Round one corner (`point` or `a`, `b`) with an arc of radius `r` tangent to both curves: /// the arc, the tangent constraints and what became of the corner point. fn fillet_corner(sk: &mut Sketch, p: &Value, r: f64, cmd: &str) -> Result<(usize, Vec, CornerKept)> { + weld_corner(sk, p); let (la, lb, c, fa, fb) = match corner(sk, p, cmd) { Ok(x) => x, // Not two lines: a corner with an arc. @@ -761,6 +811,7 @@ fn round_corner(sk: &mut Sketch, p: &Value, r: f64, cmd: &str) -> Result Result<(f64, f64)>) -> Result { let (out, info) = edit(s, p, cmd, false, |sk, _| { + weld_corner(sk, p); let (la, lb, c, fa, fb) = corner(sk, p, cmd)?; let (pc, pa, pb) = (sk.point(c).unwrap_or_default(), sk.point(fa).unwrap_or_default(), sk.point(fb).unwrap_or_default()); let (u, v) = ((pa - pc).normalized().unwrap_or(Vec2::X), (pb - pc).normalized().unwrap_or(Vec2::Y)); diff --git a/crates/engine/src/cmd/sketch_modify_tests.rs b/crates/engine/src/cmd/sketch_modify_tests.rs index 75b70e08..ac552669 100644 --- a/crates/engine/src/cmd/sketch_modify_tests.rs +++ b/crates/engine/src/cmd/sketch_modify_tests.rs @@ -726,6 +726,51 @@ fn fillet_a_line_and_an_arc() { assert!((d - 12.0).abs() < 1e-6, "{d} {c:?}"); } +#[test] +fn fillet_a_line_and_an_arc_drawn_to_meet_at_a_point() { + // #1208: the arc starts at the line's end by coordinates, so the corner is two points held + // together by a Coincident constraint. By point and by naming the two curves. + for how in [json!({"point": "l1.end"}), json!({"a": "l1", "b": "a1"})] { + let mut s = new_sketch(); + run(&mut s, "sketch.line", json!({"points": [[0, 0], [40, 0]], "ids": ["l1"]})); + run(&mut s, "sketch.arc.center_point", json!({"center": [50, 0], "start": [60, 0], "end": [40, 0], "id": "a1"})); + let mut p = how.clone(); + p["radius"] = json!(3); + let f = run(&mut s, "sketch.fillet", p); + let sk = sketch(&s); + let ai = sk.curve_index(&ids(&f["curves"])[0]).unwrap(); + assert!((sk.radius(ai).unwrap() - 3.0).abs() < 1e-7, "{how}"); + // Tangent to the line (centre 3 above it) and outside the R10 arc (13 from its centre). + let c = sk.center(ai).unwrap(); + let want = Vec2::new(50.0 - 160f64.sqrt(), 3.0); + assert!(c.dist(want) < 1e-6, "{how}: {c:?} vs {want:?}"); + // The line now ends where the fillet touches it, and the arc where the fillet touches it. + let l1 = sk.curve_index("l1").unwrap(); + let CurveKind::Line { b, .. } = sk.curves[l1].kind else { panic!("l1 is a line") }; + assert!(sk.point(b).unwrap().dist(Vec2::new(want.x, 0.0)) < 1e-6, "{how}"); + let a1 = sk.curve_index("a1").unwrap(); + let on_arc = Vec2::new(50.0, 0.0) + (want - Vec2::new(50.0, 0.0)) * (10.0 / 13.0); + assert!(sk.curves[a1].kind.point_ids().iter().any(|i| sk.point(*i).unwrap().dist(on_arc) < 1e-6), "{how}"); + // No coincident pair is left at the old corner (only the line's start on the origin). + let coincident = sk.constraints.iter().filter(|k| matches!(k.kind, ConstraintKind::Coincident { .. })).count(); + assert_eq!(coincident, 1, "{how}"); + } +} + +#[test] +fn chamfer_two_lines_drawn_to_meet_at_a_point() { + let mut s = new_sketch(); + run(&mut s, "sketch.line", json!({"points": [[0, 0], [40, 0]], "ids": ["l1"]})); + run(&mut s, "sketch.line", json!({"points": [[40, 0], [40, 30]], "ids": ["l2"]})); + let c = run(&mut s, "sketch.chamfer.equal_distance", json!({"point": "l1.end", "distance": 5})); + let sk = sketch(&s); + let ci = sk.curve_index(&ids(&c["curves"])[0]).unwrap(); + let CurveKind::Line { a, b } = sk.curves[ci].kind else { panic!("the chamfer is a line") }; + let mut ends = [sk.point(a).unwrap(), sk.point(b).unwrap()]; + ends.sort_by(|p, q| p.x.total_cmp(&q.x)); + assert!(ends[0].dist(Vec2::new(35.0, 0.0)) < 1e-6 && ends[1].dist(Vec2::new(40.0, 5.0)) < 1e-6, "{ends:?}"); +} + #[test] fn offset_free_form_curves() { let mut s = new_sketch();