1use crate::Mesh;
2use serde::Serialize;
3
4#[derive(Clone, Debug, Serialize)]
5pub struct StlReadResult {
6 pub tri_count: u32,
7 pub positions: Vec<f64>,
8}
9
10#[derive(Clone, Debug, Serialize)]
11pub struct ObjReadResult {
12 pub positions: Vec<f64>,
13 pub indices: Vec<u32>,
14}
15
16fn triangle_point(mesh: &Mesh, index: u32) -> Result<[f64; 3], String> {
17 let offset = index as usize * 3;
18 if offset + 2 >= mesh.positions.len() {
19 return Err("write_binary_stl: triangle index outside position buffer".into());
20 }
21 Ok([
22 mesh.positions[offset],
23 mesh.positions[offset + 1],
24 mesh.positions[offset + 2],
25 ])
26}
27
28fn write_f32(output: &mut [u8], offset: usize, value: f64) {
29 output[offset..offset + 4].copy_from_slice(&(value as f32).to_le_bytes());
30}
31
32pub fn write_binary_stl(mesh: &Mesh, name: &str) -> Result<Vec<u8>, String> {
34 mesh.validate()?;
35 let triangle_count = mesh.indices.len() / 3;
36 let mut output = vec![0u8; 84 + 50 * triangle_count];
37 let header = format!("binary STL - {name}");
38 let header_bytes = header.as_bytes();
39 let header_length = 79usize.min(header_bytes.len());
40 output[..header_length].copy_from_slice(&header_bytes[..header_length]);
41 output[80..84].copy_from_slice(&(triangle_count as u32).to_le_bytes());
42 let mut offset = 84;
43 for triangle in mesh.indices.chunks_exact(3) {
44 let a = triangle_point(mesh, triangle[0])?;
45 let b = triangle_point(mesh, triangle[1])?;
46 let c = triangle_point(mesh, triangle[2])?;
47 let u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
48 let v = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
49 let mut normal = [
50 u[1] * v[2] - u[2] * v[1],
51 u[2] * v[0] - u[0] * v[2],
52 u[0] * v[1] - u[1] * v[0],
53 ];
54 let length = (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
55 if length > 1e-30 {
56 for coordinate in &mut normal {
57 *coordinate /= length;
58 }
59 }
60 for coordinate in normal {
61 write_f32(&mut output, offset, coordinate);
62 offset += 4;
63 }
64 for point in [a, b, c] {
65 for coordinate in point {
66 write_f32(&mut output, offset, coordinate);
67 offset += 4;
68 }
69 }
70 output[offset..offset + 2].copy_from_slice(&0u16.to_le_bytes());
71 offset += 2;
72 }
73 Ok(output)
74}
75
76fn read_u32(data: &[u8], offset: usize) -> Result<u32, String> {
77 let bytes = data
78 .get(offset..offset + 4)
79 .ok_or_else(|| "read_binary_stl: truncated triangle count".to_string())?;
80 Ok(u32::from_le_bytes(bytes.try_into().unwrap()))
81}
82
83fn read_f32(data: &[u8], offset: usize) -> Result<f32, String> {
84 let bytes = data
85 .get(offset..offset + 4)
86 .ok_or_else(|| "read_binary_stl: truncated triangle".to_string())?;
87 Ok(f32::from_le_bytes(bytes.try_into().unwrap()))
88}
89
90pub fn read_binary_stl(data: &[u8]) -> Result<StlReadResult, String> {
92 if data.len() < 84 {
93 return Err("read_binary_stl: file is shorter than the STL header".into());
94 }
95 let triangle_count = read_u32(data, 80)?;
96 let expected = 84usize
97 .checked_add(50usize.saturating_mul(triangle_count as usize))
98 .ok_or_else(|| "read_binary_stl: file size overflow".to_string())?;
99 if data.len() < expected {
100 return Err(format!(
101 "read_binary_stl: expected {expected} bytes, received {}",
102 data.len()
103 ));
104 }
105 let mut positions = Vec::with_capacity(triangle_count as usize * 9);
106 let mut offset = 84;
107 for _ in 0..triangle_count {
108 offset += 12;
109 for _ in 0..9 {
110 positions.push(read_f32(data, offset)? as f64);
111 offset += 4;
112 }
113 offset += 2;
114 }
115 Ok(StlReadResult {
116 tri_count: triangle_count,
117 positions,
118 })
119}
120
121fn obj_face_vertex(token: &str, vertex_count: usize, line_number: usize) -> Result<u32, String> {
126 let mut parts = token.split('/');
127 let vertex_text = parts.next().unwrap_or_default();
128 if parts.count() > 2 {
129 return Err(format!(
130 "read_obj: line {line_number}: face vertex '{token}' has more than v/vt/vn parts"
131 ));
132 }
133 let raw: i64 = vertex_text.parse().map_err(|_| {
134 format!("read_obj: line {line_number}: face vertex '{token}' has no vertex index")
135 })?;
136 let resolved = match raw {
138 1.. => raw - 1,
139 0 => {
140 return Err(format!(
141 "read_obj: line {line_number}: vertex index 0 is not valid OBJ"
142 ))
143 }
144 _ => vertex_count as i64 + raw,
145 };
146 if resolved < 0 || resolved >= vertex_count as i64 {
147 return Err(format!(
148 "read_obj: line {line_number}: vertex index {raw} outside the \
149 {vertex_count} vertices defined so far"
150 ));
151 }
152 Ok(resolved as u32)
153}
154
155pub fn read_obj(text: &str) -> Result<ObjReadResult, String> {
164 let mut positions = Vec::<f64>::new();
165 let mut indices = Vec::<u32>::new();
166 for (line_index, raw_line) in text.lines().enumerate() {
167 let line_number = line_index + 1;
168 let line = raw_line.split('#').next().unwrap_or_default().trim();
170 if line.is_empty() {
171 continue;
172 }
173 let mut tokens = line.split_whitespace();
174 match tokens.next().unwrap_or_default() {
175 "v" => {
176 let mut point = [0.0f64; 3];
177 for coordinate in &mut point {
178 *coordinate = tokens
179 .next()
180 .ok_or_else(|| format!("read_obj: line {line_number}: vertex needs x y z"))?
181 .parse()
182 .map_err(|_| {
183 format!(
184 "read_obj: line {line_number}: vertex coordinate is not a number"
185 )
186 })?;
187 }
188 positions.extend(point);
191 }
192 "f" => {
193 let vertex_count = positions.len() / 3;
196 let corners = tokens
197 .map(|token| obj_face_vertex(token, vertex_count, line_number))
198 .collect::<Result<Vec<_>, _>>()?;
199 if corners.len() < 3 {
200 return Err(format!(
201 "read_obj: line {line_number}: face needs at least 3 vertices"
202 ));
203 }
204 for corner in 1..corners.len() - 1 {
205 indices.extend([corners[0], corners[corner], corners[corner + 1]]);
206 }
207 }
208 _ => {}
211 }
212 }
213 if positions.is_empty() {
217 return Err("read_obj: no vertices found — not Wavefront OBJ text?".into());
218 }
219 if indices.is_empty() {
220 return Err("read_obj: no faces found — nothing to build a mesh from".into());
221 }
222 Ok(ObjReadResult { positions, indices })
223}
224
225pub fn write_obj(mesh: &Mesh, name: &str) -> Result<String, String> {
227 mesh.validate()?;
228 let mut lines = vec![format!("# {name}"), format!("o {name}")];
229 for point in mesh.positions.chunks_exact(3) {
230 lines.push(format!("v {} {} {}", point[0], point[1], point[2]));
231 }
232 for normal in mesh.normals.chunks_exact(3) {
233 lines.push(format!("vn {} {} {}", normal[0], normal[1], normal[2]));
234 }
235 for triangle in mesh.indices.chunks_exact(3) {
236 let a = triangle[0] + 1;
237 let b = triangle[1] + 1;
238 let c = triangle[2] + 1;
239 lines.push(format!("f {a}//{a} {b}//{b} {c}//{c}"));
240 }
241 Ok(lines.join("\n") + "\n")
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use crate::{
248 make_box, make_box_brep, mesh_to_faceted_brep, solid_mass_properties,
249 tessellate_brep_watertight, Vec3,
250 };
251
252 fn box_mesh() -> Mesh {
253 let solid = make_box_brep(Vec3::default(), 2.0, 3.0, 4.0).unwrap();
254 tessellate_brep_watertight(&solid, 1e-3).unwrap()
255 }
256
257 #[test]
258 fn binary_stl_round_trip_preserves_triangle_positions() {
259 let mesh = make_box(2.0, 3.0, 4.0);
260 let bytes = write_binary_stl(&mesh, "box").unwrap();
261 let parsed = read_binary_stl(&bytes).unwrap();
262 assert_eq!(parsed.tri_count as usize, mesh.indices.len() / 3);
263 assert_eq!(parsed.positions.len(), mesh.indices.len() * 3);
264 assert_eq!(bytes.len(), 84 + 50 * parsed.tri_count as usize);
265 }
266
267 #[test]
268 fn obj_contains_every_indexed_triangle() {
269 let mesh = make_box(1.0, 1.0, 1.0);
270 let obj = write_obj(&mesh, "box").unwrap();
271 assert_eq!(
272 obj.lines().filter(|line| line.starts_with("v ")).count(),
273 24
274 );
275 assert_eq!(
276 obj.lines().filter(|line| line.starts_with("f ")).count(),
277 12
278 );
279 }
280
281 #[test]
282 fn obj_round_trip_preserves_positions_and_indices() {
283 let mesh = box_mesh();
284 let parsed = read_obj(&write_obj(&mesh, "box").unwrap()).unwrap();
285 assert_eq!(parsed.positions, mesh.positions);
288 assert_eq!(parsed.indices, mesh.indices);
289 }
290
291 #[test]
292 fn quad_face_fan_triangulates_and_state_lines_are_skipped() {
293 let obj = "mtllib scene.mtl\no quad\ng side\ns off\nusemtl steel\n\
295 vp 0.5\nvt 0 0\nvn 0 0 1\n# comment\n\
296 v 0 0 0\nv 1 0 0 # trailing comment\nv 1 1 0\nv 0 1 0\n\
297 f 1/1/1 2/2 3//1 4\n";
298 let parsed = read_obj(obj).unwrap();
299 assert_eq!(parsed.positions.len(), 12);
300 assert_eq!(parsed.indices, vec![0, 1, 2, 0, 2, 3]);
301 }
302
303 #[test]
304 fn negative_indices_match_their_positive_twin() {
305 let positive = read_obj("v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n").unwrap();
306 let negative = read_obj("v 0 0 0\nv 1 0 0\nv 0 1 0\nf -3 -2 -1\n").unwrap();
307 assert_eq!(positive.indices, vec![0, 1, 2]);
308 assert_eq!(negative.positions, positive.positions);
309 assert_eq!(negative.indices, positive.indices);
310 }
311
312 #[test]
313 fn rejects_malformed_obj_honestly() {
314 assert!(read_obj("v 1 2\nf 1 1 1\n").is_err());
316 assert!(read_obj("v 1 2 x\nf 1 1 1\n").is_err());
317 let vertices = "v 0 0 0\nv 1 0 0\nv 0 1 0\n";
320 assert!(read_obj(&format!("{vertices}f 1 2\n")).is_err());
321 assert!(read_obj(&format!("{vertices}f 1 2 4\n")).is_err());
322 assert!(read_obj(&format!("{vertices}f 1 2 -4\n")).is_err());
323 assert!(read_obj(&format!("{vertices}f 0 1 2\n")).is_err());
324 assert!(read_obj(&format!("{vertices}f 1 2 3/3/3/3\n")).is_err());
325 assert!(read_obj("solid box\nfacet normal 0 0 1\n").is_err());
327 }
328
329 #[test]
330 fn obj_text_imports_a_watertight_box_solid() {
331 let mesh = box_mesh();
332 let parsed = read_obj(&write_obj(&mesh, "box").unwrap()).unwrap();
333 let solid = mesh_to_faceted_brep(&parsed.positions, Some(&parsed.indices), -1.0).unwrap();
334 assert!(solid.validate().is_empty(), "{:?}", solid.validate());
335 assert_eq!(solid.genus, 0);
336 let volume = solid_mass_properties(&solid).unwrap().volume;
337 assert!((volume - 24.0).abs() < 1e-9, "{volume}");
338 }
339}