forked from 1b-agent/fieldcraft-upstream
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:
1 parent
0bf150633c
commit
fbaba5fecd
2 files changed
+71
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user