Skip to main content

brep_kernel/io/
mesh_io.rs

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    /// Encoding the bytes were read as (`"binary"` or `"ascii"` in JSON).
9    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
34/// Write the kernel mesh as binary STL.
35pub 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/// Encoding detected while reading an STL.
79#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
80#[serde(rename_all = "lowercase")]
81pub enum StlFormat {
82    /// The 80-byte-header binary STL encoding.
83    Binary,
84    /// The line-oriented ASCII STL encoding.
85    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/// Why STL bytes are not a readable STL.
98#[derive(Clone, Debug, PartialEq, Eq)]
99pub enum StlError {
100    /// The byte stream is neither a valid binary nor a valid ASCII STL.
101    InvalidFormat(String),
102    /// The STL ends before all declared data is present.
103    Truncated {
104        /// Total byte count required by the binary facet count.
105        expected: usize,
106        /// Actual byte count supplied.
107        actual: usize,
108    },
109    /// A numeric value or geometric facet is invalid.
110    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/// The facets of an STL in file order, before any welding.
129#[derive(Clone, Debug, PartialEq)]
130pub struct StlTriangles {
131    /// Encoding detected from the bytes.
132    pub format: StlFormat,
133    /// Each facet's three corners, in file order and winding.
134    pub triangles: Vec<[[f64; 3]; 3]>,
135}
136
137/// Parse binary or ASCII STL bytes into their facets.
138///
139/// Binary structure is checked before the leading `solid` token because
140/// binary STL headers are arbitrary and commonly begin with that word. A
141/// binary file must hold exactly the bytes its facet count declares; a file
142/// with no facets, or a facet normal or ASCII value that is not finite, is
143/// refused. Corner finiteness of binary facets is the caller's check.
144pub 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
171/// Read binary or ASCII STL bytes ([`parse_stl_triangles`]) as a flat
172/// triangle soup, refusing a corner that is not finite.
173pub 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 refuses a non-finite value, the facet
304        // normal's included.
305        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
386/// Write ASCII STL: `solid {name}`, one facet per `(normal, corners)` item,
387/// `endsolid {name}`. Values print with their own `Display`, so an `f32`
388/// source keeps its shortest `f32` text.
389pub 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
409/// Resolve one OBJ face-vertex reference (`v`, `v/vt`, `v//vn`, `v/vt/vn`)
410/// to a 0-based position index. OBJ indices are 1-based; negative values
411/// count back from the vertices defined so far, which lets exporters emit
412/// faces before the vertex table is complete.
413fn 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    // Index 0 does not exist in either the 1-based or the relative scheme.
425    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
443/// Parse Wavefront OBJ text into flat positions and triangle indices.
444///
445/// Faces with more than three vertices fan-triangulate so the result is
446/// always a triangle list (exported polygons are overwhelmingly convex, and
447/// the faceted importer welds and repairs downstream anyway). Texture and
448/// normal references, grouping state, and material lines carry nothing the
449/// kernel mesh keeps, so they are skipped; unknown keywords are OBJ
450/// extensions, not errors.
451pub 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        // '#' starts a comment; it may follow data on the same line.
457        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                // A fourth value (w) or vertex-color extension may follow;
477                // only the position matters here.
478                positions.extend(point);
479            }
480            "f" => {
481                // Negative indices resolve against the vertices defined
482                // BEFORE this face line, so count them first.
483                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            // vn/vt/vp geometry the mesh does not keep, o/g/s grouping,
497            // usemtl/mtllib materials, and vendor extensions.
498            _ => {}
499        }
500    }
501    // An input with no geometry at all is far more likely a wrong-format
502    // file than an intentionally empty model; say so instead of returning
503    // an empty mesh that only fails further down the import chain.
504    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
513/// Write Wavefront OBJ positions, normals, and indexed triangle faces.
514pub 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