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 pub format: StlFormat,
10}
11
12#[derive(Clone, Debug, Serialize)]
13pub struct ObjReadResult {
14 pub positions: Vec<f64>,
15 pub indices: Vec<u32>,
16}
17
18fn triangle_point(mesh: &Mesh, index: u32) -> Result<[f64; 3], String> {
19 let offset = index as usize * 3;
20 if offset + 2 >= mesh.positions.len() {
21 return Err("write_binary_stl: triangle index outside position buffer".into());
22 }
23 Ok([
24 mesh.positions[offset],
25 mesh.positions[offset + 1],
26 mesh.positions[offset + 2],
27 ])
28}
29
30fn write_f32(output: &mut [u8], offset: usize, value: f64) {
31 output[offset..offset + 4].copy_from_slice(&(value as f32).to_le_bytes());
32}
33
34pub fn write_binary_stl(mesh: &Mesh, name: &str) -> Result<Vec<u8>, String> {
36 mesh.validate()?;
37 let triangle_count = mesh.indices.len() / 3;
38 let mut output = vec![0u8; 84 + 50 * triangle_count];
39 let header = format!("binary STL - {name}");
40 let header_bytes = header.as_bytes();
41 let header_length = 79usize.min(header_bytes.len());
42 output[..header_length].copy_from_slice(&header_bytes[..header_length]);
43 output[80..84].copy_from_slice(&(triangle_count as u32).to_le_bytes());
44 let mut offset = 84;
45 for triangle in mesh.indices.chunks_exact(3) {
46 let a = triangle_point(mesh, triangle[0])?;
47 let b = triangle_point(mesh, triangle[1])?;
48 let c = triangle_point(mesh, triangle[2])?;
49 let u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
50 let v = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
51 let mut normal = [
52 u[1] * v[2] - u[2] * v[1],
53 u[2] * v[0] - u[0] * v[2],
54 u[0] * v[1] - u[1] * v[0],
55 ];
56 let length = (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
57 if length > 1e-30 {
58 for coordinate in &mut normal {
59 *coordinate /= length;
60 }
61 }
62 for coordinate in normal {
63 write_f32(&mut output, offset, coordinate);
64 offset += 4;
65 }
66 for point in [a, b, c] {
67 for coordinate in point {
68 write_f32(&mut output, offset, coordinate);
69 offset += 4;
70 }
71 }
72 output[offset..offset + 2].copy_from_slice(&0u16.to_le_bytes());
73 offset += 2;
74 }
75 Ok(output)
76}
77
78#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
80#[serde(rename_all = "lowercase")]
81pub enum StlFormat {
82 Binary,
84 Ascii,
86}
87
88impl std::fmt::Display for StlFormat {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 f.write_str(match self {
91 Self::Binary => "binary",
92 Self::Ascii => "ASCII",
93 })
94 }
95}
96
97#[derive(Clone, Debug, PartialEq, Eq)]
99pub enum StlError {
100 InvalidFormat(String),
102 Truncated {
104 expected: usize,
106 actual: usize,
108 },
109 InvalidGeometry(String),
111}
112
113impl std::fmt::Display for StlError {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 match self {
116 Self::InvalidFormat(message) => write!(f, "invalid STL format: {message}"),
117 Self::Truncated { expected, actual } => write!(
118 f,
119 "truncated binary STL: expected {expected} bytes, found {actual}"
120 ),
121 Self::InvalidGeometry(message) => write!(f, "invalid STL geometry: {message}"),
122 }
123 }
124}
125
126impl std::error::Error for StlError {}
127
128#[derive(Clone, Debug, PartialEq)]
130pub struct StlTriangles {
131 pub format: StlFormat,
133 pub triangles: Vec<[[f64; 3]; 3]>,
135}
136
137pub fn parse_stl_triangles(bytes: &[u8]) -> Result<StlTriangles, StlError> {
145 if bytes.is_empty() {
146 return Err(StlError::InvalidFormat("the input is empty".into()));
147 }
148
149 let binary_length = declared_binary_length(bytes);
150 let (format, triangles) = if binary_length == Some(bytes.len()) {
151 (StlFormat::Binary, parse_binary(bytes)?)
152 } else {
153 match parse_ascii(bytes) {
154 Ok(triangles) => (StlFormat::Ascii, triangles),
155 Err(ascii_error) => {
156 if let Some(expected) = binary_length {
157 if expected > bytes.len() && looks_like_binary(bytes) {
158 return Err(StlError::Truncated {
159 expected,
160 actual: bytes.len(),
161 });
162 }
163 }
164 return Err(ascii_error);
165 }
166 }
167 };
168 Ok(StlTriangles { format, triangles })
169}
170
171pub fn read_stl(data: &[u8]) -> Result<StlReadResult, String> {
174 let parsed = parse_stl_triangles(data).map_err(|error| format!("read_stl: {error}"))?;
175 let mut positions = Vec::with_capacity(parsed.triangles.len() * 9);
176 for (facet, triangle) in parsed.triangles.iter().enumerate() {
177 if triangle.iter().flatten().any(|value| !value.is_finite()) {
178 return Err(format!(
179 "read_stl: {}",
180 StlError::InvalidGeometry(format!("facet {facet} has a non-finite vertex"))
181 ));
182 }
183 positions.extend(triangle.iter().flatten());
184 }
185 Ok(StlReadResult {
186 tri_count: parsed.triangles.len() as u32,
187 positions,
188 format: parsed.format,
189 })
190}
191
192fn declared_binary_length(bytes: &[u8]) -> Option<usize> {
193 let count_bytes: [u8; 4] = bytes.get(80..84)?.try_into().ok()?;
194 let count = u32::from_le_bytes(count_bytes) as usize;
195 84_usize.checked_add(count.checked_mul(50)?)
196}
197
198fn looks_like_binary(bytes: &[u8]) -> bool {
199 bytes
200 .iter()
201 .take(84)
202 .any(|&byte| !matches!(byte, b'\t' | b'\n' | b'\r' | 0x20..=0x7e))
203}
204
205fn parse_binary(bytes: &[u8]) -> Result<Vec<[[f64; 3]; 3]>, StlError> {
206 let expected = declared_binary_length(bytes)
207 .ok_or_else(|| StlError::InvalidFormat("binary header is incomplete".into()))?;
208 if expected != bytes.len() {
209 return Err(if expected > bytes.len() {
210 StlError::Truncated {
211 expected,
212 actual: bytes.len(),
213 }
214 } else {
215 StlError::InvalidFormat(format!(
216 "binary STL has {} unexpected trailing bytes",
217 bytes.len() - expected
218 ))
219 });
220 }
221 let count = (expected - 84) / 50;
222 if count == 0 {
223 return Err(StlError::InvalidGeometry(
224 "the STL contains no triangles".into(),
225 ));
226 }
227 let mut triangles = Vec::with_capacity(count);
228 for facet in 0..count {
229 let offset = 84 + facet * 50;
230 let normal = read_binary_vec3(bytes, offset)?;
231 if !normal.iter().all(|value| value.is_finite()) {
232 return Err(StlError::InvalidGeometry(format!(
233 "facet {facet} has a non-finite normal"
234 )));
235 }
236 triangles.push([
237 read_binary_vec3(bytes, offset + 12)?,
238 read_binary_vec3(bytes, offset + 24)?,
239 read_binary_vec3(bytes, offset + 36)?,
240 ]);
241 }
242 Ok(triangles)
243}
244
245fn read_binary_vec3(bytes: &[u8], offset: usize) -> Result<[f64; 3], StlError> {
246 let component = |start: usize| -> Result<f64, StlError> {
247 let raw: [u8; 4] = bytes
248 .get(start..start + 4)
249 .and_then(|slice| slice.try_into().ok())
250 .ok_or_else(|| StlError::InvalidFormat("binary vector is incomplete".into()))?;
251 Ok(f32::from_le_bytes(raw) as f64)
252 };
253 Ok([
254 component(offset)?,
255 component(offset + 4)?,
256 component(offset + 8)?,
257 ])
258}
259
260fn parse_ascii(bytes: &[u8]) -> Result<Vec<[[f64; 3]; 3]>, StlError> {
261 let text = std::str::from_utf8(bytes)
262 .map_err(|_| StlError::InvalidFormat("ASCII STL is not valid UTF-8".into()))?;
263 let lines: Vec<(usize, &str)> = text
264 .lines()
265 .enumerate()
266 .filter_map(|(index, line)| {
267 let trimmed = line.trim();
268 (!trimmed.is_empty()).then_some((index + 1, trimmed))
269 })
270 .collect();
271 let Some(&(first_line, first_raw)) = lines.first() else {
272 return Err(StlError::InvalidFormat("the input is empty".into()));
273 };
274 let first = first_raw.strip_prefix('\u{feff}').unwrap_or(first_raw);
275 if !first
276 .split_whitespace()
277 .next()
278 .is_some_and(|token| token.eq_ignore_ascii_case("solid"))
279 {
280 return Err(StlError::InvalidFormat(format!(
281 "line {first_line}: ASCII STL must begin with 'solid'"
282 )));
283 }
284
285 let mut cursor = 1;
286 let mut triangles = Vec::new();
287 while cursor < lines.len() {
288 let (line_number, line) = lines[cursor];
289 if line
290 .split_whitespace()
291 .next()
292 .is_some_and(|token| token.eq_ignore_ascii_case("endsolid"))
293 {
294 cursor += 1;
295 if cursor != lines.len() {
296 return Err(StlError::InvalidFormat(format!(
297 "line {}: content follows 'endsolid'",
298 lines[cursor].0
299 )));
300 }
301 break;
302 }
303 parse_prefixed_vec3(line_number, line, &["facet", "normal"])?;
306 cursor += 1;
307 expect_ascii_line(&lines, cursor, &["outer", "loop"])?;
308 cursor += 1;
309 let mut vertices = [[0.0; 3]; 3];
310 for vertex in &mut vertices {
311 let &(number, source) = lines
312 .get(cursor)
313 .ok_or_else(|| StlError::InvalidFormat("ASCII STL ends inside a facet".into()))?;
314 *vertex = parse_prefixed_vec3(number, source, &["vertex"])?;
315 cursor += 1;
316 }
317 expect_ascii_line(&lines, cursor, &["endloop"])?;
318 cursor += 1;
319 expect_ascii_line(&lines, cursor, &["endfacet"])?;
320 cursor += 1;
321 triangles.push(vertices);
322 }
323 if triangles.is_empty() {
324 return Err(StlError::InvalidGeometry(
325 "the STL contains no triangles".into(),
326 ));
327 }
328 Ok(triangles)
329}
330
331fn expect_ascii_line(
332 lines: &[(usize, &str)],
333 cursor: usize,
334 expected: &[&str],
335) -> Result<(), StlError> {
336 let &(number, source) = lines
337 .get(cursor)
338 .ok_or_else(|| StlError::InvalidFormat("ASCII STL ends inside a facet".into()))?;
339 let tokens: Vec<_> = source.split_whitespace().collect();
340 if tokens.len() == expected.len()
341 && tokens
342 .iter()
343 .zip(expected)
344 .all(|(actual, expected)| actual.eq_ignore_ascii_case(expected))
345 {
346 Ok(())
347 } else {
348 Err(StlError::InvalidFormat(format!(
349 "line {number}: expected '{}'",
350 expected.join(" ")
351 )))
352 }
353}
354
355fn parse_prefixed_vec3(
356 line_number: usize,
357 source: &str,
358 prefix: &[&str],
359) -> Result<[f64; 3], StlError> {
360 let tokens: Vec<_> = source.split_whitespace().collect();
361 if tokens.len() != prefix.len() + 3
362 || !tokens[..prefix.len()]
363 .iter()
364 .zip(prefix)
365 .all(|(actual, expected)| actual.eq_ignore_ascii_case(expected))
366 {
367 return Err(StlError::InvalidFormat(format!(
368 "line {line_number}: expected '{} x y z'",
369 prefix.join(" ")
370 )));
371 }
372 let mut values = [0.0; 3];
373 for (index, token) in tokens[prefix.len()..].iter().enumerate() {
374 values[index] = token.parse::<f64>().map_err(|_| {
375 StlError::InvalidFormat(format!("line {line_number}: '{token}' is not a number"))
376 })?;
377 }
378 if !values.iter().all(|value| value.is_finite()) {
379 return Err(StlError::InvalidGeometry(format!(
380 "line {line_number}: vector contains a non-finite value"
381 )));
382 }
383 Ok(values)
384}
385
386pub fn write_ascii_stl<T, I>(name: &str, facets: I) -> String
390where
391 T: std::fmt::Display,
392 I: IntoIterator<Item = ([T; 3], [[T; 3]; 3])>,
393{
394 let mut out = format!("solid {name}\n");
395 for (normal, corners) in facets {
396 out.push_str(&format!(
397 " facet normal {} {} {}\n outer loop\n",
398 normal[0], normal[1], normal[2]
399 ));
400 for v in &corners {
401 out.push_str(&format!(" vertex {} {} {}\n", v[0], v[1], v[2]));
402 }
403 out.push_str(" endloop\n endfacet\n");
404 }
405 out.push_str(&format!("endsolid {name}\n"));
406 out
407}
408
409fn obj_face_vertex(token: &str, vertex_count: usize, line_number: usize) -> Result<u32, String> {
414 let mut parts = token.split('/');
415 let vertex_text = parts.next().unwrap_or_default();
416 if parts.count() > 2 {
417 return Err(format!(
418 "read_obj: line {line_number}: face vertex '{token}' has more than v/vt/vn parts"
419 ));
420 }
421 let raw: i64 = vertex_text.parse().map_err(|_| {
422 format!("read_obj: line {line_number}: face vertex '{token}' has no vertex index")
423 })?;
424 let resolved = match raw {
426 1.. => raw - 1,
427 0 => {
428 return Err(format!(
429 "read_obj: line {line_number}: vertex index 0 is not valid OBJ"
430 ))
431 }
432 _ => vertex_count as i64 + raw,
433 };
434 if resolved < 0 || resolved >= vertex_count as i64 {
435 return Err(format!(
436 "read_obj: line {line_number}: vertex index {raw} outside the \
437 {vertex_count} vertices defined so far"
438 ));
439 }
440 Ok(resolved as u32)
441}
442
443pub fn read_obj(text: &str) -> Result<ObjReadResult, String> {
452 let mut positions = Vec::<f64>::new();
453 let mut indices = Vec::<u32>::new();
454 for (line_index, raw_line) in text.lines().enumerate() {
455 let line_number = line_index + 1;
456 let line = raw_line.split('#').next().unwrap_or_default().trim();
458 if line.is_empty() {
459 continue;
460 }
461 let mut tokens = line.split_whitespace();
462 match tokens.next().unwrap_or_default() {
463 "v" => {
464 let mut point = [0.0f64; 3];
465 for coordinate in &mut point {
466 *coordinate = tokens
467 .next()
468 .ok_or_else(|| format!("read_obj: line {line_number}: vertex needs x y z"))?
469 .parse()
470 .map_err(|_| {
471 format!(
472 "read_obj: line {line_number}: vertex coordinate is not a number"
473 )
474 })?;
475 }
476 positions.extend(point);
479 }
480 "f" => {
481 let vertex_count = positions.len() / 3;
484 let corners = tokens
485 .map(|token| obj_face_vertex(token, vertex_count, line_number))
486 .collect::<Result<Vec<_>, _>>()?;
487 if corners.len() < 3 {
488 return Err(format!(
489 "read_obj: line {line_number}: face needs at least 3 vertices"
490 ));
491 }
492 for corner in 1..corners.len() - 1 {
493 indices.extend([corners[0], corners[corner], corners[corner + 1]]);
494 }
495 }
496 _ => {}
499 }
500 }
501 if positions.is_empty() {
505 return Err("read_obj: no vertices found — not Wavefront OBJ text?".into());
506 }
507 if indices.is_empty() {
508 return Err("read_obj: no faces found — nothing to build a mesh from".into());
509 }
510 Ok(ObjReadResult { positions, indices })
511}
512
513pub fn write_obj(mesh: &Mesh, name: &str) -> Result<String, String> {
515 mesh.validate()?;
516 let mut lines = vec![format!("# {name}"), format!("o {name}")];
517 for point in mesh.positions.chunks_exact(3) {
518 lines.push(format!("v {} {} {}", point[0], point[1], point[2]));
519 }
520 for normal in mesh.normals.chunks_exact(3) {
521 lines.push(format!("vn {} {} {}", normal[0], normal[1], normal[2]));
522 }
523 for triangle in mesh.indices.chunks_exact(3) {
524 let a = triangle[0] + 1;
525 let b = triangle[1] + 1;
526 let c = triangle[2] + 1;
527 lines.push(format!("f {a}//{a} {b}//{b} {c}//{c}"));
528 }
529 Ok(lines.join("\n") + "\n")
530}
531