1use crate::traits::finalize_mesh;
6use std::fs;
7use std::io::{self, BufRead, BufReader, Cursor};
8use std::path::Path;
9
10use crate::mesh_weld::CornerWeld;
11use crate::raw_attribute::{make_f32x2_attribute, make_f32x3_attribute};
12use draco_core::geometry_attribute::GeometryAttributeType;
13use draco_core::mesh::Mesh;
14
15use crate::traits::{PointCloudReader, ReadFromBytes, Reader};
16
17#[derive(Debug)]
21pub struct ObjReader {
22 source: ObjReaderSource,
23}
24
25#[derive(Debug, Clone)]
26enum ObjReaderSource {
27 Path(std::path::PathBuf),
28 Bytes(Vec<u8>),
29}
30
31impl ObjReader {
32 pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
34 let path = path.as_ref().to_path_buf();
35 if !path.exists() {
36 return Err(io::Error::new(
37 io::ErrorKind::NotFound,
38 format!("File not found: {}", path.display()),
39 ));
40 }
41 Ok(Self {
42 source: ObjReaderSource::Path(path),
43 })
44 }
45
46 pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Self {
48 Self {
49 source: ObjReaderSource::Bytes(bytes.into()),
50 }
51 }
52
53 pub fn read_from_bytes(bytes: &[u8]) -> io::Result<Mesh> {
55 let mut reader = Self::from_bytes(bytes.to_vec());
56 reader.read_mesh()
57 }
58
59 pub fn read_positions(&mut self) -> io::Result<Vec<[f32; 3]>> {
61 match &self.source {
62 ObjReaderSource::Path(path) => read_obj_positions(path),
63 ObjReaderSource::Bytes(bytes) => {
64 read_obj_positions_from_reader(BufReader::new(Cursor::new(bytes.as_slice())))
65 }
66 }
67 }
68
69 fn read_mesh_data(&self) -> io::Result<ParsedObjMesh> {
71 match &self.source {
72 ObjReaderSource::Path(path) => {
73 let file = fs::File::open(path)?;
74 read_obj_mesh_from_reader(BufReader::new(file))
75 }
76 ObjReaderSource::Bytes(bytes) => {
77 read_obj_mesh_from_reader(BufReader::new(Cursor::new(bytes.as_slice())))
78 }
79 }
80 }
81
82 pub fn read_mesh(&mut self) -> io::Result<Mesh> {
88 let parsed = self.read_mesh_data()?;
89 let mut mesh = Mesh::new();
90
91 if parsed.positions.is_empty() {
92 return Ok(mesh);
93 }
94
95 mesh.set_num_points(parsed.positions.len());
96 mesh.set_num_faces(parsed.faces.len());
97
98 mesh.add_attribute(make_f32x3_attribute(
99 GeometryAttributeType::Position,
100 &parsed.positions,
101 ));
102
103 if let Some(normals) = parsed.normals.as_ref() {
104 mesh.add_attribute(make_f32x3_attribute(GeometryAttributeType::Normal, normals));
105 }
106
107 if let Some(texcoords) = parsed.texcoords.as_ref() {
108 mesh.add_attribute(make_f32x2_attribute(
109 GeometryAttributeType::TexCoord,
110 texcoords,
111 ));
112 }
113
114 use draco_core::geometry_indices::{FaceIndex, PointIndex};
116 for (i, face) in parsed.faces.iter().enumerate() {
117 mesh.set_face(
118 FaceIndex(i as u32),
119 [
120 PointIndex(face[0]),
121 PointIndex(face[1]),
122 PointIndex(face[2]),
123 ],
124 );
125 }
126
127 finalize_mesh(&mut mesh)?;
133
134 Ok(mesh)
135 }
136}
137
138impl Reader for ObjReader {
139 fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
140 ObjReader::open(path)
141 }
142
143 fn read_meshes(&mut self) -> io::Result<Vec<Mesh>> {
144 let m = self.read_mesh()?;
145 Ok(vec![m])
146 }
147}
148
149impl ReadFromBytes for ObjReader {
150 fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
151 Ok(Self::from_bytes(bytes.to_vec()))
152 }
153}
154
155impl PointCloudReader for ObjReader {
156 fn read_points(&mut self) -> io::Result<Vec<[f32; 3]>> {
157 self.read_positions()
158 }
159}
160
161pub fn read_obj_positions<P: AsRef<Path>>(path: P) -> io::Result<Vec<[f32; 3]>> {
168 let file = fs::File::open(path)?;
169 let reader = BufReader::new(file);
170 read_obj_positions_from_reader(reader)
171}
172
173fn read_obj_positions_from_reader<R: BufRead>(reader: R) -> io::Result<Vec<[f32; 3]>> {
174 let mut positions = Vec::new();
175
176 for line in reader.lines() {
177 let line = line?;
178 let trimmed = strip_obj_comment(&line);
179
180 if trimmed.starts_with("vn ") || trimmed.starts_with("vt ") {
181 continue;
182 }
183
184 let mut parts = trimmed.split_whitespace();
185 if parts.next() != Some("v") {
186 continue;
187 }
188
189 let x = parts.next().and_then(|s| s.parse().ok());
190 let y = parts.next().and_then(|s| s.parse().ok());
191 let z = parts.next().and_then(|s| s.parse().ok());
192
193 match (x, y, z) {
196 (Some(x), Some(y), Some(z)) => positions.push([x, y, z]),
197 _ => return Err(invalid_obj("OBJ vertex line must have three numbers")),
198 }
199 }
200
201 Ok(positions)
202}
203
204#[derive(Debug)]
205struct ParsedObjMesh {
206 positions: Vec<[f32; 3]>,
207 texcoords: Option<Vec<[f32; 2]>>,
208 normals: Option<Vec<[f32; 3]>>,
209 faces: Vec<[u32; 3]>,
210}
211
212#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
213struct ObjVertexRef {
214 position: usize,
215 texcoord: Option<usize>,
216 normal: Option<usize>,
217}
218
219fn strip_obj_comment(line: &str) -> &str {
220 line.split('#').next().unwrap_or("").trim()
221}
222
223fn parse_obj_index(token: &str, len: usize, label: &str) -> io::Result<usize> {
224 let raw = token
225 .parse::<i32>()
226 .map_err(|_| invalid_obj(format!("Bad {label} index: {token}")))?;
227 if raw == 0 {
228 return Err(invalid_obj(format!("OBJ {label} indices are 1-based")));
229 }
230
231 let index = if raw > 0 { raw - 1 } else { len as i32 + raw };
232
233 if index < 0 || index as usize >= len {
234 return Err(invalid_obj(format!(
235 "OBJ {label} index {raw} is out of range for {len} values"
236 )));
237 }
238
239 Ok(index as usize)
240}
241
242fn parse_face_vertex(
243 token: &str,
244 position_count: usize,
245 texcoord_count: usize,
246 normal_count: usize,
247) -> io::Result<ObjVertexRef> {
248 let parts: Vec<&str> = token.split('/').collect();
249 if parts.is_empty() || parts.len() > 3 || parts[0].is_empty() {
250 return Err(invalid_obj(format!("Bad OBJ face vertex: {token}")));
251 }
252
253 let position = parse_obj_index(parts[0], position_count, "position")?;
254 let texcoord = if parts.get(1).is_some_and(|part| !part.is_empty()) {
255 Some(parse_obj_index(parts[1], texcoord_count, "texcoord")?)
256 } else {
257 None
258 };
259 let normal = if parts.get(2).is_some_and(|part| !part.is_empty()) {
260 Some(parse_obj_index(parts[2], normal_count, "normal")?)
261 } else {
262 None
263 };
264
265 Ok(ObjVertexRef {
266 position,
267 texcoord,
268 normal,
269 })
270}
271
272fn push_obj_vertex(
280 vertex_ref: ObjVertexRef,
281 weld: &mut CornerWeld<ObjVertexRef>,
282 vertices: &mut Vec<ObjVertexRef>,
283) -> u32 {
284 let (point_id, is_new) = weld.intern(vertex_ref);
285 if is_new {
286 vertices.push(vertex_ref);
287 }
288 point_id
289}
290
291fn invalid_obj(message: impl Into<String>) -> io::Error {
292 io::Error::new(io::ErrorKind::InvalidData, message.into())
293}
294
295fn read_obj_mesh_from_reader<R: BufRead>(reader: R) -> io::Result<ParsedObjMesh> {
296 let mut source_positions = Vec::new();
297 let mut source_texcoords = Vec::new();
298 let mut source_normals = Vec::new();
299 let mut faces = Vec::new();
300 let mut vertices = Vec::new();
301 let mut weld: CornerWeld<ObjVertexRef> = CornerWeld::with_capacity(0);
304
305 for line in reader.lines() {
306 let line = line?;
307 let trimmed = strip_obj_comment(&line);
308
309 let mut parts = trimmed.split_whitespace();
314 let keyword = parts.next();
315
316 if keyword == Some("v") {
317 let x = parts.next().and_then(|s| s.parse().ok());
323 let y = parts.next().and_then(|s| s.parse().ok());
324 let z = parts.next().and_then(|s| s.parse().ok());
325
326 match (x, y, z) {
327 (Some(x), Some(y), Some(z)) => source_positions.push([x, y, z]),
328 _ => return Err(invalid_obj("OBJ vertex line must have three numbers")),
329 }
330 } else if keyword == Some("vt") {
331 let u = parts.next().and_then(|s| s.parse().ok());
332 let v = parts.next().and_then(|s| s.parse().ok());
333
334 match (u, v) {
335 (Some(u), Some(v)) => source_texcoords.push([u, v]),
336 _ => {
337 return Err(invalid_obj(
338 "OBJ texture coordinate line must have two numbers",
339 ))
340 }
341 }
342 } else if keyword == Some("vn") {
343 let x = parts.next().and_then(|s| s.parse().ok());
344 let y = parts.next().and_then(|s| s.parse().ok());
345 let z = parts.next().and_then(|s| s.parse().ok());
346
347 match (x, y, z) {
348 (Some(x), Some(y), Some(z)) => source_normals.push([x, y, z]),
349 _ => return Err(invalid_obj("OBJ normal line must have three numbers")),
350 }
351 } else if keyword == Some("f") {
352 let face_vertices = parts
353 .map(|part| {
354 parse_face_vertex(
355 part,
356 source_positions.len(),
357 source_texcoords.len(),
358 source_normals.len(),
359 )
360 })
361 .collect::<io::Result<Vec<_>>>()?;
362 if face_vertices.len() < 3 {
363 continue;
364 }
365
366 for i in 1..face_vertices.len() - 1 {
367 let triangle = [face_vertices[0], face_vertices[i], face_vertices[i + 1]];
368 faces.push([
369 push_obj_vertex(triangle[0], &mut weld, &mut vertices),
370 push_obj_vertex(triangle[1], &mut weld, &mut vertices),
371 push_obj_vertex(triangle[2], &mut weld, &mut vertices),
372 ]);
373 }
374 }
375 }
376
377 if faces.is_empty() {
378 return Ok(ParsedObjMesh {
379 positions: source_positions,
380 texcoords: None,
381 normals: None,
382 faces,
383 });
384 }
385
386 let uses_texcoords = vertices.iter().any(|vertex| vertex.texcoord.is_some());
387 let uses_normals = vertices.iter().any(|vertex| vertex.normal.is_some());
388 if uses_texcoords && vertices.iter().any(|vertex| vertex.texcoord.is_none()) {
389 return Err(invalid_obj(
390 "OBJ texture coordinate indices must be present on every face vertex",
391 ));
392 }
393 if uses_normals && vertices.iter().any(|vertex| vertex.normal.is_none()) {
394 return Err(invalid_obj(
395 "OBJ normal indices must be present on every face vertex",
396 ));
397 }
398
399 let positions = vertices
400 .iter()
401 .map(|vertex| source_positions[vertex.position])
402 .collect();
403 let texcoords = uses_texcoords.then(|| {
404 vertices
405 .iter()
406 .map(|vertex| source_texcoords[vertex.texcoord.unwrap()])
407 .collect()
408 });
409 let normals = uses_normals.then(|| {
410 vertices
411 .iter()
412 .map(|vertex| source_normals[vertex.normal.unwrap()])
413 .collect()
414 });
415
416 Ok(ParsedObjMesh {
417 positions,
418 texcoords,
419 normals,
420 faces,
421 })
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427 use std::io::Write;
428 use tempfile::NamedTempFile;
429
430 #[test]
431 fn test_read_obj_positions() {
432 let mut file = NamedTempFile::new().unwrap();
433 writeln!(file, "# comment").unwrap();
434 writeln!(file, "v 1.0 2.0 3.0").unwrap();
435 writeln!(file, "v 4.5 5.5 6.5").unwrap();
436 writeln!(file, "vn 0 1 0").unwrap();
437 writeln!(file, "vt 0.5 0.5").unwrap();
438 writeln!(file, "v -1.0 -2.0 -3.0").unwrap();
439 file.flush().unwrap();
440
441 let positions = read_obj_positions(file.path()).unwrap();
442 assert_eq!(positions.len(), 3);
443 assert_eq!(positions[0], [1.0, 2.0, 3.0]);
444 assert_eq!(positions[1], [4.5, 5.5, 6.5]);
445 assert_eq!(positions[2], [-1.0, -2.0, -3.0]);
446 }
447
448 #[test]
449 fn test_read_obj_mesh_preserves_normals_and_texcoords() {
450 use draco_core::draco_types::DataType;
451 let obj = br#"
452v 0.0 0.0 0.0
453v 1.0 0.0 0.0
454v 1.0 1.0 0.0
455v 0.0 1.0 0.0
456vt 0.0 0.0
457vt 1.0 0.0
458vt 1.0 1.0
459vt 0.0 1.0
460vn 0.0 0.0 1.0
461f 1/1/1 2/2/1 3/3/1 4/4/1
462"#;
463
464 let mut reader = ObjReader::from_bytes(obj.as_slice());
465 let mesh = reader.read_mesh().unwrap();
466
467 assert_eq!(mesh.num_points(), 4);
468 assert_eq!(mesh.num_faces(), 2);
469
470 let normal_att = mesh.named_attribute(GeometryAttributeType::Normal).unwrap();
471 assert_eq!(normal_att.num_components(), 3);
472 assert_eq!(normal_att.data_type(), DataType::Float32);
473
474 let texcoord_att = mesh
475 .named_attribute(GeometryAttributeType::TexCoord)
476 .unwrap();
477 assert_eq!(texcoord_att.num_components(), 2);
478 assert_eq!(texcoord_att.data_type(), DataType::Float32);
479 assert_eq!(texcoord_att.buffer().data().len(), 4 * 2 * 4);
480 }
481}