use glam::DVec3;
use pantometry_units::{Area, Length, LengthVec, Volume};
type Vertex = (u64, u64, u64);
type Edge = (Vertex, Vertex);
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Triangle {
pub a: DVec3,
pub b: DVec3,
pub c: DVec3,
}
impl Triangle {
pub fn normal_area(&self) -> DVec3 {
(self.b - self.a).cross(self.c - self.a)
}
pub fn area(&self) -> Area {
Area::from_si(0.5 * self.normal_area().length())
}
}
#[derive(Clone, Debug, Default)]
pub struct Mesh {
triangles: Vec<Triangle>,
}
impl Mesh {
pub fn new(triangles: Vec<Triangle>) -> Mesh {
Mesh { triangles }
}
pub fn from_stl(bytes: &[u8]) -> Result<Mesh, String> {
if bytes.len() >= 84 {
let count = u32::from_le_bytes([bytes[80], bytes[81], bytes[82], bytes[83]]) as usize;
if let Some(expected) = count.checked_mul(50).and_then(|n| n.checked_add(84)) {
if expected == bytes.len() {
return Mesh::from_binary_stl(bytes, count);
}
}
}
Mesh::from_ascii_stl(bytes)
}
fn from_binary_stl(bytes: &[u8], count: usize) -> Result<Mesh, String> {
let mut triangles = Vec::with_capacity(count);
let f32_at = |at: usize| -> f64 {
f32::from_le_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]]) as f64
};
for n in 0..count {
let base = 84 + 50 * n + 12;
let v = |k: usize| {
DVec3::new(
f32_at(base + 12 * k),
f32_at(base + 12 * k + 4),
f32_at(base + 12 * k + 8),
) * 1e-3
};
triangles.push(Triangle {
a: v(0),
b: v(1),
c: v(2),
});
}
Ok(Mesh { triangles })
}
fn from_ascii_stl(bytes: &[u8]) -> Result<Mesh, String> {
let text = std::str::from_utf8(bytes)
.map_err(|e| format!("not a binary STL by length, and not UTF-8 text either: {e}"))?;
let mut vertices: Vec<DVec3> = Vec::new();
let mut triangles = Vec::new();
for (n, line) in text.lines().enumerate() {
let mut word = line.split_whitespace();
if word.next() != Some("vertex") {
continue;
}
let mut coordinate = || -> Result<f64, String> {
word.next()
.ok_or_else(|| format!("line {}: a vertex needs three numbers", n + 1))?
.parse::<f64>()
.map_err(|e| format!("line {}: {e}", n + 1))
};
let (x, y, z) = (coordinate()?, coordinate()?, coordinate()?);
vertices.push(DVec3::new(x, y, z) * 1e-3);
if vertices.len() == 3 {
triangles.push(Triangle {
a: vertices[0],
b: vertices[1],
c: vertices[2],
});
vertices.clear();
}
}
if !vertices.is_empty() {
return Err(format!(
"the file ends with {} vertices left over, so a facet is incomplete",
vertices.len()
));
}
if triangles.is_empty() {
return Err("no facets found; is this an STL?".to_string());
}
Ok(Mesh { triangles })
}
pub fn triangles(&self) -> &[Triangle] {
&self.triangles
}
pub fn bounds(&self) -> Option<(LengthVec, LengthVec)> {
let first = self.triangles.first()?;
let mut low = first.a;
let mut high = first.a;
for t in &self.triangles {
for v in [t.a, t.b, t.c] {
low = low.min(v);
high = high.max(v);
}
}
Some((LengthVec::from_si(low), LengthVec::from_si(high)))
}
pub fn volume(&self) -> Volume {
Volume::from_si(
self.triangles
.iter()
.map(|t| t.a.dot(t.b.cross(t.c)) / 6.0)
.sum::<f64>(),
)
}
pub fn area(&self) -> Area {
Area::from_si(self.triangles.iter().map(|t| t.area().to_si()).sum())
}
pub fn is_closed(&self) -> bool {
let zeroed = |c: f64| if c == 0.0 { 0.0 } else { c };
let key = |v: DVec3| {
(
zeroed(v.x).to_bits(),
zeroed(v.y).to_bits(),
zeroed(v.z).to_bits(),
)
};
let mut edges: std::collections::HashMap<Edge, i32> = std::collections::HashMap::new();
for t in &self.triangles {
for (p, q) in [(t.a, t.b), (t.b, t.c), (t.c, t.a)] {
let (p, q) = (key(p), key(q));
let edge = if p <= q { (p, q) } else { (q, p) };
*edges.entry(edge).or_insert(0) += 1;
}
}
!edges.is_empty() && edges.values().all(|n| *n == 2)
}
pub fn triangles_below(&self, cell: Length) -> usize {
let face = cell.to_si() * cell.to_si();
self.triangles
.iter()
.filter(|t| t.area().to_si() < face)
.count()
}
}