Skip to main content

cadvm_core/
mesh.rs

1//! Lightweight metadata for triangle-mesh formats (STL, OBJ).
2//!
3//! Like the STEP scanner, this does **not** interpret geometry deeply — it just
4//! surfaces cheap, useful figures (triangle/vertex counts and a bounding box)
5//! for `show`, `log` and the metadata `diff`.
6
7use serde::{Deserialize, Serialize};
8
9use crate::format::CadFormat;
10
11/// An axis-aligned bounding box of a mesh.
12#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13pub struct MeshBBox {
14    pub min: [f64; 3],
15    pub max: [f64; 3],
16}
17
18impl MeshBBox {
19    /// Box dimensions `[dx, dy, dz]`.
20    pub fn size(&self) -> [f64; 3] {
21        [
22            self.max[0] - self.min[0],
23            self.max[1] - self.min[1],
24            self.max[2] - self.min[2],
25        ]
26    }
27}
28
29/// Lightweight mesh metadata.
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub struct MeshMetadata {
32    pub triangles: Option<u64>,
33    pub vertices: Option<u64>,
34    #[serde(default)]
35    pub bbox: Option<MeshBBox>,
36}
37
38/// Extract mesh metadata for a tracked mesh format. Returns `None` for non-mesh
39/// formats or unparseable content.
40pub fn extract(content: &[u8], format: CadFormat) -> Option<MeshMetadata> {
41    match format {
42        CadFormat::Stl => Some(extract_stl(content)),
43        CadFormat::Obj => Some(extract_obj(content)),
44        _ => None,
45    }
46}
47
48struct BBoxAccum {
49    min: [f64; 3],
50    max: [f64; 3],
51    any: bool,
52}
53
54impl BBoxAccum {
55    fn new() -> Self {
56        BBoxAccum {
57            min: [f64::INFINITY; 3],
58            max: [f64::NEG_INFINITY; 3],
59            any: false,
60        }
61    }
62    fn add(&mut self, p: [f64; 3]) {
63        self.any = true;
64        for (m, &v) in self.min.iter_mut().zip(p.iter()) {
65            *m = m.min(v);
66        }
67        for (m, &v) in self.max.iter_mut().zip(p.iter()) {
68            *m = m.max(v);
69        }
70    }
71    fn finish(self) -> Option<MeshBBox> {
72        self.any.then_some(MeshBBox {
73            min: self.min,
74            max: self.max,
75        })
76    }
77}
78
79/// STL: binary (84-byte header + 50 bytes/triangle) or ASCII (`facet`/`vertex`).
80fn extract_stl(content: &[u8]) -> MeshMetadata {
81    // Binary STL is detected by the exact size implied by its triangle count,
82    // not by the leading "solid" word (which binary files may also contain).
83    if content.len() >= 84 {
84        let count =
85            u32::from_le_bytes([content[80], content[81], content[82], content[83]]) as usize;
86        if content.len() == 84 + count * 50 {
87            return stl_binary(content, count);
88        }
89    }
90    stl_ascii(content)
91}
92
93fn stl_binary(content: &[u8], count: usize) -> MeshMetadata {
94    let mut bbox = BBoxAccum::new();
95    let read_f32 = |b: &[u8]| f32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f64;
96    for t in 0..count {
97        let base = 84 + t * 50 + 12; // skip the 3-float normal
98        for v in 0..3 {
99            let o = base + v * 12;
100            bbox.add([
101                read_f32(&content[o..o + 4]),
102                read_f32(&content[o + 4..o + 8]),
103                read_f32(&content[o + 8..o + 12]),
104            ]);
105        }
106    }
107    MeshMetadata {
108        triangles: Some(count as u64),
109        vertices: Some(count as u64 * 3),
110        bbox: bbox.finish(),
111    }
112}
113
114fn stl_ascii(content: &[u8]) -> MeshMetadata {
115    let text = String::from_utf8_lossy(content);
116    let mut triangles = 0u64;
117    let mut bbox = BBoxAccum::new();
118    for line in text.lines() {
119        let line = line.trim_start();
120        if line.starts_with("facet") {
121            triangles += 1;
122        } else if let Some(rest) = line.strip_prefix("vertex") {
123            if let Some(p) = parse_xyz(rest) {
124                bbox.add(p);
125            }
126        }
127    }
128    MeshMetadata {
129        triangles: Some(triangles),
130        vertices: Some(triangles * 3),
131        bbox: bbox.finish(),
132    }
133}
134
135/// OBJ: count `v` vertices and triangulate `f` faces (n-gon → n-2 triangles).
136fn extract_obj(content: &[u8]) -> MeshMetadata {
137    let text = String::from_utf8_lossy(content);
138    let mut vertices = 0u64;
139    let mut triangles = 0u64;
140    let mut bbox = BBoxAccum::new();
141    for line in text.lines() {
142        let line = line.trim_start();
143        if let Some(rest) = line.strip_prefix("v ") {
144            vertices += 1;
145            if let Some(p) = parse_xyz(rest) {
146                bbox.add(p);
147            }
148        } else if let Some(rest) = line.strip_prefix("f ") {
149            let refs = rest.split_whitespace().count() as u64;
150            triangles += refs.saturating_sub(2);
151        }
152    }
153    MeshMetadata {
154        triangles: Some(triangles),
155        vertices: Some(vertices),
156        bbox: bbox.finish(),
157    }
158}
159
160/// Parse the first three whitespace-separated floats of a line.
161fn parse_xyz(s: &str) -> Option<[f64; 3]> {
162    let mut it = s.split_whitespace();
163    let x = it.next()?.parse().ok()?;
164    let y = it.next()?.parse().ok()?;
165    let z = it.next()?.parse().ok()?;
166    Some([x, y, z])
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn ascii_stl_counts() {
175        let stl = "solid s\n\
176            facet normal 0 0 1\n outer loop\n vertex 0 0 0\n vertex 1 0 0\n vertex 0 1 0\n endloop\n endfacet\n\
177            facet normal 0 0 1\n outer loop\n vertex 0 0 0\n vertex 1 0 0\n vertex 1 1 2\n endloop\n endfacet\n\
178            endsolid s\n";
179        let m = extract(stl.as_bytes(), CadFormat::Stl).unwrap();
180        assert_eq!(m.triangles, Some(2));
181        assert_eq!(m.vertices, Some(6));
182        assert_eq!(m.bbox.unwrap().max, [1.0, 1.0, 2.0]);
183    }
184
185    #[test]
186    fn obj_counts_and_triangulates() {
187        let obj = "v 0 0 0\nv 1 0 0\nv 1 1 0\nv 0 1 0\nf 1 2 3 4\nf 1 2 3\n";
188        let m = extract(obj.as_bytes(), CadFormat::Obj).unwrap();
189        assert_eq!(m.vertices, Some(4));
190        // quad (2 tris) + triangle (1 tri) = 3
191        assert_eq!(m.triangles, Some(3));
192        assert_eq!(m.bbox.unwrap().min, [0.0, 0.0, 0.0]);
193    }
194
195    #[test]
196    fn binary_stl_one_triangle() {
197        let mut data = vec![0u8; 84 + 50];
198        data[80..84].copy_from_slice(&1u32.to_le_bytes());
199        // one triangle: normal (skip) + 3 verts at offset 84+12
200        let verts: [f32; 9] = [0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 3.0, 0.0];
201        let mut o = 84 + 12;
202        for f in verts {
203            data[o..o + 4].copy_from_slice(&f.to_le_bytes());
204            o += 4;
205        }
206        let m = extract(&data, CadFormat::Stl).unwrap();
207        assert_eq!(m.triangles, Some(1));
208        assert_eq!(m.bbox.unwrap().max, [2.0, 3.0, 0.0]);
209    }
210}