From abb15e4b0e89da7f769d15e889278e12d1bc7548 Mon Sep 17 00:00:00 2001 From: cronos Date: Fri, 16 Oct 2026 10:27:29 +0900 Subject: [PATCH] Preserve circle and arc centre heights when measuring straight edges (#1546) Thanks @crono2250 for the fix. Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/measure_shape.rs | 61 +++++++++++++++++++++-- crates/engine/src/measure_centre_tests.rs | 29 +++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/cmd/measure_shape.rs b/crates/engine/src/cmd/measure_shape.rs index 5c7f4ad9..00f958c8 100644 --- a/crates/engine/src/cmd/measure_shape.rs +++ b/crates/engine/src/cmd/measure_shape.rs @@ -215,11 +215,22 @@ fn from_round(a: &Shape, b: &Shape) -> Option { } Shape::Line { a: la, b: lb } => { let dir = (lb - la).normalized()?; - let (p, q) = line_line(ca, na, la, dir); - let d = p.dist(q); let across = dir.dot(na).abs() < 1e-6; - let min = (full_a && across && d >= ra).then_some(d - ra); - Centre { distance: d, from: p, to: q, min, max: None } + if pa { + // A circle/arc has a fixed centre. Project that point onto the picked + // finite edge, retaining the height difference and clamping to its ends. + let to = la + dir * (ca - la).dot(dir).clamp(0.0, (lb - la).len()); + let offset = to - ca; + let height = offset.dot(na); + let radial = (offset - na * height).len(); + let min = (full_a && across && radial >= ra).then(|| (radial - ra).hypot(height)); + Centre { distance: ca.dist(to), from: ca, to, min, max: None } + } else { + let (p, q) = line_line(ca, na, la, dir); + let d = p.dist(q); + let min = (full_a && across && d >= ra).then_some(d - ra); + Centre { distance: d, from: p, to: q, min, max: None } + } } Shape::Point(p) => { if pa { @@ -410,6 +421,48 @@ mod tests { assert!(c.from.dist(Vec3::new(10.0, 15.0, 10.0)) < 1e-12 && c.to.dist(Vec3::new(10.0, 0.0, 10.0)) < 1e-12); } + #[test] + fn a_circle_or_arc_to_a_line_keeps_its_real_center() { + let edge = Shape::Line { a: Vec3::new(0.0, 0.0, 6.0), b: Vec3::new(68.0, 0.0, 6.0) }; + for angle in [TAU, PI / 2.0] { + let round = Shape::Round { center: Vec3::new(34.0, 25.0, 20.0), axis: Vec3::Z, radius: 12.0, angle, planar: true, lo: 0.0, hi: 0.0 }; + let c = centre(&round, &edge).unwrap(); + assert_eq!((c.from, c.to), (Vec3::new(34.0, 25.0, 20.0), Vec3::new(34.0, 0.0, 6.0))); + assert!((c.distance - 25_f64.hypot(14.0)).abs() < 1e-12); + if angle == TAU { + assert!((c.min.unwrap() - 13_f64.hypot(14.0)).abs() < 1e-12); + } else { + assert!(c.min.is_none()); + } + let swapped = centre(&edge, &round).unwrap(); + assert_eq!((swapped.from, swapped.to, swapped.distance), (c.to, c.from, c.distance)); + } + } + + #[test] + fn a_tilted_arc_to_a_sloped_line_keeps_all_components() { + let edge = Shape::Line { a: Vec3::ZERO, b: Vec3::new(6.0, 0.0, 6.0) }; + for axis in [Vec3::X, Vec3::Y, Vec3::Z] { + let arc = Shape::Round { center: Vec3::new(3.0, 4.0, 6.0), axis, radius: 2.0, angle: PI, planar: true, lo: 0.0, hi: 0.0 }; + let c = centre(&arc, &edge).unwrap(); + assert!(c.from.dist(Vec3::new(3.0, 4.0, 6.0)) < 1e-12); + assert!(c.to.dist(Vec3::new(4.5, 0.0, 4.5)) < 1e-12); + assert!((c.distance - 20.5_f64.sqrt()).abs() < 1e-12); + } + } + + #[test] + fn a_circle_center_to_a_finite_edge_does_not_use_its_extension() { + for (center, expected) in [(Vec3::new(20.0, 3.0, 4.0), Vec3::new(10.0, 0.0, 0.0)), (Vec3::new(-10.0, 3.0, 4.0), Vec3::ZERO)] { + for (a, b) in [(Vec3::ZERO, Vec3::new(10.0, 0.0, 0.0)), (Vec3::new(10.0, 0.0, 0.0), Vec3::ZERO)] { + let c = centre(&circle(center, 2.0), &Shape::Line { a, b }).unwrap(); + assert_eq!((c.from, c.to), (center, expected)); + assert!((c.distance - 125_f64.sqrt()).abs() < 1e-12); + } + } + assert!(centre(&circle(Vec3::ZERO, 2.0), &Shape::Line { a: Vec3::ZERO, b: Vec3::ZERO }).is_none()); + } + #[test] fn coverage_of_a_half_and_a_whole_ring() { let ring = |n: usize, span: f64| -> Vec { diff --git a/crates/engine/src/measure_centre_tests.rs b/crates/engine/src/measure_centre_tests.rs index 3526597c..1fbae18d 100644 --- a/crates/engine/src/measure_centre_tests.rs +++ b/crates/engine/src/measure_centre_tests.rs @@ -137,6 +137,35 @@ fn elevated_cylinder_rims_measure_between_the_picked_circles_centres() { } } +#[test] +fn an_elevated_circle_to_a_straight_edge_keeps_its_height_difference() { + let mut s = Session::default(); + run(&mut s, "solid.box", json!({"length":68,"width":50,"height":6,"body_name":"Plate"})); + run(&mut s, "solid.cylinder", json!({"base":[34,25,6],"radius":12,"height":14,"body_name":"Boss","operation":"new_body"})); + let straight = edge(&s, |p| p.y.abs() < 1e-9 && (p.z - 6.0).abs() < 1e-9); + let round = { + let st = s.model.state(); + let b = st.bodies.iter().find(|b| b.name == "Boss").unwrap(); + let m = b.mesh(); + let index = m.edges.iter().position(|e| e.len() > 2 && e.iter().all(|p| (p.z - 20.0).abs() < 1e-9)).unwrap(); + let e = &m.edges[index]; + json!({"type":"edge","body":b.name,"index":index,"point":e[e.len()/2]}) + }; + for items in [vec![straight.clone(), round.clone()], vec![round, straight]] { + let r = measure(&mut s, items); + assert!(close(&r["center_distance"], 25_f64.hypot(14.0), 1e-9), "{r}"); + let (line, circle) = if r["items"][0]["round"] == "circle" { ("center_to", "center_from") } else { ("center_from", "center_to") }; + for (key, expected) in [(line, [34.0, 0.0, 6.0]), (circle, [34.0, 25.0, 20.0])] { + for (axis, value) in expected.into_iter().enumerate() { + assert!(close(&r[key][axis], value, 1e-9), "{r}"); + } + } + assert!((r["center_delta"][2].as_f64().unwrap().abs() - 14.0).abs() < 1e-9, "{r}"); + assert!(close(&r["min"], 13_f64.hypot(14.0), 1e-9), "{r}"); + assert!(close(&r["max"], (34_f64.hypot(25.0) + 12.0).hypot(14.0), 0.05), "{r}"); + } +} + #[test] fn a_hole_to_the_plate_edge_and_side_measures_from_its_axis() { let mut s = Session::default();