Skip to main content

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}