Skip to main content

bunny_codec/
obj.rs

1use bunny_mesh::Triangle32;
2
3mod error;
4mod float;
5mod iter;
6
7pub use error::ObjError;
8use iter::find_record;
9pub use iter::{ObjTriangles, ObjVertices};
10
11/// A borrowed OBJ mesh view over the original text input.
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub struct ObjMesh<'a> {
14    source: &'a str,
15    vertex_count: usize,
16    face_count: usize,
17}
18
19impl<'a> ObjMesh<'a> {
20    /// Returns the borrowed OBJ source text.
21    #[must_use]
22    pub const fn source(self) -> &'a str {
23        self.source
24    }
25
26    /// Returns the number of parsed `v` records.
27    #[must_use]
28    pub const fn vertex_count(self) -> usize {
29        self.vertex_count
30    }
31
32    /// Returns the number of parsed triangular `f` records.
33    #[must_use]
34    pub const fn face_count(self) -> usize {
35        self.face_count
36    }
37
38    /// Reads the requested vertex from the borrowed source.
39    ///
40    /// This performs a linear scan from the start of the source. Use
41    /// [`ObjMesh::vertices`] for full-mesh iteration.
42    ///
43    /// # Errors
44    /// Returns `ObjError::IndexOutOfBounds` if the index is out of range.
45    pub fn vertex(self, index: usize) -> Result<ObjVertex, ObjError> {
46        find_record(self.source, "v", index)
47            .ok_or(ObjError::IndexOutOfBounds)
48            .and_then(parse_vertex_line)
49    }
50
51    /// Reads the requested triangle from the borrowed source.
52    ///
53    /// This performs a linear scan from the start of the source. Use
54    /// [`ObjMesh::triangles`] for full-mesh iteration.
55    ///
56    /// # Errors
57    /// Returns `ObjError::IndexOutOfBounds` if the index is out of range,
58    /// or another `ObjError` if the face record is invalid.
59    pub fn triangle(self, index: usize) -> Result<Triangle32, ObjError> {
60        find_record(self.source, "f", index)
61            .ok_or(ObjError::IndexOutOfBounds)
62            .and_then(parse_face_line)
63    }
64
65    /// Returns a forward iterator over decoded OBJ vertex records.
66    ///
67    /// The iterator scans the borrowed source once and does not allocate.
68    #[must_use]
69    pub fn vertices(self) -> ObjVertices<'a> {
70        ObjVertices::new(self.source)
71    }
72
73    /// Returns a forward iterator over decoded OBJ triangle face records.
74    ///
75    /// The iterator scans the borrowed source once and does not allocate.
76    #[must_use]
77    pub fn triangles(self) -> ObjTriangles<'a> {
78        ObjTriangles::new(self.source, self.vertex_count)
79    }
80}
81
82/// A text OBJ vertex decoded from a borrowed `v` record.
83#[derive(Clone, Copy, Debug, PartialEq)]
84pub struct ObjVertex {
85    /// X coordinate.
86    pub x: f32,
87    /// Y coordinate.
88    pub y: f32,
89    /// Z coordinate.
90    pub z: f32,
91}
92
93/// Parses a supported OBJ text mesh as a zero-copy borrowed view.
94///
95/// The supported profile accepts `v x y z` vertices, triangular `f` faces, and
96/// common metadata statements. Face indices are converted from OBJ one-based
97/// indexing into zero-based `Triangle32` values.
98///
99/// # Errors
100/// Returns `ObjError` when the source contains unsupported statements,
101/// malformed vertices, malformed faces, or out-of-range face indices.
102pub fn parse_obj_text(source: &str) -> Result<ObjMesh<'_>, ObjError> {
103    let counts = count_records(source)?;
104    if counts.vertex_count == 0 {
105        return Err(ObjError::MissingVertices);
106    }
107    if counts.face_count == 0 {
108        return Err(ObjError::MissingFaces);
109    }
110    validate_face_indices(source, counts.vertex_count)?;
111    Ok(ObjMesh {
112        source,
113        vertex_count: counts.vertex_count,
114        face_count: counts.face_count,
115    })
116}
117
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119struct ObjCounts {
120    vertex_count: usize,
121    face_count: usize,
122}
123
124fn count_records(source: &str) -> Result<ObjCounts, ObjError> {
125    let mut vertex_count = 0;
126    let mut face_count = 0;
127    for line in source.lines() {
128        match statement_kind(line)? {
129            Some("v") => {
130                parse_vertex_line(line)?;
131                vertex_count += 1;
132            }
133            Some("f") => {
134                parse_face_line(line)?;
135                face_count += 1;
136            }
137            Some(_) | None => {}
138        }
139    }
140    Ok(ObjCounts {
141        vertex_count,
142        face_count,
143    })
144}
145
146fn validate_face_indices(source: &str, vertex_count: usize) -> Result<(), ObjError> {
147    for line in source.lines() {
148        if statement_kind(line)? == Some("f") {
149            let triangle = parse_face_line(line)?;
150            if !face_indices_are_valid(triangle, vertex_count) {
151                return Err(ObjError::IndexOutOfBounds);
152            }
153        }
154    }
155    Ok(())
156}
157
158fn face_indices_are_valid(face: Triangle32, vertex_count: usize) -> bool {
159    u32_index_is_valid(face.v0, vertex_count)
160        && u32_index_is_valid(face.v1, vertex_count)
161        && u32_index_is_valid(face.v2, vertex_count)
162}
163
164fn u32_index_is_valid(index: u32, vertex_count: usize) -> bool {
165    usize::try_from(index).is_ok_and(|index| index < vertex_count)
166}
167
168fn statement_kind(line: &str) -> Result<Option<&str>, ObjError> {
169    let mut parts = record_body(line).split_whitespace();
170    let Some(kind) = parts.next() else {
171        return Ok(None);
172    };
173    if kind.starts_with('#') || harmless_statement(kind) {
174        Ok(None)
175    } else if matches!(kind, "v" | "f") {
176        Ok(Some(kind))
177    } else {
178        Err(ObjError::UnsupportedStatement)
179    }
180}
181
182fn harmless_statement(kind: &str) -> bool {
183    matches!(kind, "o" | "g" | "s" | "usemtl" | "mtllib" | "vt" | "vn")
184}
185
186fn record_body(line: &str) -> &str {
187    line.split_once('#')
188        .map_or(line, |(record, _comment)| record)
189        .trim()
190}
191
192fn parse_vertex_line(line: &str) -> Result<ObjVertex, ObjError> {
193    let mut parts = record_body(line).split_whitespace();
194    if parts.next() != Some("v") {
195        return Err(ObjError::InvalidVertex);
196    }
197    let vertex = ObjVertex {
198        x: parse_coord(parts.next())?,
199        y: parse_coord(parts.next())?,
200        z: parse_coord(parts.next())?,
201    };
202    if !(vertex.x.is_finite() && vertex.y.is_finite() && vertex.z.is_finite()) {
203        return Err(ObjError::NonFiniteVertex);
204    }
205    if parts.next().is_some() {
206        Err(ObjError::InvalidVertex)
207    } else {
208        Ok(vertex)
209    }
210}
211
212fn parse_coord(value: Option<&str>) -> Result<f32, ObjError> {
213    float::parse_ascii_float(value.ok_or(ObjError::InvalidVertex)?).ok_or(ObjError::InvalidVertex)
214}
215
216fn parse_face_line(line: &str) -> Result<Triangle32, ObjError> {
217    let mut parts = record_body(line).split_whitespace();
218    if parts.next() != Some("f") {
219        return Err(ObjError::NonTriangularFace);
220    }
221    let face = Triangle32::new(
222        parse_index(parts.next())?,
223        parse_index(parts.next())?,
224        parse_index(parts.next())?,
225    );
226    if parts.next().is_some() {
227        Err(ObjError::NonTriangularFace)
228    } else {
229        Ok(face)
230    }
231}
232
233fn parse_index(token: Option<&str>) -> Result<u32, ObjError> {
234    let token = token.ok_or(ObjError::NonTriangularFace)?;
235    let mut fields = token.split('/');
236    let vertex_index = parse_vertex_index(fields.next().ok_or(ObjError::InvalidIndex)?)?;
237    match (fields.next(), fields.next(), fields.next()) {
238        (None, None, None) => Ok(vertex_index),
239        (Some(texture), None, None) => {
240            parse_auxiliary_index(texture)?;
241            Ok(vertex_index)
242        }
243        (Some(texture), Some(normal), None) => {
244            if !texture.is_empty() {
245                parse_auxiliary_index(texture)?;
246            }
247            parse_auxiliary_index(normal)?;
248            Ok(vertex_index)
249        }
250        _ => Err(ObjError::InvalidIndex),
251    }
252}
253
254fn parse_vertex_index(index_text: &str) -> Result<u32, ObjError> {
255    let one_based = index_text
256        .parse::<i64>()
257        .map_err(|_| ObjError::InvalidIndex)?;
258    let zero_based = one_based.checked_sub(1).ok_or(ObjError::InvalidIndex)?;
259    u32::try_from(zero_based).map_err(|_| ObjError::InvalidIndex)
260}
261
262fn parse_auxiliary_index(index_text: &str) -> Result<(), ObjError> {
263    let one_based = index_text
264        .parse::<i64>()
265        .map_err(|_| ObjError::InvalidIndex)?;
266    let zero_based = one_based.checked_sub(1).ok_or(ObjError::InvalidIndex)?;
267    u32::try_from(zero_based)
268        .map(|_| ())
269        .map_err(|_| ObjError::InvalidIndex)
270}