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}
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
32/// Write the kernel mesh as binary STL.
33pub 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
90/// Parse binary STL triangle positions for round-trip and import validation.
91pub 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
121/// Resolve one OBJ face-vertex reference (`v`, `v/vt`, `v//vn`, `v/vt/vn`)
122/// to a 0-based position index. OBJ indices are 1-based; negative values
123/// count back from the vertices defined so far, which lets exporters emit
124/// faces before the vertex table is complete.
125fn 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    // Index 0 does not exist in either the 1-based or the relative scheme.
137    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
155/// Parse Wavefront OBJ text into flat positions and triangle indices.
156///
157/// Faces with more than three vertices fan-triangulate so the result is
158/// always a triangle list (exported polygons are overwhelmingly convex, and
159/// the faceted importer welds and repairs downstream anyway). Texture and
160/// normal references, grouping state, and material lines carry nothing the
161/// kernel mesh keeps, so they are skipped; unknown keywords are OBJ
162/// extensions, not errors.
163pub 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        // '#' starts a comment; it may follow data on the same line.
169        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                // A fourth value (w) or vertex-color extension may follow;
189                // only the position matters here.
190                positions.extend(point);
191            }
192            "f" => {
193                // Negative indices resolve against the vertices defined
194                // BEFORE this face line, so count them first.
195                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            // vn/vt/vp geometry the mesh does not keep, o/g/s grouping,
209            // usemtl/mtllib materials, and vendor extensions.
210            _ => {}
211        }
212    }
213    // An input with no geometry at all is far more likely a wrong-format
214    // file than an intentionally empty model; say so instead of returning
215    // an empty mesh that only fails further down the import chain.
216    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
225/// Write Wavefront OBJ positions, normals, and indexed triangle faces.
226pub 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// BREP private tests: bcf4c69420da6504