1use crate::error::{DagError, DagResult};
8use crate::types::IndexedGeometry;
9
10pub fn parse_obj(text: &str) -> DagResult<IndexedGeometry> {
15 let mut positions: Vec<f32> = Vec::new();
16 let mut raw_faces: Vec<[usize; 3]> = Vec::new();
17
18 for (line_no, raw_line) in text.lines().enumerate() {
19 let line_no = line_no + 1;
20 let line = raw_line.trim();
21 if line.is_empty() || line.starts_with('#') {
22 continue;
23 }
24 let mut tokens = line.split_whitespace();
25 let tag = tokens.next().unwrap_or_default();
26 match tag {
27 "v" => {
28 let coords: Vec<f32> = tokens
29 .map(|t| parse_f32(t, line_no))
30 .collect::<DagResult<_>>()?;
31 if coords.len() < 3 {
32 return Err(DagError::ObjParse {
33 line: line_no,
34 reason: format!("vertex needs 3 (optionally 4 with w) components, got {}", coords.len()),
35 });
36 }
37 if !coords[..3].iter().all(|v| v.is_finite()) {
38 return Err(DagError::ObjParse { line: line_no, reason: "vertex component must be finite".into() });
39 }
40 positions.extend_from_slice(&coords[..3]);
41 }
42 "f" => {
43 let indices: Vec<usize> = tokens
44 .map(|t| parse_face_vertex(t, line_no, positions.len() / 3))
45 .collect::<DagResult<_>>()?;
46 if indices.len() != 3 {
47 return Err(DagError::ObjParse {
48 line: line_no,
49 reason: format!("only triangle faces are supported, got {} vertices", indices.len()),
50 });
51 }
52 raw_faces.push([indices[0], indices[1], indices[2]]);
53 }
54 _ => {}
56 }
57 }
58
59 let mut indices = Vec::with_capacity(raw_faces.len() * 3);
60 for (face_no, face) in raw_faces.iter().enumerate() {
61 for &vertex in face {
62 if vertex >= positions.len() / 3 {
63 return Err(DagError::ObjParse {
64 line: 0,
65 reason: format!("face {} references vertex {} beyond {} defined", face_no + 1, vertex + 1, positions.len() / 3),
66 });
67 }
68 indices.push(vertex as u32);
69 }
70 }
71 Ok(IndexedGeometry { positions, indices })
72}
73
74fn parse_f32(token: &str, line_no: usize) -> DagResult<f32> {
76 token
77 .parse::<f32>()
78 .map_err(|_| DagError::ObjParse { line: line_no, reason: format!("invalid number: {token:?}") })
79}
80
81fn parse_face_vertex(token: &str, line_no: usize, vertex_count: usize) -> DagResult<usize> {
84 let vertex_str = token.split('/').next().unwrap_or_default();
85 let raw: i64 = vertex_str
86 .parse::<i64>()
87 .map_err(|_| DagError::ObjParse { line: line_no, reason: format!("invalid face index: {token:?}") })?;
88 if raw == 0 {
89 return Err(DagError::ObjParse { line: line_no, reason: "face index 0 is not valid OBJ".into() });
90 }
91 if raw > 0 {
92 Ok((raw - 1) as usize)
93 } else {
94 if vertex_count == 0 {
95 return Err(DagError::ObjParse { line: line_no, reason: "negative face index before any vertex".into() });
96 }
97 let resolved = vertex_count as i64 + raw;
98 Ok(resolved as usize)
99 }
100}
101
102#[must_use]
104pub fn write_obj(geometry: &IndexedGeometry, comment: &str) -> String {
105 let mut out = String::with_capacity(geometry.positions.len() * 16 + geometry.indices.len() * 16);
106 out.push_str("# ");
107 out.push_str(comment);
108 out.push('\n');
109 for tri in geometry.positions.chunks_exact(3) {
110 out.push_str(&format!("v {} {} {}\n", tri[0], tri[1], tri[2]));
111 }
112 for face in geometry.indices.chunks_exact(3) {
113 out.push_str(&format!("f {} {} {}\n", face[0] + 1, face[1] + 1, face[2] + 1));
114 }
115 out
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 const SAMPLE: &str = "v 0 0 0\nv 1 0 0\nv 0 1 0\nv 1 1 0\nf 1 2 3\nf 2 4 3\n# comment\n";
123
124 #[test]
125 fn parses_triangles() {
126 let g = parse_obj(SAMPLE).expect("parse");
127 assert_eq!(g.vertex_count(), 4);
128 assert_eq!(g.triangle_count(), 2);
129 assert_eq!(g.indices, [0, 1, 2, 1, 3, 2]);
130 }
131
132 #[test]
133 fn parses_slash_forms_and_negative_indices() {
134 let text = "v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1/1/1 2//1 -1\n";
135 let g = parse_obj(text).expect("parse");
136 assert_eq!(g.triangle_count(), 1);
137 assert_eq!(g.indices, [0, 1, 2]);
138 }
139
140 #[test]
141 fn rejects_polygon_face() {
142 let text = "v 0 0 0\nv 1 0 0\nv 0 1 0\nv 1 1 0\nf 1 2 3 4\n";
143 let err = parse_obj(text).unwrap_err();
144 assert!(err.to_string().contains("only triangle faces"), "{err}");
145 }
146
147 #[test]
148 fn rejects_out_of_range_index() {
149 let text = "v 0 0 0\nf 1 2 3\n";
150 let err = parse_obj(text).unwrap_err();
151 assert!(err.to_string().contains("beyond"), "{err}");
152 }
153
154 #[test]
155 fn rejects_garbage_numbers() {
156 let text = "v zero 0 0\n";
157 assert!(parse_obj(text).is_err());
158 let text2 = "v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 x 3\n";
159 assert!(parse_obj(text2).is_err());
160 }
161
162 #[test]
163 fn rejects_non_finite_vertex() {
164 let text = "v nan 0 0\n";
165 assert!(parse_obj(text).is_err());
166 }
167
168 #[test]
169 fn empty_input_is_empty_mesh() {
170 let g = parse_obj("").expect("parse");
171 assert_eq!(g.triangle_count(), 0);
172 }
173
174 #[test]
175 fn obj_roundtrip_text() {
176 let g = parse_obj(SAMPLE).expect("parse");
177 let text = write_obj(&g, "roundtrip");
178 let g2 = parse_obj(&text).expect("reparse");
179 assert_eq!(g.positions, g2.positions);
180 assert_eq!(g.indices, g2.indices);
181 }
182}