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 {
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}