1#![allow(clippy::items_after_test_module)]
6#![allow(clippy::field_reassign_with_default)]
7
8use super::types::{
9 MeshInstance, MeshTransform, ObjFace, ObjGroup, ObjMaterial, ObjMesh, ObjMeshStats,
10};
11
12#[cfg(test)]
13mod tests {
14
15 use crate::obj::types::*;
16 use oxiphysics_core::math::Vec3;
17 fn make_default_face(vis: Vec<usize>) -> ObjFace {
18 ObjFace {
19 vertex_indices: vis,
20 normal_indices: None,
21 uv_indices: None,
22 smoothing_group: 0,
23 material: None,
24 }
25 }
26 #[test]
27 fn test_obj_write_and_read_roundtrip() {
28 let path = "/tmp/oxiphy_test.obj";
29 let verts = vec![
30 Vec3::new(0.0, 0.0, 0.0),
31 Vec3::new(1.0, 0.0, 0.0),
32 Vec3::new(0.0, 1.0, 0.0),
33 ];
34 let tris = vec![[0, 1, 2]];
35 ObjWriter::write_legacy(path, &verts, &tris, None).unwrap();
36 let (read_verts, read_tris) = ObjReader::read(path).unwrap();
37 assert_eq!(read_verts.len(), 3);
38 assert_eq!(read_tris.len(), 1);
39 assert_eq!(read_tris[0], [0, 1, 2]);
40 assert!((read_verts[1].x - 1.0).abs() < 1e-10);
41 std::fs::remove_file(path).ok();
42 }
43 #[test]
44 fn test_obj_write_wavefront() {
45 let path = "/tmp/oxiphy_test_wavefront.obj";
46 let verts = vec![
47 Vec3::new(0.0, 0.0, 0.0),
48 Vec3::new(1.0, 0.0, 0.0),
49 Vec3::new(0.5, 1.0, 0.0),
50 ];
51 let tris = vec![[0, 1, 2]];
52 ObjWriter::write_legacy(path, &verts, &tris, None).unwrap();
53 let content = std::fs::read_to_string(path).unwrap();
54 assert!(content.lines().any(|l| l.starts_with("v ")));
55 assert!(content.lines().any(|l| l.starts_with("f ")));
56 let v_count = content.lines().filter(|l| l.starts_with("v ")).count();
57 let f_count = content.lines().filter(|l| l.starts_with("f ")).count();
58 assert_eq!(v_count, 3);
59 assert_eq!(f_count, 1);
60 std::fs::remove_file(path).ok();
61 }
62 #[test]
63 fn test_obj_reader_handles_vertex_face_parsing() {
64 let path = "/tmp/oxiphy_test_parse.obj";
65 let verts = vec![
66 Vec3::new(0.0, 0.0, 0.0),
67 Vec3::new(1.0, 0.0, 0.0),
68 Vec3::new(0.0, 1.0, 0.0),
69 ];
70 let norms = vec![
71 Vec3::new(0.0, 0.0, 1.0),
72 Vec3::new(0.0, 0.0, 1.0),
73 Vec3::new(0.0, 0.0, 1.0),
74 ];
75 let tris = vec![[0, 1, 2]];
76 ObjWriter::write_legacy(path, &verts, &tris, Some(&norms)).unwrap();
77 let (read_verts, read_tris) = ObjReader::read(path).unwrap();
78 assert_eq!(read_verts.len(), 3);
79 assert_eq!(read_tris.len(), 1);
80 assert_eq!(read_tris[0], [0, 1, 2]);
81 std::fs::remove_file(path).ok();
82 }
83 #[test]
84 fn test_obj_mesh_write_read_vertex_roundtrip() {
85 let mut mesh = ObjMesh::default();
86 mesh.vertices = vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
87 mesh.faces.push(make_default_face(vec![0, 1, 2]));
88 let s = ObjWriter::write(&mesh);
89 let parsed = ObjReader::from_str(&s).unwrap();
90 assert_eq!(parsed.vertices.len(), 3);
91 assert!((parsed.vertices[0][0] - 1.0).abs() < 1e-10);
92 assert!((parsed.vertices[2][2] - 9.0).abs() < 1e-10);
93 }
94 #[test]
95 fn test_obj_mesh_face_format() {
96 let mut mesh = ObjMesh::default();
97 mesh.vertices = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
98 mesh.faces.push(make_default_face(vec![0, 1, 2]));
99 let s = ObjWriter::write(&mesh);
100 assert!(s.contains("f 1 2 3"), "face line not found in: {s}");
101 }
102 #[test]
103 fn test_obj_mesh_normal_export() {
104 let mut mesh = ObjMesh::default();
105 mesh.vertices = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
106 mesh.normals = vec![[0.0, 0.0, 1.0], [0.0, 0.0, 1.0], [0.0, 0.0, 1.0]];
107 mesh.faces.push(ObjFace {
108 vertex_indices: vec![0, 1, 2],
109 normal_indices: Some(vec![0, 1, 2]),
110 uv_indices: None,
111 smoothing_group: 0,
112 material: None,
113 });
114 let s = ObjWriter::write(&mesh);
115 assert!(s.contains("vn"), "normals not exported: {s}");
116 assert!(s.contains("//"), "face should use v//vn format: {s}");
117 }
118 #[test]
119 fn test_multi_object_groups() {
120 let g1 = ObjGroup {
121 name: "body".into(),
122 face_start: 0,
123 face_count: 4,
124 };
125 let g2 = ObjGroup {
126 name: "wheel".into(),
127 face_start: 4,
128 face_count: 2,
129 };
130 assert_eq!(g1.name, "body");
131 assert_eq!(g2.face_start, 4);
132 assert_eq!(g1.face_count + g2.face_count, 6);
133 }
134 #[test]
135 fn test_triangle_soup_count() {
136 let mut mesh = ObjMesh::default();
137 mesh.vertices = vec![
138 [0.0, 0.0, 0.0],
139 [1.0, 0.0, 0.0],
140 [0.0, 1.0, 0.0],
141 [1.0, 1.0, 0.0],
142 ];
143 mesh.faces.push(make_default_face(vec![0, 1, 2]));
144 mesh.faces.push(make_default_face(vec![1, 3, 2]));
145 let soup = mesh.to_triangle_soup();
146 assert_eq!(soup.len(), 2);
147 }
148 #[test]
149 fn test_mtl_writer_output() {
150 let mat = ObjMaterial {
151 name: "Red".into(),
152 kd: [1.0, 0.0, 0.0],
153 ks: [0.5, 0.5, 0.5],
154 ns: 32.0,
155 ka: [0.1, 0.0, 0.0],
156 dissolve: 1.0,
157 map_kd: None,
158 };
159 let s = MtlWriter::write(&[mat]);
160 assert!(s.contains("newmtl Red"), "material name missing: {s}");
161 assert!(s.contains("Kd 1"), "diffuse colour missing: {s}");
162 assert!(s.contains("Ns 32"), "shininess missing: {s}");
163 }
164 #[test]
165 fn test_triangle_soup_quad_triangulation() {
166 let mut mesh = ObjMesh::default();
167 mesh.vertices = vec![
168 [0.0, 0.0, 0.0],
169 [1.0, 0.0, 0.0],
170 [1.0, 1.0, 0.0],
171 [0.0, 1.0, 0.0],
172 ];
173 mesh.faces.push(make_default_face(vec![0, 1, 2, 3]));
174 let soup = mesh.to_triangle_soup();
175 assert_eq!(
176 soup.len(),
177 2,
178 "quad should triangulate to 2 triangles, got {}",
179 soup.len()
180 );
181 }
182 #[test]
183 fn test_group_parsing() {
184 let data = "\
185v 0 0 0
186v 1 0 0
187v 0 1 0
188v 1 1 0
189g group1
190f 1 2 3
191g group2
192f 2 4 3
193";
194 let mesh = ObjReader::from_str(data).unwrap();
195 assert_eq!(mesh.groups.len(), 2);
196 assert_eq!(mesh.groups[0].name, "group1");
197 assert_eq!(mesh.groups[0].face_count, 1);
198 assert_eq!(mesh.groups[1].name, "group2");
199 assert_eq!(mesh.groups[1].face_count, 1);
200 }
201 #[test]
202 fn test_smoothing_group_parsing() {
203 let data = "\
204v 0 0 0
205v 1 0 0
206v 0 1 0
207v 1 1 0
208s 1
209f 1 2 3
210s 2
211f 2 4 3
212";
213 let mesh = ObjReader::from_str(data).unwrap();
214 assert_eq!(mesh.faces[0].smoothing_group, 1);
215 assert_eq!(mesh.faces[1].smoothing_group, 2);
216 }
217 #[test]
218 fn test_smoothing_group_off() {
219 let data = "\
220v 0 0 0
221v 1 0 0
222v 0 1 0
223s off
224f 1 2 3
225";
226 let mesh = ObjReader::from_str(data).unwrap();
227 assert_eq!(mesh.faces[0].smoothing_group, 0);
228 }
229 #[test]
230 fn test_material_parsing() {
231 let data = "\
232v 0 0 0
233v 1 0 0
234v 0 1 0
235v 1 1 0
236usemtl Red
237f 1 2 3
238usemtl Blue
239f 2 4 3
240";
241 let mesh = ObjReader::from_str(data).unwrap();
242 assert_eq!(mesh.faces[0].material.as_deref(), Some("Red"));
243 assert_eq!(mesh.faces[1].material.as_deref(), Some("Blue"));
244 }
245 #[test]
246 fn test_faces_in_group() {
247 let mut mesh = ObjMesh::default();
248 mesh.vertices = vec![
249 [0.0, 0.0, 0.0],
250 [1.0, 0.0, 0.0],
251 [0.0, 1.0, 0.0],
252 [1.0, 1.0, 0.0],
253 ];
254 mesh.faces.push(make_default_face(vec![0, 1, 2]));
255 mesh.faces.push(make_default_face(vec![1, 3, 2]));
256 mesh.faces.push(make_default_face(vec![0, 3, 2]));
257 mesh.groups.push(ObjGroup {
258 name: "A".into(),
259 face_start: 0,
260 face_count: 2,
261 });
262 mesh.groups.push(ObjGroup {
263 name: "B".into(),
264 face_start: 2,
265 face_count: 1,
266 });
267 assert_eq!(mesh.faces_in_group("A").len(), 2);
268 assert_eq!(mesh.faces_in_group("B").len(), 1);
269 assert_eq!(mesh.faces_in_group("C").len(), 0);
270 }
271 #[test]
272 fn test_faces_in_smoothing_group() {
273 let mut mesh = ObjMesh::default();
274 mesh.vertices = vec![[0.0; 3]; 4];
275 mesh.faces.push(ObjFace {
276 vertex_indices: vec![0, 1, 2],
277 normal_indices: None,
278 uv_indices: None,
279 smoothing_group: 1,
280 material: None,
281 });
282 mesh.faces.push(ObjFace {
283 vertex_indices: vec![1, 3, 2],
284 normal_indices: None,
285 uv_indices: None,
286 smoothing_group: 2,
287 material: None,
288 });
289 mesh.faces.push(ObjFace {
290 vertex_indices: vec![0, 3, 2],
291 normal_indices: None,
292 uv_indices: None,
293 smoothing_group: 1,
294 material: None,
295 });
296 assert_eq!(mesh.faces_in_smoothing_group(1).len(), 2);
297 assert_eq!(mesh.faces_in_smoothing_group(2).len(), 1);
298 }
299 #[test]
300 fn test_faces_with_material() {
301 let mut mesh = ObjMesh::default();
302 mesh.vertices = vec![[0.0; 3]; 4];
303 mesh.faces.push(ObjFace {
304 vertex_indices: vec![0, 1, 2],
305 normal_indices: None,
306 uv_indices: None,
307 smoothing_group: 0,
308 material: Some("Red".into()),
309 });
310 mesh.faces.push(ObjFace {
311 vertex_indices: vec![1, 3, 2],
312 normal_indices: None,
313 uv_indices: None,
314 smoothing_group: 0,
315 material: Some("Blue".into()),
316 });
317 assert_eq!(mesh.faces_with_material("Red").len(), 1);
318 assert_eq!(mesh.faces_with_material("Blue").len(), 1);
319 assert_eq!(mesh.faces_with_material("Green").len(), 0);
320 }
321 #[test]
322 fn test_triangle_count() {
323 let mut mesh = ObjMesh::default();
324 mesh.vertices = vec![[0.0; 3]; 5];
325 mesh.faces.push(make_default_face(vec![0, 1, 2]));
326 mesh.faces.push(make_default_face(vec![0, 1, 2, 3]));
327 mesh.faces.push(make_default_face(vec![0, 1, 2, 3, 4]));
328 assert_eq!(mesh.triangle_count(), 6);
329 }
330 #[test]
331 fn test_face_normal() {
332 let mut mesh = ObjMesh::default();
333 mesh.vertices = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
334 mesh.faces.push(make_default_face(vec![0, 1, 2]));
335 let n = mesh.face_normal(0).unwrap();
336 assert!((n[0] - 0.0).abs() < 1e-10);
337 assert!((n[1] - 0.0).abs() < 1e-10);
338 assert!((n[2] - 1.0).abs() < 1e-10);
339 }
340 #[test]
341 fn test_bounding_box() {
342 let mut mesh = ObjMesh::default();
343 mesh.vertices = vec![[1.0, 2.0, 3.0], [-1.0, -2.0, -3.0], [0.0, 0.0, 0.0]];
344 let (min, max) = mesh.bounding_box().unwrap();
345 assert!((min[0] - (-1.0)).abs() < 1e-10);
346 assert!((min[1] - (-2.0)).abs() < 1e-10);
347 assert!((min[2] - (-3.0)).abs() < 1e-10);
348 assert!((max[0] - 1.0).abs() < 1e-10);
349 assert!((max[1] - 2.0).abs() < 1e-10);
350 assert!((max[2] - 3.0).abs() < 1e-10);
351 }
352 #[test]
353 fn test_bounding_box_empty() {
354 let mesh = ObjMesh::default();
355 assert!(mesh.bounding_box().is_none());
356 }
357 #[test]
358 fn test_texture_coordinate_roundtrip() {
359 let mut mesh = ObjMesh::default();
360 mesh.vertices = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
361 mesh.uvs = vec![[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
362 mesh.faces.push(ObjFace {
363 vertex_indices: vec![0, 1, 2],
364 normal_indices: None,
365 uv_indices: Some(vec![0, 1, 2]),
366 smoothing_group: 0,
367 material: None,
368 });
369 let s = ObjWriter::write(&mesh);
370 let parsed = ObjReader::from_str(&s).unwrap();
371 assert_eq!(parsed.uvs.len(), 3);
372 assert!((parsed.uvs[2][1] - 1.0).abs() < 1e-10);
373 assert!(parsed.faces[0].uv_indices.is_some());
374 }
375 #[test]
376 fn test_curve_struct() {
377 let curve = ObjCurve {
378 name: "test_curve".into(),
379 degree: 3,
380 control_points: vec![0, 1, 2, 3],
381 knots: vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
382 };
383 assert_eq!(curve.degree, 3);
384 assert_eq!(curve.control_points.len(), 4);
385 assert_eq!(curve.knots.len(), 8);
386 }
387 #[test]
388 fn test_surface_struct() {
389 let surface = ObjSurface {
390 name: "test_surface".into(),
391 degree_u: 2,
392 degree_v: 2,
393 control_points: vec![0, 1, 2, 3, 4, 5, 6, 7, 8],
394 n_u: 3,
395 knots_u: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
396 knots_v: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
397 };
398 assert_eq!(surface.degree_u, 2);
399 assert_eq!(surface.n_u, 3);
400 assert_eq!(surface.control_points.len(), 9);
401 }
402 #[test]
403 fn test_material_basic() {
404 let mat = ObjMaterial::basic("test_mat", [0.5, 0.5, 0.5]);
405 assert_eq!(mat.name, "test_mat");
406 assert!((mat.kd[0] - 0.5).abs() < 1e-10);
407 assert!((mat.dissolve - 1.0).abs() < 1e-10);
408 }
409 #[test]
410 fn test_write_with_groups_roundtrip() {
411 let mut mesh = ObjMesh::default();
412 mesh.vertices = vec![
413 [0.0, 0.0, 0.0],
414 [1.0, 0.0, 0.0],
415 [0.0, 1.0, 0.0],
416 [1.0, 1.0, 0.0],
417 ];
418 mesh.faces.push(make_default_face(vec![0, 1, 2]));
419 mesh.faces.push(make_default_face(vec![1, 3, 2]));
420 mesh.groups.push(ObjGroup {
421 name: "grp1".into(),
422 face_start: 0,
423 face_count: 1,
424 });
425 mesh.groups.push(ObjGroup {
426 name: "grp2".into(),
427 face_start: 1,
428 face_count: 1,
429 });
430 let s = ObjWriter::write_with_groups(&mesh, true);
431 assert!(s.contains("g grp1"));
432 assert!(s.contains("g grp2"));
433 let parsed = ObjReader::from_str(&s).unwrap();
434 assert_eq!(parsed.groups.len(), 2);
435 }
436 #[test]
437 fn test_write_with_uvs() {
438 let path = "/tmp/oxiphy_test_uvs.obj";
439 let verts = vec![
440 Vec3::new(0.0, 0.0, 0.0),
441 Vec3::new(1.0, 0.0, 0.0),
442 Vec3::new(0.0, 1.0, 0.0),
443 ];
444 let uvs = vec![[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]];
445 let tris = vec![[0, 1, 2]];
446 ObjWriter::write_with_uvs(path, &verts, &uvs, &tris).unwrap();
447 let content = std::fs::read_to_string(path).unwrap();
448 assert!(content.lines().any(|l| l.starts_with("vt ")));
449 std::fs::remove_file(path).ok();
450 }
451 #[test]
452 fn test_mtl_writer_with_texture() {
453 let mat = ObjMaterial {
454 name: "Textured".into(),
455 kd: [1.0, 1.0, 1.0],
456 ks: [0.0; 3],
457 ns: 1.0,
458 ka: [0.1, 0.1, 0.1],
459 dissolve: 0.8,
460 map_kd: Some("diffuse.png".into()),
461 };
462 let s = MtlWriter::write(&[mat]);
463 assert!(s.contains("map_Kd diffuse.png"));
464 assert!(s.contains("d 0.8"));
465 }
466 #[test]
467 fn test_write_smoothing_groups() {
468 let mut mesh = ObjMesh::default();
469 mesh.vertices = vec![
470 [0.0, 0.0, 0.0],
471 [1.0, 0.0, 0.0],
472 [0.0, 1.0, 0.0],
473 [1.0, 1.0, 0.0],
474 ];
475 mesh.faces.push(ObjFace {
476 vertex_indices: vec![0, 1, 2],
477 normal_indices: None,
478 uv_indices: None,
479 smoothing_group: 1,
480 material: None,
481 });
482 mesh.faces.push(ObjFace {
483 vertex_indices: vec![1, 3, 2],
484 normal_indices: None,
485 uv_indices: None,
486 smoothing_group: 2,
487 material: None,
488 });
489 let s = ObjWriter::write(&mesh);
490 assert!(s.contains("s 1"), "smoothing group 1 missing: {s}");
491 assert!(s.contains("s 2"), "smoothing group 2 missing: {s}");
492 }
493 #[test]
494 fn test_write_material_headers() {
495 let mut mesh = ObjMesh::default();
496 mesh.vertices = vec![[0.0; 3]; 4];
497 mesh.faces.push(ObjFace {
498 vertex_indices: vec![0, 1, 2],
499 normal_indices: None,
500 uv_indices: None,
501 smoothing_group: 0,
502 material: Some("Mat1".into()),
503 });
504 mesh.faces.push(ObjFace {
505 vertex_indices: vec![1, 3, 2],
506 normal_indices: None,
507 uv_indices: None,
508 smoothing_group: 0,
509 material: Some("Mat2".into()),
510 });
511 let s = ObjWriter::write(&mesh);
512 assert!(s.contains("usemtl Mat1"));
513 assert!(s.contains("usemtl Mat2"));
514 }
515
516 fn make_sphere_mesh(n_lat: usize, n_lon: usize) -> ObjMesh {
521 use std::f64::consts::PI;
522 let mut vertices: Vec<[f64; 3]> = Vec::new();
523 for lat in 0..=n_lat {
525 let theta = PI * lat as f64 / n_lat as f64;
526 for lon in 0..n_lon {
527 let phi = 2.0 * PI * lon as f64 / n_lon as f64;
528 vertices.push([
529 theta.sin() * phi.cos(),
530 theta.cos(),
531 theta.sin() * phi.sin(),
532 ]);
533 }
534 }
535 let mut faces: Vec<ObjFace> = Vec::new();
536 let idx = |lat: usize, lon: usize| lat * n_lon + (lon % n_lon);
537 for lat in 0..n_lat {
538 for lon in 0..n_lon {
539 let a = idx(lat, lon);
540 let b = idx(lat + 1, lon);
541 let c = idx(lat + 1, lon + 1);
542 let d = idx(lat, lon + 1);
543 faces.push(ObjFace {
544 vertex_indices: vec![a, b, c],
545 normal_indices: None,
546 uv_indices: None,
547 smoothing_group: 0,
548 material: None,
549 });
550 faces.push(ObjFace {
551 vertex_indices: vec![a, c, d],
552 normal_indices: None,
553 uv_indices: None,
554 smoothing_group: 0,
555 material: None,
556 });
557 }
558 }
559 ObjMesh {
560 vertices,
561 normals: Vec::new(),
562 uvs: Vec::new(),
563 faces,
564 groups: Vec::new(),
565 }
566 }
567
568 #[test]
569 fn test_decimate_below_target_returns_clone() {
570 let mesh = make_sphere_mesh(4, 8); let result = ObjLod::decimate(&mesh, 200);
572 assert_eq!(result.faces.len(), mesh.faces.len());
573 }
574
575 #[test]
576 fn test_decimate_qem_reduces_face_count() {
577 let mesh = make_sphere_mesh(8, 16); let target = 64;
579 let result = ObjLod::decimate(&mesh, target);
580 assert!(
581 result.faces.len() <= target,
582 "expected ≤{target} faces, got {}",
583 result.faces.len()
584 );
585 assert!(
586 !result.faces.is_empty(),
587 "decimated mesh must have at least one face"
588 );
589 }
590
591 #[test]
592 fn test_decimate_vertex_count_decreases() {
593 let mesh = make_sphere_mesh(8, 16); let original_verts = mesh.vertices.len();
595 let result = ObjLod::decimate(&mesh, 32);
596 assert!(
597 result.vertices.len() < original_verts,
598 "QEM should reduce vertex count ({} vs {})",
599 result.vertices.len(),
600 original_verts
601 );
602 }
603}
604#[allow(dead_code)]
606pub fn instantiate_mesh(mesh: &ObjMesh, instance: &MeshInstance) -> ObjMesh {
607 let mut out = mesh.clone();
608 for v in &mut out.vertices {
609 let p = instance.transform.apply(*v);
610 *v = p;
611 }
612 for n in &mut out.normals {
613 let rot_only = MeshTransform {
614 translation: [0.0; 3],
615 scale: 1.0,
616 ..instance.transform.clone()
617 };
618 *n = rot_only.apply(*n);
619 }
620 out
621}
622#[allow(dead_code)]
626pub fn weld_vertices(mesh: &ObjMesh, tolerance: f64) -> ObjMesh {
627 let tol2 = tolerance * tolerance;
628 let mut new_verts: Vec<[f64; 3]> = Vec::new();
629 let mut remap: Vec<usize> = Vec::with_capacity(mesh.vertices.len());
630 for &v in &mesh.vertices {
631 let found = new_verts.iter().position(|&u| {
632 let dx = u[0] - v[0];
633 let dy = u[1] - v[1];
634 let dz = u[2] - v[2];
635 dx * dx + dy * dy + dz * dz <= tol2
636 });
637 if let Some(idx) = found {
638 remap.push(idx);
639 } else {
640 remap.push(new_verts.len());
641 new_verts.push(v);
642 }
643 }
644 let mut out = ObjMesh {
645 vertices: new_verts,
646 normals: mesh.normals.clone(),
647 uvs: mesh.uvs.clone(),
648 groups: mesh.groups.clone(),
649 ..Default::default()
650 };
651 for face in &mesh.faces {
652 let new_vis: Vec<usize> = face.vertex_indices.iter().map(|&i| remap[i]).collect();
653 out.faces.push(ObjFace {
654 vertex_indices: new_vis,
655 normal_indices: face.normal_indices.clone(),
656 uv_indices: face.uv_indices.clone(),
657 smoothing_group: face.smoothing_group,
658 material: face.material.clone(),
659 });
660 }
661 out
662}
663#[allow(dead_code)]
667pub fn merge_obj_meshes(a: &ObjMesh, b: &ObjMesh) -> ObjMesh {
668 let mut out = a.clone();
669 let v_offset = a.vertices.len();
670 let n_offset = a.normals.len();
671 let uv_offset = a.uvs.len();
672 out.vertices.extend_from_slice(&b.vertices);
673 out.normals.extend_from_slice(&b.normals);
674 out.uvs.extend_from_slice(&b.uvs);
675 for face in &b.faces {
676 let new_vis: Vec<usize> = face.vertex_indices.iter().map(|&i| i + v_offset).collect();
677 let new_ns = face
678 .normal_indices
679 .as_ref()
680 .map(|ns| ns.iter().map(|&i| i + n_offset).collect::<Vec<_>>());
681 let new_uvs = face
682 .uv_indices
683 .as_ref()
684 .map(|uvs| uvs.iter().map(|&i| i + uv_offset).collect::<Vec<_>>());
685 out.faces.push(ObjFace {
686 vertex_indices: new_vis,
687 normal_indices: new_ns,
688 uv_indices: new_uvs,
689 smoothing_group: face.smoothing_group,
690 material: face.material.clone(),
691 });
692 }
693 let face_offset = a.faces.len();
694 for g in &b.groups {
695 out.groups.push(ObjGroup {
696 name: g.name.clone(),
697 face_start: g.face_start + face_offset,
698 face_count: g.face_count,
699 });
700 }
701 out
702}
703#[allow(dead_code)]
708pub fn recompute_normals(mesh: &mut ObjMesh) {
709 let n = mesh.vertices.len();
710 let mut accum = vec![[0.0_f64; 3]; n];
711 let mut weights = vec![0.0_f64; n];
712 for face in &mesh.faces {
713 let vis = &face.vertex_indices;
714 if vis.len() < 3 {
715 continue;
716 }
717 for i in 1..(vis.len() - 1) {
718 let v0 = mesh.vertices[vis[0]];
719 let v1 = mesh.vertices[vis[i]];
720 let v2 = mesh.vertices[vis[i + 1]];
721 let e1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
722 let e2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
723 let nx = e1[1] * e2[2] - e1[2] * e2[1];
724 let ny = e1[2] * e2[0] - e1[0] * e2[2];
725 let nz = e1[0] * e2[1] - e1[1] * e2[0];
726 let area = (nx * nx + ny * ny + nz * nz).sqrt() * 0.5;
727 for &vi in &[vis[0], vis[i], vis[i + 1]] {
728 accum[vi][0] += nx;
729 accum[vi][1] += ny;
730 accum[vi][2] += nz;
731 weights[vi] += area;
732 }
733 }
734 }
735 mesh.normals = accum
736 .iter()
737 .zip(weights.iter())
738 .map(|(n, &w)| {
739 if w < 1e-30 {
740 [0.0, 0.0, 1.0]
741 } else {
742 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
743 if len < 1e-30 {
744 [0.0, 0.0, 1.0]
745 } else {
746 [n[0] / len, n[1] / len, n[2] / len]
747 }
748 }
749 })
750 .collect();
751 for face in &mut mesh.faces {
752 let n_idx: Vec<usize> = face.vertex_indices.clone();
753 face.normal_indices = Some(n_idx);
754 }
755}
756#[allow(dead_code)]
758pub fn parse_mtl(data: &str) -> Vec<ObjMaterial> {
759 let mut materials: Vec<ObjMaterial> = Vec::new();
760 let mut current: Option<ObjMaterial> = None;
761 for raw in data.lines() {
762 let line = raw.trim();
763 if line.is_empty() || line.starts_with('#') {
764 continue;
765 }
766 let tokens: Vec<&str> = line.splitn(2, ' ').collect();
767 if tokens.is_empty() {
768 continue;
769 }
770 match tokens[0] {
771 "newmtl" => {
772 if let Some(mat) = current.take() {
773 materials.push(mat);
774 }
775 let name = tokens
776 .get(1)
777 .map(|s| s.trim().to_string())
778 .unwrap_or_default();
779 current = Some(ObjMaterial {
780 name,
781 kd: [0.8, 0.8, 0.8],
782 ks: [0.0; 3],
783 ns: 1.0,
784 ka: [0.0; 3],
785 dissolve: 1.0,
786 map_kd: None,
787 });
788 }
789 "Kd" | "kd" => {
790 if let Some(ref mut mat) = current
791 && let Some(rest) = tokens.get(1)
792 {
793 let p: Vec<&str> = rest.split_whitespace().collect();
794 if p.len() >= 3 {
795 mat.kd[0] = p[0].parse().unwrap_or(0.8);
796 mat.kd[1] = p[1].parse().unwrap_or(0.8);
797 mat.kd[2] = p[2].parse().unwrap_or(0.8);
798 }
799 }
800 }
801 "Ks" | "ks" => {
802 if let Some(ref mut mat) = current
803 && let Some(rest) = tokens.get(1)
804 {
805 let p: Vec<&str> = rest.split_whitespace().collect();
806 if p.len() >= 3 {
807 mat.ks[0] = p[0].parse().unwrap_or(0.0);
808 mat.ks[1] = p[1].parse().unwrap_or(0.0);
809 mat.ks[2] = p[2].parse().unwrap_or(0.0);
810 }
811 }
812 }
813 "Ka" | "ka" => {
814 if let Some(ref mut mat) = current
815 && let Some(rest) = tokens.get(1)
816 {
817 let p: Vec<&str> = rest.split_whitespace().collect();
818 if p.len() >= 3 {
819 mat.ka[0] = p[0].parse().unwrap_or(0.0);
820 mat.ka[1] = p[1].parse().unwrap_or(0.0);
821 mat.ka[2] = p[2].parse().unwrap_or(0.0);
822 }
823 }
824 }
825 "Ns" | "ns" => {
826 if let Some(ref mut mat) = current
827 && let Some(rest) = tokens.get(1)
828 {
829 mat.ns = rest.trim().parse().unwrap_or(1.0);
830 }
831 }
832 "d" => {
833 if let Some(ref mut mat) = current
834 && let Some(rest) = tokens.get(1)
835 {
836 mat.dissolve = rest.trim().parse().unwrap_or(1.0);
837 }
838 }
839 "Tr" => {
840 if let Some(ref mut mat) = current
841 && let Some(rest) = tokens.get(1)
842 {
843 let tr: f64 = rest.trim().parse().unwrap_or(0.0);
844 mat.dissolve = 1.0 - tr;
845 }
846 }
847 "map_Kd" | "map_kd" => {
848 if let Some(ref mut mat) = current
849 && let Some(rest) = tokens.get(1)
850 {
851 mat.map_kd = Some(rest.trim().to_string());
852 }
853 }
854 _ => {}
855 }
856 }
857 if let Some(mat) = current {
858 materials.push(mat);
859 }
860 materials
861}
862#[allow(dead_code)]
864pub fn compute_mesh_stats(mesh: &ObjMesh) -> ObjMeshStats {
865 let mut mat_names: Vec<&str> = Vec::new();
866 let mut faces_with_normals = 0;
867 let mut faces_with_uvs = 0;
868 let mut surface_area = 0.0_f64;
869 for face in &mesh.faces {
870 if face.normal_indices.is_some() {
871 faces_with_normals += 1;
872 }
873 if face.uv_indices.is_some() {
874 faces_with_uvs += 1;
875 }
876 if let Some(ref m) = face.material
877 && !mat_names.contains(&m.as_str())
878 {
879 mat_names.push(m.as_str());
880 }
881 let vis = &face.vertex_indices;
882 for i in 1..(vis.len().saturating_sub(1)) {
883 if vis[0] < mesh.vertices.len()
884 && vis[i] < mesh.vertices.len()
885 && vis[i + 1] < mesh.vertices.len()
886 {
887 let v0 = mesh.vertices[vis[0]];
888 let v1 = mesh.vertices[vis[i]];
889 let v2 = mesh.vertices[vis[i + 1]];
890 let e1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
891 let e2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
892 let cx = e1[1] * e2[2] - e1[2] * e2[1];
893 let cy = e1[2] * e2[0] - e1[0] * e2[2];
894 let cz = e1[0] * e2[1] - e1[1] * e2[0];
895 surface_area += (cx * cx + cy * cy + cz * cz).sqrt() * 0.5;
896 }
897 }
898 }
899 ObjMeshStats {
900 vertex_count: mesh.vertices.len(),
901 face_count: mesh.faces.len(),
902 triangle_count: mesh.triangle_count(),
903 material_count: mat_names.len(),
904 group_count: mesh.groups.len(),
905 faces_with_normals,
906 faces_with_uvs,
907 surface_area,
908 bbox: mesh.bounding_box(),
909 }
910}