diff --git a/crates/engine/src/cmd/measure_shape.rs b/crates/engine/src/cmd/measure_shape.rs index 507d57a3..5c7f4ad9 100644 --- a/crates/engine/src/cmd/measure_shape.rs +++ b/crates/engine/src/cmd/measure_shape.rs @@ -170,14 +170,20 @@ fn from_round(a: &Shape, b: &Shape) -> Option { let Shape::Round { center: ca, axis: na, radius: ra, planar: pa, lo: loa, hi: hia, .. } = *a else { return None }; let full_a = a.full(); let c = match *b { - Shape::Round { center: cb, axis: nb, radius: rb, lo: lob, hi: hib, .. } => { + Shape::Round { center: cb, axis: nb, radius: rb, planar: pb, lo: lob, hi: hib, .. } => { + // A picked circle/arc has a fixed centre; only a cylindrical face has an + // infinite axis. Do not project a rim's centre onto an imaginary extension. + let (from, to) = match (pa, pb) { + (true, true) => (ca, cb), + (true, false) => (ca, cb + nb * (ca - cb).dot(nb)), + (false, true) => (ca + na * (cb - ca).dot(na), cb), + (false, false) => line_line(ca, na, cb, nb), + }; + let (mut min, mut max) = (None, None); if parallel(na, nb) { let w = cb - ca; let along = w.dot(na); - // Drawn level with A's centre: on B's axis, the foot of A's centre. - let foot = cb - na * along; let d = (w - na * along).len(); - let (mut min, mut max) = (None, None); if full_a && b.full() { // B's extent along A's axis. let s = if na.dot(nb) >= 0.0 { 1.0 } else { -1.0 }; @@ -189,11 +195,8 @@ fn from_round(a: &Shape, b: &Shape) -> Option { min = Some(near.hypot(gap)); max = Some(far.hypot(reach)); } - Centre { distance: d, from: ca, to: foot, min, max } - } else { - let (p, q) = line_line(ca, na, cb, nb); - Centre { distance: p.dist(q), from: p, to: q, min: None, max: None } } + Centre { distance: from.dist(to), from, to, min, max } } Shape::Plane { point, normal } => { let n = normal.normalized()?; @@ -339,6 +342,50 @@ mod tests { assert!((c.max.unwrap() - 55.0).abs() < 1e-12); } + #[test] + fn circles_at_different_heights_keep_both_actual_centres() { + let (a, b) = (Vec3::new(0.0, 0.0, 10.0), Vec3::new(34.0, 19.0, 32.0)); + let c = centre(&circle(a, 4.0), &circle(b, 4.0)).unwrap(); + assert_eq!((c.from, c.to), (a, b)); + assert!((c.distance - 2001_f64.sqrt()).abs() < 1e-12); + assert!((c.min.unwrap() - (1517_f64.sqrt() - 8.0).hypot(22.0)).abs() < 1e-12); + assert!((c.max.unwrap() - (1517_f64.sqrt() + 8.0).hypot(22.0)).abs() < 1e-12); + let swapped = centre(&circle(b, 4.0), &circle(a, 4.0)).unwrap(); + assert_eq!((swapped.from, swapped.to, swapped.distance), (b, a, c.distance)); + } + + #[test] + fn arcs_on_different_planes_measure_between_their_centres() { + let a = Shape::Round { center: Vec3::ZERO, axis: Vec3::Z, radius: 2.0, angle: PI, planar: true, lo: 0.0, hi: 0.0 }; + let b = Shape::Round { center: Vec3::new(3.0, 4.0, 12.0), axis: Vec3::X, radius: 4.0, angle: PI / 2.0, planar: true, lo: 0.0, hi: 0.0 }; + let c = centre(&a, &b).unwrap(); + assert_eq!((c.from, c.to, c.distance), (Vec3::ZERO, Vec3::new(3.0, 4.0, 12.0), 13.0)); + assert!(c.min.is_none() && c.max.is_none()); + } + + #[test] + fn a_circle_to_a_cylinder_uses_its_fixed_centre_and_the_cylinders_axis() { + let a = circle(Vec3::new(0.0, 0.0, 10.0), 4.0); + for axis in [Vec3::Z, Vec3::X] { + let b = Shape::Round { center: Vec3::new(34.0, 19.0, 27.0), axis, radius: 4.0, angle: TAU, planar: false, lo: -5.0, hi: 5.0 }; + let c = centre(&a, &b).unwrap(); + let expected = if axis == Vec3::Z { Vec3::new(34.0, 19.0, 10.0) } else { Vec3::new(0.0, 19.0, 27.0) }; + assert_eq!((c.from, c.to), (Vec3::new(0.0, 0.0, 10.0), expected)); + assert!((c.distance - c.from.dist(expected)).abs() < 1e-12); + let swapped = centre(&b, &a).unwrap(); + assert_eq!((swapped.from, swapped.to, swapped.distance), (c.to, c.from, c.distance)); + } + } + + #[test] + fn two_cylindrical_faces_still_measure_axis_pitch() { + let a = Shape::Round { center: Vec3::new(0.0, 0.0, 5.0), axis: Vec3::Z, radius: 4.0, angle: TAU, planar: false, lo: -5.0, hi: 5.0 }; + let b = Shape::Round { center: Vec3::new(34.0, 19.0, 27.0), axis: Vec3::Z, radius: 4.0, angle: TAU, planar: false, lo: -5.0, hi: 5.0 }; + let c = centre(&a, &b).unwrap(); + assert_eq!((c.from, c.to), (Vec3::new(0.0, 0.0, 5.0), Vec3::new(34.0, 19.0, 5.0))); + assert!((c.distance - 1517_f64.sqrt()).abs() < 1e-12); + } + #[test] fn a_cylinder_to_a_side_plane_measures_from_its_axis() { let cyl = Shape::Round { center: Vec3::new(10.0, 15.0, 5.0), axis: Vec3::Z, radius: 2.5, angle: TAU, planar: false, lo: -5.0, hi: 5.0 }; diff --git a/crates/engine/src/measure_centre_tests.rs b/crates/engine/src/measure_centre_tests.rs index 74791889..3526597c 100644 --- a/crates/engine/src/measure_centre_tests.rs +++ b/crates/engine/src/measure_centre_tests.rs @@ -104,6 +104,39 @@ fn two_holes_measure_centre_to_centre_with_min_and_max() { assert_eq!(r["items"][0]["round"], "circle", "{r}"); } +#[test] +fn elevated_cylinder_rims_measure_between_the_picked_circles_centres() { + let mut s = Session::default(); + run(&mut s, "solid.cylinder", json!({"radius":4,"height":10,"body_name":"A"})); + run(&mut s, "solid.cylinder", json!({"base":[34,19,22],"radius":4,"height":10,"body_name":"B","operation":"new_body"})); + let items: Vec<_> = s + .model + .state() + .bodies + .iter() + .map(|b| { + let z = if b.name == "A" { 10.0 } else { 32.0 }; + let m = b.mesh(); + let index = m.edges.iter().position(|e| e.len() > 2 && e.iter().all(|p| (p.z - z).abs() < 1e-9)).unwrap(); + let e = &m.edges[index]; + json!({"type":"edge","body":b.name,"index":index,"point":e[e.len()/2]}) + }) + .collect(); + assert_eq!(items.len(), 2); + for picks in [items.clone(), items.into_iter().rev().collect()] { + let r = measure(&mut s, picks); + assert!(close(&r["center_distance"], 2001_f64.sqrt(), 1e-9), "{r}"); + for (endpoint, item) in [("center_from", 0), ("center_to", 1)] { + for axis in 0..3 { + assert!(close(&r[endpoint][axis], r["items"][item]["center"][axis].as_f64().unwrap(), 1e-9), "{r}"); + } + } + assert!((r["center_delta"][2].as_f64().unwrap().abs() - 22.0).abs() < 1e-9, "{r}"); + assert!(close(&r["min"], (1517_f64.sqrt() - 8.0).hypot(22.0), 1e-9), "{r}"); + assert!(close(&r["max"], (1517_f64.sqrt() + 8.0).hypot(22.0), 1e-9), "{r}"); + } +} + #[test] fn a_hole_to_the_plate_edge_and_side_measures_from_its_axis() { let mut s = Session::default();