pantometry_shape/mesh.rs
1//! A bag of triangles, read from what a CAD tool exported, and what can be measured from one.
2
3use glam::DVec3;
4use pantometry_units::{Area, Length, LengthVec, Volume};
5
6/// A vertex as its three coordinates' bit patterns, which is how [`Mesh::is_closed`] decides that two
7/// triangles are talking about the same point. See that method for why it is exact.
8type Vertex = (u64, u64, u64);
9
10/// An edge as its two vertices, ordered so the same edge keys the same however it was wound.
11type Edge = (Vertex, Vertex);
12
13/// One triangle, in metres.
14///
15/// Wound counter-clockwise seen from outside, which is what makes [`Mesh::volume`] positive. STL states
16/// a normal per facet as well, and it is **ignored**: exporters disagree about it often enough that the
17/// winding is the more reliable of the two, and carrying a normal that might contradict the vertices
18/// would mean choosing between them at every use.
19#[derive(Clone, Copy, Debug, PartialEq)]
20pub struct Triangle {
21 /// First vertex.
22 pub a: DVec3,
23 /// Second vertex.
24 pub b: DVec3,
25 /// Third vertex.
26 pub c: DVec3,
27}
28
29impl Triangle {
30 /// Twice the area as a vector along the normal — the cross product of two edges.
31 pub fn normal_area(&self) -> DVec3 {
32 (self.b - self.a).cross(self.c - self.a)
33 }
34
35 /// The triangle's area.
36 pub fn area(&self) -> Area {
37 Area::from_si(0.5 * self.normal_area().length())
38 }
39}
40
41/// A closed surface as triangles, in metres.
42///
43/// # STL carries no topology, and that shapes what can be checked
44///
45/// The format is a flat list of facets, each with its three vertices written out in full. Two triangles
46/// sharing an edge repeat those two vertices, and nothing in the file says they are the same points. So
47/// [`Mesh::is_closed`] has to *infer* the topology by matching coordinates, and it matches them exactly —
48/// bit for bit.
49///
50/// That is the right strictness for the thing being asked. A mesh whose shared vertices differ in the
51/// last bit is not closed for any purpose that matters here: a ray can pass between the two triangles,
52/// and the rasteriser below will see it. Reporting it as open is the true answer, and a tolerance would
53/// turn a real defect into a silent one.
54#[derive(Clone, Debug, Default)]
55pub struct Mesh {
56 triangles: Vec<Triangle>,
57}
58
59impl Mesh {
60 /// A mesh from triangles, in metres.
61 pub fn new(triangles: Vec<Triangle>) -> Mesh {
62 Mesh { triangles }
63 }
64
65 /// Read an STL, binary or ASCII.
66 ///
67 /// # Which one it is, decided by arithmetic rather than by the first word
68 ///
69 /// The usual test is whether the file starts with `solid`, and it is wrong: a binary STL's header is
70 /// eighty arbitrary bytes and plenty of exporters write `solid` into it. The reliable test is the
71 /// length — a binary file is exactly `84 + 50n` bytes for its own declared `n`, and no ASCII file of
72 /// that content is. This uses that, and falls back to ASCII.
73 ///
74 /// Lengths are taken as **millimetres**, because STL has no units and every mechanical CAD tool
75 /// writes millimetres. That is a convention rather than a fact about the format, so it is stated
76 /// here and nowhere else has to guess.
77 pub fn from_stl(bytes: &[u8]) -> Result<Mesh, String> {
78 if bytes.len() >= 84 {
79 let count = u32::from_le_bytes([bytes[80], bytes[81], bytes[82], bytes[83]]) as usize;
80 if let Some(expected) = count.checked_mul(50).and_then(|n| n.checked_add(84)) {
81 if expected == bytes.len() {
82 return Mesh::from_binary_stl(bytes, count);
83 }
84 }
85 }
86 Mesh::from_ascii_stl(bytes)
87 }
88
89 fn from_binary_stl(bytes: &[u8], count: usize) -> Result<Mesh, String> {
90 let mut triangles = Vec::with_capacity(count);
91 let f32_at = |at: usize| -> f64 {
92 f32::from_le_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]]) as f64
93 };
94 for n in 0..count {
95 // 84-byte header and count, then 50 per facet: a normal this ignores, three vertices, and
96 // two attribute bytes nothing standard uses.
97 let base = 84 + 50 * n + 12;
98 let v = |k: usize| {
99 DVec3::new(
100 f32_at(base + 12 * k),
101 f32_at(base + 12 * k + 4),
102 f32_at(base + 12 * k + 8),
103 ) * 1e-3
104 };
105 triangles.push(Triangle {
106 a: v(0),
107 b: v(1),
108 c: v(2),
109 });
110 }
111 Ok(Mesh { triangles })
112 }
113
114 fn from_ascii_stl(bytes: &[u8]) -> Result<Mesh, String> {
115 let text = std::str::from_utf8(bytes)
116 .map_err(|e| format!("not a binary STL by length, and not UTF-8 text either: {e}"))?;
117 let mut vertices: Vec<DVec3> = Vec::new();
118 let mut triangles = Vec::new();
119 for (n, line) in text.lines().enumerate() {
120 let mut word = line.split_whitespace();
121 if word.next() != Some("vertex") {
122 continue;
123 }
124 let mut coordinate = || -> Result<f64, String> {
125 word.next()
126 .ok_or_else(|| format!("line {}: a vertex needs three numbers", n + 1))?
127 .parse::<f64>()
128 .map_err(|e| format!("line {}: {e}", n + 1))
129 };
130 let (x, y, z) = (coordinate()?, coordinate()?, coordinate()?);
131 vertices.push(DVec3::new(x, y, z) * 1e-3);
132 if vertices.len() == 3 {
133 triangles.push(Triangle {
134 a: vertices[0],
135 b: vertices[1],
136 c: vertices[2],
137 });
138 vertices.clear();
139 }
140 }
141 if !vertices.is_empty() {
142 return Err(format!(
143 "the file ends with {} vertices left over, so a facet is incomplete",
144 vertices.len()
145 ));
146 }
147 if triangles.is_empty() {
148 return Err("no facets found; is this an STL?".to_string());
149 }
150 Ok(Mesh { triangles })
151 }
152
153 /// The triangles, in the order read.
154 pub fn triangles(&self) -> &[Triangle] {
155 &self.triangles
156 }
157
158 /// The axis-aligned bounds, as `(low, high)`.
159 ///
160 /// `None` for a mesh with no triangles, because the bounds of nothing are not a box at the origin.
161 pub fn bounds(&self) -> Option<(LengthVec, LengthVec)> {
162 let first = self.triangles.first()?;
163 let mut low = first.a;
164 let mut high = first.a;
165 for t in &self.triangles {
166 for v in [t.a, t.b, t.c] {
167 low = low.min(v);
168 high = high.max(v);
169 }
170 }
171 Some((LengthVec::from_si(low), LengthVec::from_si(high)))
172 }
173
174 /// The enclosed volume, by the divergence theorem.
175 ///
176 /// `Σ a · (b × c) / 6` — the signed volume of the tetrahedron each triangle makes with the origin,
177 /// summed. Everything outside the surface cancels exactly, so for a **closed** mesh this is the
178 /// enclosed volume and it is exact to floating point, with no tolerance and no sampling.
179 ///
180 /// That exactness is what makes it the reference [`Loss::volume_error`](crate::Loss::volume_error) measures
181 /// against: comparing a rasterisation to an analytic sphere would conflate two errors, the
182 /// tessellation's and the grid's. Comparing it to the mesh's own volume isolates the one being
183 /// measured.
184 ///
185 /// For an **open** mesh the number is meaningless rather than approximate — check [`Mesh::is_closed`].
186 /// A negative volume means the winding is inside out, which is a real and common export defect.
187 pub fn volume(&self) -> Volume {
188 Volume::from_si(
189 self.triangles
190 .iter()
191 .map(|t| t.a.dot(t.b.cross(t.c)) / 6.0)
192 .sum::<f64>(),
193 )
194 }
195
196 /// The total surface area.
197 pub fn area(&self) -> Area {
198 Area::from_si(self.triangles.iter().map(|t| t.area().to_si()).sum())
199 }
200
201 /// Whether every edge is shared by exactly two triangles.
202 ///
203 /// Matched on the vertices' **bit patterns**, for the reason in this type's documentation: STL stores
204 /// no topology, so shared vertices are shared only if they were written identically, and a ray passes
205 /// through a gap of one bit as readily as through a gap of one millimetre.
206 ///
207 /// With one exception, and it is not a tolerance. **Negative zero is folded onto zero**, because
208 /// `-0.0` and `0.0` are the *same point* — the distance between them is nothing, and no ray passes
209 /// between them. Their bit patterns differ, so a raw comparison reports a watertight mesh as open,
210 /// and that is a false alarm rather than a strict answer. It arises constantly on anything symmetric
211 /// about an axis, where one side's coordinate is a product that happened to carry a minus sign.
212 ///
213 /// A mesh that is not closed has no enclosed volume and cannot be rasterised by parity, so
214 /// [`Voxels::of`](crate::Voxels::of) refuses one rather than producing a shape with holes in it.
215 pub fn is_closed(&self) -> bool {
216 // `+ 0.0` would do it in one operation, but this says what is meant and does not read as a
217 // no-op that a later reader deletes.
218 let zeroed = |c: f64| if c == 0.0 { 0.0 } else { c };
219 let key = |v: DVec3| {
220 (
221 zeroed(v.x).to_bits(),
222 zeroed(v.y).to_bits(),
223 zeroed(v.z).to_bits(),
224 )
225 };
226 let mut edges: std::collections::HashMap<Edge, i32> = std::collections::HashMap::new();
227 for t in &self.triangles {
228 for (p, q) in [(t.a, t.b), (t.b, t.c), (t.c, t.a)] {
229 let (p, q) = (key(p), key(q));
230 // Undirected, so the two triangles sharing an edge meet on the same key however they
231 // wound it.
232 let edge = if p <= q { (p, q) } else { (q, p) };
233 *edges.entry(edge).or_insert(0) += 1;
234 }
235 }
236 !edges.is_empty() && edges.values().all(|n| *n == 2)
237 }
238
239 /// How many triangles are smaller than one face of a cell of side `cell`.
240 ///
241 /// A feature the grid cannot hold, counted before anything is rasterised. It is not a proof that
242 /// something is lost — a large flat face can be tessellated into small triangles and lose nothing —
243 /// but a mesh where many facets are below the cell's own area is a mesh whose detail is finer than
244 /// the grid, and that is worth being told before the run rather than after.
245 pub fn triangles_below(&self, cell: Length) -> usize {
246 let face = cell.to_si() * cell.to_si();
247 self.triangles
248 .iter()
249 .filter(|t| t.area().to_si() < face)
250 .count()
251 }
252}