From fbaba5fecde3178529b8fb7748dccafcec8c248a Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:14:45 -0400 Subject: [PATCH] Geometry: a face's area centroid and a point a fraction along an edge Shape::face_centre and Edge::point_at place datum points on a design's named faces and edges. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo --- crates/geometry/src/edges.rs | 31 ++++++++++++++++++++++++++++ crates/geometry/src/lib.rs | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/crates/geometry/src/edges.rs b/crates/geometry/src/edges.rs index b68cc2f..9895cc3 100644 --- a/crates/geometry/src/edges.rs +++ b/crates/geometry/src/edges.rs @@ -33,6 +33,24 @@ impl Edge { pub fn length(&self) -> f64 { self.points.windows(2).map(|w| distance(w[0], w[1])).sum() } + + /// The point `fraction` of its length along it from its start (0: the start, 1: the end); + /// `None` for a fraction outside 0..=1 or an edge without points. + pub fn point_at(&self, fraction: f64) -> Option<[f64; 3]> { + if !(0.0..=1.0).contains(&fraction) { + return None; + } + let mut left = fraction * self.length(); + for w in self.points.windows(2) { + let l = distance(w[0], w[1]); + if left <= l && l > 0.0 { + let t = left / l; + return Some([0, 1, 2].map(|k| w[0][k] + t * (w[1][k] - w[0][k]))); + } + left -= l; + } + self.points.last().copied() + } } fn distance(a: [f64; 3], b: [f64; 3]) -> f64 { @@ -186,6 +204,19 @@ mod tests { assert_eq!(n, names(&ea)); } + #[test] + fn points_along_an_edge() { + let e = super::Edge { name: "e".into(), faces: vec![], points: vec![[0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [2.0, 2.0, 0.0]] }; + assert_eq!(e.point_at(0.0), Some([0.0, 0.0, 0.0])); + assert_eq!(e.point_at(0.5), Some([2.0, 0.0, 0.0])); + assert_eq!(e.point_at(0.75), Some([2.0, 1.0, 0.0])); + assert_eq!(e.point_at(1.0), Some([2.0, 2.0, 0.0])); + for bad in [-0.1, 1.5, f64::NAN] { + assert_eq!(e.point_at(bad), None); + } + assert_eq!(super::Edge { points: vec![], ..e }.point_at(0.5), None); + } + #[test] fn partition_cut_edges_are_named_after_the_cut_faces() { let d = open_design(&sample::plate_design(&Plate::default()).unwrap()).unwrap(); diff --git a/crates/geometry/src/lib.rs b/crates/geometry/src/lib.rs index 5f80d5f..7d4d3ea 100644 --- a/crates/geometry/src/lib.rs +++ b/crates/geometry/src/lib.rs @@ -160,6 +160,30 @@ impl Shape { self.faces.iter().position(|f| f.name == name).and_then(|i| u32::try_from(i).ok()) } + /// The area centroid of the face with this persistent name, from a tessellation fine enough + /// to place it (on a curved face the centroid lies off the surface, inside its curvature). + pub fn face_centre(&self, name: &str) -> Result<[f64; 3]> { + let face = self.face_index(name).ok_or_else(|| Error::Invalid(format!("body `{}` has no face `{name}`", self.name)))?; + let mesh = self.tessellate((self.size()? * edges::EDGE_TOLERANCE).max(1e-4))?; + let (mut sum, mut area) = ([0.0; 3], 0.0); + for (t, _) in mesh.triangles.iter().zip(&mesh.tri_face).filter(|(_, f)| **f == face) { + let corner = |i: u32| mesh.positions.get(i as usize).copied(); + let (Some(a), Some(b), Some(c)) = (corner(t[0]), corner(t[1]), corner(t[2])) else { continue }; + let (u, v) = ([0, 1, 2].map(|k| b[k] - a[k]), [0, 1, 2].map(|k| c[k] - a[k])); + let n = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]]; + let w = 0.5 * (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt(); + for (k, s) in sum.iter_mut().enumerate() { + *s += w * (a[k] + b[k] + c[k]) / 3.0; + } + area += w; + } + if area > 0.0 && area.is_finite() { + Ok(sum.map(|s| s / area)) + } else { + Err(Error::Failed(format!("face `{name}` of body `{}` has no area", self.name))) + } + } + /// The analytic surface of each B-rep face, by face index (the [`SurfaceMesh::tri_face`] /// tags): `None` for a free-form face. Meshers project nodes onto these surfaces. pub fn face_surfaces(&self) -> Result>> { @@ -241,6 +265,22 @@ impl SurfaceMesh { mod tests { use super::*; + /// The plate's side face at x = 0 (40 × 30 × 10, the hole well inside) has its centre in its + /// middle; an unknown face is an error. + #[test] + fn a_face_centre_is_its_area_centroid() { + let d = open_design(&sample::plate_design(&sample::Plate::default()).unwrap()).unwrap(); + let body = &d.bodies[0]; + let surfaces = body.face_surfaces().unwrap(); + let side = surfaces + .iter() + .position(|s| matches!(s, Some(FaceSurface::Plane { normal, point }) if normal[0] < -0.5 && point[0].abs() < 1e-9)) + .unwrap(); + let c = body.face_centre(&body.faces()[side].name).unwrap(); + assert!((c[0]).abs() < 1e-9 && (c[1] - 15.0).abs() < 1e-6 && (c[2] - 5.0).abs() < 1e-6, "{c:?}"); + assert!(body.face_centre("nope").is_err()); + } + #[test] fn block_measures() { let m = block([0., 0., 0.], [10., 20., 30.]).unwrap().measure().unwrap();