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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 23:51:01 -04:00
1 parent 0bf150633c
commit fbaba5fecd
2 files changed
+71

No files matched your search

+31
View File
@@ -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();
+40
View File
@@ -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<Vec<Option<FaceSurface>>> {
@@ -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();