1use brepkit_math::vec::{Point3, Vec3};
4use brepkit_operations::tessellate::TriangleMesh;
5use brepkit_topology::Topology;
6use brepkit_topology::solid::SolidId;
7
8pub fn read_obj(input: &str) -> Result<TriangleMesh, crate::IoError> {
21 let mut positions: Vec<Point3> = Vec::new();
22 let mut normals: Vec<Vec3> = Vec::new();
23 let mut indices: Vec<u32> = Vec::new();
24
25 for line in input.lines() {
26 let line = line.trim();
27 if line.is_empty() || line.starts_with('#') {
28 continue;
29 }
30
31 let mut parts = line.split_whitespace();
32 match parts.next() {
33 Some("v") => {
34 let coords = parse_3_floats(&mut parts, line)?;
35 positions.push(Point3::new(coords[0], coords[1], coords[2]));
36 }
37 Some("vn") => {
38 let coords = parse_3_floats(&mut parts, line)?;
39 normals.push(Vec3::new(coords[0], coords[1], coords[2]));
40 }
41 Some("f") => {
42 let face_indices = parse_face_indices(&mut parts, line)?;
43 if face_indices.len() < 3 {
44 return Err(crate::IoError::ParseError {
45 reason: format!("face with fewer than 3 vertices: {line}"),
46 });
47 }
48 let v0 = face_indices[0];
50 for i in 1..face_indices.len() - 1 {
51 indices.push(v0);
52 indices.push(face_indices[i]);
53 indices.push(face_indices[i + 1]);
54 }
55 }
56 _ => {
57 }
59 }
60 }
61
62 if normals.is_empty() {
63 normals = compute_vertex_normals(&positions, &indices);
64 }
65
66 normals.resize(positions.len(), Vec3::new(0.0, 0.0, 1.0));
67
68 Ok(TriangleMesh {
69 positions,
70 normals,
71 indices,
72 })
73}
74
75fn parse_face_index(token: &str, line: &str) -> Result<u32, crate::IoError> {
78 let idx_str = token.split('/').next().unwrap_or(token);
79 let idx: i64 = idx_str.parse().map_err(|_| crate::IoError::ParseError {
80 reason: format!("invalid face index in: {line}"),
81 })?;
82 if idx <= 0 {
83 return Err(crate::IoError::ParseError {
84 reason: format!("negative or zero face index in: {line}"),
85 });
86 }
87 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
88 Ok((idx - 1) as u32) }
90
91fn parse_face_indices(
92 parts: &mut std::str::SplitWhitespace<'_>,
93 line: &str,
94) -> Result<Vec<u32>, crate::IoError> {
95 let mut result = Vec::new();
96 for token in parts {
97 result.push(parse_face_index(token, line)?);
98 }
99 Ok(result)
100}
101
102fn parse_3_floats(
103 parts: &mut std::str::SplitWhitespace<'_>,
104 line: &str,
105) -> Result<[f64; 3], crate::IoError> {
106 let mut coords = [0.0; 3];
107 for coord in &mut coords {
108 *coord = parts
109 .next()
110 .ok_or_else(|| crate::IoError::ParseError {
111 reason: format!("expected 3 coordinates in: {line}"),
112 })?
113 .parse()
114 .map_err(|_| crate::IoError::ParseError {
115 reason: format!("invalid float in: {line}"),
116 })?;
117 }
118 Ok(coords)
119}
120
121fn compute_vertex_normals(positions: &[Point3], indices: &[u32]) -> Vec<Vec3> {
123 let mut normals = vec![Vec3::new(0.0, 0.0, 0.0); positions.len()];
124
125 for tri in indices.chunks_exact(3) {
126 let i0 = tri[0] as usize;
127 let i1 = tri[1] as usize;
128 let i2 = tri[2] as usize;
129
130 if i0 >= positions.len() || i1 >= positions.len() || i2 >= positions.len() {
131 continue;
132 }
133
134 let e1 = positions[i1] - positions[i0];
135 let e2 = positions[i2] - positions[i0];
136 let face_normal = e1.cross(e2);
137
138 normals[i0] += face_normal;
139 normals[i1] += face_normal;
140 normals[i2] += face_normal;
141 }
142
143 for n in &mut normals {
144 let len = n.length();
145 if len > 1e-15 {
146 *n = Vec3::new(n.x() / len, n.y() / len, n.z() / len);
147 } else {
148 *n = Vec3::new(0.0, 0.0, 1.0);
149 }
150 }
151
152 normals
153}
154
155pub fn read_obj_solid(
166 topo: &mut Topology,
167 input: &str,
168 tolerance: f64,
169) -> Result<SolidId, crate::IoError> {
170 let mesh = read_obj(input)?;
171 crate::stl::import::import_mesh(topo, &mesh, tolerance)
172}
173
174#[cfg(test)]
175#[allow(clippy::unwrap_used)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn read_simple_triangle() {
181 let obj = "\
182v 0.0 0.0 0.0
183v 1.0 0.0 0.0
184v 0.0 1.0 0.0
185f 1 2 3
186";
187 let mesh = read_obj(obj).unwrap();
188 assert_eq!(mesh.positions.len(), 3);
189 assert_eq!(mesh.indices.len(), 3);
190 assert_eq!(mesh.indices, vec![0, 1, 2]);
191 }
192
193 #[test]
194 fn read_quad_fan_triangulation() {
195 let obj = "\
196v 0.0 0.0 0.0
197v 1.0 0.0 0.0
198v 1.0 1.0 0.0
199v 0.0 1.0 0.0
200f 1 2 3 4
201";
202 let mesh = read_obj(obj).unwrap();
203 assert_eq!(mesh.positions.len(), 4);
204 assert_eq!(mesh.indices.len(), 6);
206 }
207
208 #[test]
209 fn read_with_normals() {
210 let obj = "\
211v 0.0 0.0 0.0
212v 1.0 0.0 0.0
213v 0.0 1.0 0.0
214vn 0.0 0.0 1.0
215f 1//1 2//1 3//1
216";
217 let mesh = read_obj(obj).unwrap();
218 assert_eq!(mesh.positions.len(), 3);
219 assert_eq!(mesh.normals.len(), 3);
220 }
221
222 #[test]
223 fn read_with_comments() {
224 let obj = "\
225# This is a comment
226v 0.0 0.0 0.0
227# Another comment
228v 1.0 0.0 0.0
229v 0.0 1.0 0.0
230f 1 2 3
231";
232 let mesh = read_obj(obj).unwrap();
233 assert_eq!(mesh.positions.len(), 3);
234 }
235
236 #[test]
237 fn roundtrip_write_read() {
238 let mut topo = brepkit_topology::Topology::new();
239 let solid = brepkit_operations::primitives::make_box(&mut topo, 1.0, 1.0, 1.0).unwrap();
240
241 let obj_str = crate::obj::write_obj(&topo, &[solid], 0.1).unwrap();
242 let mesh = read_obj(&obj_str).unwrap();
243
244 assert!(!mesh.positions.is_empty(), "should have vertices");
245 assert!(!mesh.indices.is_empty(), "should have faces");
246 assert_eq!(mesh.indices.len() % 3, 0, "indices should be multiple of 3");
247 }
248
249 #[test]
250 fn read_empty_error() {
251 let obj = "";
252 let mesh = read_obj(obj).unwrap();
253 assert_eq!(mesh.positions.len(), 0);
254 assert_eq!(mesh.indices.len(), 0);
255 }
256
257 #[test]
258 fn computed_normals_are_unit() {
259 let obj = "\
260v 0.0 0.0 0.0
261v 1.0 0.0 0.0
262v 0.0 1.0 0.0
263f 1 2 3
264";
265 let mesh = read_obj(obj).unwrap();
266 for n in &mesh.normals {
267 let len = n.length();
268 assert!(
269 (len - 1.0).abs() < 1e-10,
270 "normal should be unit length, got {len}"
271 );
272 }
273 }
274
275 #[test]
276 fn read_obj_solid_returns_solid_id() {
277 let obj = "\
279v 0 0 0
280v 1 0 0
281v 0 1 0
282v 0 0 1
283f 1 2 3
284f 1 3 4
285f 1 4 2
286f 2 4 3
287";
288 let mut topo = brepkit_topology::Topology::new();
289 let result = read_obj_solid(&mut topo, obj, 1e-6);
290 assert!(
291 result.is_ok(),
292 "read_obj_solid should return Ok: {result:?}"
293 );
294 }
295}