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#[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 #[must_use]
22 pub const fn source(self) -> &'a str {
23 self.source
24 }
25
26 #[must_use]
28 pub const fn vertex_count(self) -> usize {
29 self.vertex_count
30 }
31
32 #[must_use]
34 pub const fn face_count(self) -> usize {
35 self.face_count
36 }
37
38 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 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 #[must_use]
69 pub fn vertices(self) -> ObjVertices<'a> {
70 ObjVertices::new(self.source)
71 }
72
73 #[must_use]
77 pub fn triangles(self) -> ObjTriangles<'a> {
78 ObjTriangles::new(self.source, self.vertex_count)
79 }
80}
81
82#[derive(Clone, Copy, Debug, PartialEq)]
84pub struct ObjVertex {
85 pub x: f32,
87 pub y: f32,
89 pub z: f32,
91}
92
93pub 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 { source, vertex_count: counts.vertex_count, face_count: counts.face_count })
112}
113
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115struct ObjCounts {
116 vertex_count: usize,
117 face_count: usize,
118}
119
120fn count_records(source: &str) -> Result<ObjCounts, ObjError> {
121 let mut vertex_count = 0;
122 let mut face_count = 0;
123 for line in source.lines() {
124 match statement_kind(line)? {
125 Some("v") => {
126 parse_vertex_line(line)?;
127 vertex_count += 1;
128 }
129 Some("f") => {
130 parse_face_line(line)?;
131 face_count += 1;
132 }
133 Some(_) | None => {}
134 }
135 }
136 Ok(ObjCounts { vertex_count, face_count })
137}
138
139fn validate_face_indices(source: &str, vertex_count: usize) -> Result<(), ObjError> {
140 for line in source.lines() {
141 if statement_kind(line)? == Some("f") {
142 let triangle = parse_face_line(line)?;
143 if !face_indices_are_valid(triangle, vertex_count) {
144 return Err(ObjError::IndexOutOfBounds);
145 }
146 }
147 }
148 Ok(())
149}
150
151fn face_indices_are_valid(face: Triangle32, vertex_count: usize) -> bool {
152 u32_index_is_valid(face.v0, vertex_count)
153 && u32_index_is_valid(face.v1, vertex_count)
154 && u32_index_is_valid(face.v2, vertex_count)
155}
156
157fn u32_index_is_valid(index: u32, vertex_count: usize) -> bool {
158 usize::try_from(index).is_ok_and(|index| index < vertex_count)
159}
160
161fn statement_kind(line: &str) -> Result<Option<&str>, ObjError> {
162 let mut parts = record_body(line).split_whitespace();
163 let Some(kind) = parts.next() else {
164 return Ok(None);
165 };
166 if kind.starts_with('#') || harmless_statement(kind) {
167 Ok(None)
168 } else if matches!(kind, "v" | "f") {
169 Ok(Some(kind))
170 } else {
171 Err(ObjError::UnsupportedStatement)
172 }
173}
174
175fn harmless_statement(kind: &str) -> bool {
176 matches!(kind, "o" | "g" | "s" | "usemtl" | "mtllib" | "vt" | "vn")
177}
178
179fn record_body(line: &str) -> &str {
180 line.split_once('#').map_or(line, |(record, _comment)| record).trim()
181}
182
183fn parse_vertex_line(line: &str) -> Result<ObjVertex, ObjError> {
184 let mut parts = record_body(line).split_whitespace();
185 if parts.next() != Some("v") {
186 return Err(ObjError::InvalidVertex);
187 }
188 let vertex = ObjVertex {
189 x: parse_coord(parts.next())?,
190 y: parse_coord(parts.next())?,
191 z: parse_coord(parts.next())?,
192 };
193 if !(vertex.x.is_finite() && vertex.y.is_finite() && vertex.z.is_finite()) {
194 return Err(ObjError::NonFiniteVertex);
195 }
196 if parts.next().is_some() {
197 Err(ObjError::InvalidVertex)
198 } else {
199 Ok(vertex)
200 }
201}
202
203fn parse_coord(value: Option<&str>) -> Result<f32, ObjError> {
204 float::parse_ascii_float(value.ok_or(ObjError::InvalidVertex)?).ok_or(ObjError::InvalidVertex)
205}
206
207fn parse_face_line(line: &str) -> Result<Triangle32, ObjError> {
208 let mut parts = record_body(line).split_whitespace();
209 if parts.next() != Some("f") {
210 return Err(ObjError::NonTriangularFace);
211 }
212 let face = Triangle32::new(
213 parse_index(parts.next())?,
214 parse_index(parts.next())?,
215 parse_index(parts.next())?,
216 );
217 if parts.next().is_some() {
218 Err(ObjError::NonTriangularFace)
219 } else {
220 Ok(face)
221 }
222}
223
224fn parse_index(token: Option<&str>) -> Result<u32, ObjError> {
225 let token = token.ok_or(ObjError::NonTriangularFace)?;
226 let mut fields = token.split('/');
227 let vertex_index = parse_vertex_index(fields.next().ok_or(ObjError::InvalidIndex)?)?;
228 match (fields.next(), fields.next(), fields.next()) {
229 (None, None, None) => Ok(vertex_index),
230 (Some(texture), None, None) => {
231 parse_auxiliary_index(texture)?;
232 Ok(vertex_index)
233 }
234 (Some(texture), Some(normal), None) => {
235 if !texture.is_empty() {
236 parse_auxiliary_index(texture)?;
237 }
238 parse_auxiliary_index(normal)?;
239 Ok(vertex_index)
240 }
241 _ => Err(ObjError::InvalidIndex),
242 }
243}
244
245fn parse_vertex_index(index_text: &str) -> Result<u32, ObjError> {
246 let one_based = index_text.parse::<i64>().map_err(|_| ObjError::InvalidIndex)?;
247 let zero_based = one_based.checked_sub(1).ok_or(ObjError::InvalidIndex)?;
248 u32::try_from(zero_based).map_err(|_| ObjError::InvalidIndex)
249}
250
251fn parse_auxiliary_index(index_text: &str) -> Result<(), ObjError> {
252 let one_based = index_text.parse::<i64>().map_err(|_| ObjError::InvalidIndex)?;
253 let zero_based = one_based.checked_sub(1).ok_or(ObjError::InvalidIndex)?;
254 u32::try_from(zero_based).map(|_| ()).map_err(|_| ObjError::InvalidIndex)
255}