Files
OneCAD/crates/kernel/src/measure.rs
T

120 lines
4.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use serde::Serialize;
use solvecraft_geom::{Aabb3, Mesh, Vec3};
use crate::Result;
use crate::body::Body;
use crate::topo::{TopoCounts, merged_topology};
/// Measured properties of a body.
#[derive(Clone, Debug, Serialize)]
pub struct BodyMeasure {
pub volume: f64,
pub area: f64,
pub centroid: Vec3,
pub bbox: Aabb3,
/// Raw kernel topology counts (closed curves and surfaces may be split in pieces).
pub faces: usize,
pub edges: usize,
pub vertices: usize,
pub shells: usize,
/// Counts after merging split pieces of the same analytic surface/curve (comparable with
/// systems that use periodic faces and edges).
pub merged: TopoCounts,
}
/// Per-face sums: area, 6 × signed volume and 24 × first moments (divergence theorem, origin 0).
fn face_sums(m: &Mesh, nf: usize) -> Vec<(f64, f64, Vec3)> {
let mut out = vec![(0.0, 0.0, Vec3::ZERO); nf];
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
let (Some([a, b, c]), Some(slot)) = (m.tri(t), out.get_mut(*f as usize)) else { continue };
let d = a.dot(b.cross(c));
slot.0 += (b - a).cross(c - a).len() * 0.5;
slot.1 += d;
slot.2 += (a + b + c) * d;
}
out
}
/// Smallest radius of curvature along the mesh's edge polylines (straight edges ignored).
fn min_edge_radius(m: &Mesh) -> Option<f64> {
let mut best: Option<f64> = None;
for e in &m.edges {
for w in e.windows(3) {
let (Some(&a), Some(&b), Some(&c)) = (w.first(), w.get(1), w.get(2)) else { continue };
let (ab, bc, ca) = (b - a, c - b, a - c);
let cross = ab.cross(bc).len();
if !(cross > 1e-12 * ab.len() * bc.len()) {
continue;
}
let r = ab.len() * bc.len() * ca.len() / (2.0 * cross);
if r.is_finite() && r > 0.0 && best.is_none_or(|x| r < x) {
best = Some(r);
}
}
}
best
}
/// Measure a body from a fine tessellation (chord tolerance 5e-5 of the body size, finer for
/// small radii down to 5e-6). Faces whose fine tessellation disagrees with a coarse one (a
/// meshing failure) use the coarse result.
pub fn measure(b: &Body) -> Result<BodyMeasure> {
if let Some((pos, tris)) = b.triangle_mesh() {
// Exact for a mesh body; topology counts are facets, sides and vertices.
let m = b.tessellate(1.0)?;
let mm = m.measure();
let edges = b.mesh.as_ref().map(|m| m.edge_count()).unwrap_or(0);
let merged =
TopoCounts { faces: tris.len(), edges, vertices: pos.len(), face_types: [("mesh".to_string(), tris.len())].into_iter().collect() };
return Ok(BodyMeasure {
volume: mm.volume,
area: mm.area,
centroid: mm.centroid,
bbox: m.bounds(),
faces: tris.len(),
edges,
vertices: pos.len(),
shells: 1,
merged,
});
}
let size = b.size();
let coarse = b.tessellate_with((size * 1e-3).max(1e-3), true)?;
// Chord error relative to a radius sets the volume error: keep it near 3e-4 of the
// smallest radius (thin tubes, small fillets).
let tight = min_edge_radius(&coarse).map(|r| r * 3e-4).unwrap_or(f64::INFINITY);
let fine_tol = (size * 5e-5).min(tight).max(size * 5e-6).max(1e-4);
let fine = b.tessellate_with(fine_tol, true)?;
let medium = b.tessellate_with((fine_tol * 5.0).max(2e-4), true)?;
let nf = b.face_count();
let (sf, sm, sc) = (face_sums(&fine, nf), face_sums(&medium, nf), face_sums(&coarse, nf));
let close = |x: f64, y: f64| (x - y).abs() <= 0.01 * x.abs().max(y.abs()) + 1e-9;
let (mut area, mut v6, mut mom) = (0.0, 0.0, Vec3::ZERO);
for ((f, m), c) in sf.iter().zip(&sm).zip(&sc) {
// Trust the finest tessellation that agrees with the next coarser one.
let pick = if close(f.0, m.0) {
f
} else if close(m.0, c.0) {
m
} else {
f
};
area += pick.0;
v6 += pick.1;
mom += pick.2;
}
let volume = v6 / 6.0;
let centroid = if v6.abs() > 1e-12 { mom / (4.0 * v6) } else { fine.bounds().center() };
Ok(BodyMeasure {
volume,
area,
centroid,
bbox: coarse.bounds().union(&fine.bounds()),
faces: b.face_count(),
edges: b.edge_count(),
vertices: b.vertex_count(),
shells: b.shell_count(),
merged: merged_topology(b, &b.tessellate((size * 1e-3).max(1e-3))?)?,
})
}