1use crate::mtl;
79use lib3mf_core::error::{Lib3mfError, Result};
80use lib3mf_core::model::resources::ResourceId;
81use lib3mf_core::model::{
82 BaseMaterial, BaseMaterialsGroup, BuildItem, Color, Mesh, Model, Object, ObjectType, Triangle,
83 Vertex,
84};
85use std::collections::HashMap;
86use std::io::{BufRead, BufReader, Read, Write};
87use std::path::Path;
88
89const DEFAULT_GRAY: Color = Color {
91 r: 128,
92 g: 128,
93 b: 128,
94 a: 255,
95};
96
97struct ObjFace {
103 indices: Vec<u32>,
104 material_name: Option<String>,
105}
106
107struct ObjGroup {
109 name: Option<String>,
110 faces: Vec<ObjFace>,
111}
112
113struct ObjIntermediate {
115 global_vertices: Vec<(f32, f32, f32)>,
116 groups: Vec<ObjGroup>,
117 mtllib: Option<String>,
118 had_explicit_group: bool,
119}
120
121pub struct ObjImporter;
129
130impl ObjImporter {
131 pub fn read_from_path(path: &Path) -> Result<Model> {
156 let dir = path.parent().unwrap_or(Path::new("."));
157 let file = std::fs::File::open(path).map_err(Lib3mfError::Io)?;
158 let intermediate = Self::parse_obj(BufReader::new(file))?;
159
160 let materials = if let Some(ref mtl_filename) = intermediate.mtllib {
162 let mtl_path = dir.join(mtl_filename);
163 mtl::parse_mtl_file(&mtl_path)
164 } else {
165 HashMap::new()
166 };
167
168 Self::build_model(intermediate, &materials)
169 }
170
171 pub fn read<R: Read>(reader: R) -> Result<Model> {
227 let intermediate = Self::parse_obj(BufReader::new(reader))?;
228 Self::build_model_compat(intermediate)
230 }
231
232 fn parse_obj<R: BufRead>(mut reader: R) -> Result<ObjIntermediate> {
234 let mut global_vertices: Vec<(f32, f32, f32)> = Vec::new();
235 let mut groups: Vec<ObjGroup> = Vec::new();
236 let mut current_group = ObjGroup {
237 name: None,
238 faces: Vec::new(),
239 };
240 let mut current_material: Option<String> = None;
241 let mut mtllib: Option<String> = None;
242 let mut had_explicit_group = false;
243
244 let mut line = String::new();
245
246 while reader.read_line(&mut line).map_err(Lib3mfError::Io)? > 0 {
247 let trimmed = line.trim();
248 if trimmed.is_empty() || trimmed.starts_with('#') {
249 line.clear();
250 continue;
251 }
252
253 let parts: Vec<&str> = trimmed.split_whitespace().collect();
254 if parts.is_empty() {
255 line.clear();
256 continue;
257 }
258
259 match parts[0] {
260 "v" => {
261 if parts.len() < 4 {
262 return Err(Lib3mfError::Validation("Invalid OBJ vertex".to_string()));
263 }
264 let x = parts[1]
265 .parse::<f32>()
266 .map_err(|_| Lib3mfError::Validation("Invalid float".to_string()))?;
267 let y = parts[2]
268 .parse::<f32>()
269 .map_err(|_| Lib3mfError::Validation("Invalid float".to_string()))?;
270 let z = parts[3]
271 .parse::<f32>()
272 .map_err(|_| Lib3mfError::Validation("Invalid float".to_string()))?;
273 global_vertices.push((x, y, z));
274 }
275 "f" => {
276 if parts.len() < 4 {
277 line.clear();
279 continue;
280 }
281
282 let mut indices = Vec::new();
283 for part in &parts[1..] {
284 let subparts: Vec<&str> = part.split('/').collect();
286 let v_idx = subparts[0]
287 .parse::<i32>()
288 .map_err(|_| Lib3mfError::Validation("Invalid index".to_string()))?;
289
290 let idx = if v_idx > 0 {
291 (v_idx - 1) as u32
292 } else {
293 return Err(Lib3mfError::Validation(
294 "Relative OBJ indices not supported yet".to_string(),
295 ));
296 };
297 indices.push(idx);
298 }
299
300 if indices.len() >= 3 {
302 for i in 1..indices.len() - 1 {
303 current_group.faces.push(ObjFace {
304 indices: vec![indices[0], indices[i], indices[i + 1]],
305 material_name: current_material.clone(),
306 });
307 }
308 }
309 }
310 "g" | "o" => {
311 had_explicit_group = true;
312 if !current_group.faces.is_empty() {
314 groups.push(current_group);
315 }
316 let name = if parts.len() >= 2 {
317 Some(parts[1..].join(" "))
318 } else {
319 None
320 };
321 current_group = ObjGroup {
322 name,
323 faces: Vec::new(),
324 };
325 }
326 "usemtl" => {
327 if parts.len() >= 2 {
328 current_material = Some(parts[1..].join(" "));
329 }
330 }
331 "mtllib" => {
332 if parts.len() >= 2 {
333 mtllib = Some(parts[1..].join(" "));
334 }
335 }
336 _ => {} }
338
339 line.clear();
340 }
341
342 if !current_group.faces.is_empty() {
344 groups.push(current_group);
345 }
346
347 Ok(ObjIntermediate {
348 global_vertices,
349 groups,
350 mtllib,
351 had_explicit_group,
352 })
353 }
354
355 fn build_model(
357 intermediate: ObjIntermediate,
358 materials_map: &HashMap<String, mtl::MtlMaterial>,
359 ) -> Result<Model> {
360 let mut model = Model::default();
361
362 if intermediate.groups.is_empty() {
363 return Ok(model);
364 }
365
366 let mut referenced_materials: Vec<String> = Vec::new();
368 let mut material_seen: HashMap<String, u32> = HashMap::new();
369 for group in &intermediate.groups {
370 for face in &group.faces {
371 if let Some(ref mat_name) = face.material_name
372 && !material_seen.contains_key(mat_name)
373 {
374 let idx = referenced_materials.len() as u32;
375 material_seen.insert(mat_name.clone(), idx);
376 referenced_materials.push(mat_name.clone());
377 }
378 }
379 }
380
381 let has_materials = !referenced_materials.is_empty();
382 let mut next_id: u32 = 1;
383
384 let materials_group_id = if has_materials {
386 let group_id = ResourceId(next_id);
387 next_id += 1;
388
389 let mut base_materials = Vec::new();
390 for mat_name in &referenced_materials {
391 let base_mat = if let Some(mtl_mat) = materials_map.get(mat_name) {
392 BaseMaterial {
393 name: mtl_mat.name.clone(),
394 display_color: mtl_mat.display_color,
395 }
396 } else {
397 eprintln!(
399 "Warning: undefined material '{}', using default gray",
400 mat_name
401 );
402 BaseMaterial {
403 name: mat_name.clone(),
404 display_color: DEFAULT_GRAY,
405 }
406 };
407 base_materials.push(base_mat);
408 }
409
410 model.resources.add_base_materials(BaseMaterialsGroup {
411 id: group_id,
412 materials: base_materials,
413 })?;
414
415 Some(group_id)
416 } else {
417 None
418 };
419
420 let single_object_mode = !intermediate.had_explicit_group && intermediate.groups.len() == 1;
422
423 if single_object_mode && !has_materials {
424 let group = &intermediate.groups[0];
426 let mesh = Self::build_mesh_full(&intermediate.global_vertices, group, None, None);
427
428 let resource_id = ResourceId(next_id);
429 let object = Object {
430 id: resource_id,
431 object_type: ObjectType::Model,
432 name: Some("OBJ Import".to_string()),
433 part_number: None,
434 uuid: None,
435 pid: None,
436 pindex: None,
437 thumbnail: None,
438 geometry: lib3mf_core::model::Geometry::Mesh(mesh),
439 };
440 model.resources.add_object(object)?;
441 model.build.items.push(BuildItem {
442 object_id: resource_id,
443 transform: glam::Mat4::IDENTITY,
444 part_number: None,
445 uuid: None,
446 path: None,
447 printable: None,
448 });
449 } else {
450 for group in &intermediate.groups {
452 let obj_id = ResourceId(next_id);
453 next_id += 1;
454
455 let mesh = Self::build_mesh_full(
456 &intermediate.global_vertices,
457 group,
458 materials_group_id,
459 Some(&material_seen),
460 );
461
462 let name = group
463 .name
464 .clone()
465 .unwrap_or_else(|| "OBJ Import".to_string());
466
467 let object = Object {
468 id: obj_id,
469 object_type: ObjectType::Model,
470 name: Some(name),
471 part_number: None,
472 uuid: None,
473 pid: None,
474 pindex: None,
475 thumbnail: None,
476 geometry: lib3mf_core::model::Geometry::Mesh(mesh),
477 };
478 model.resources.add_object(object)?;
479 model.build.items.push(BuildItem {
480 object_id: obj_id,
481 transform: glam::Mat4::IDENTITY,
482 part_number: None,
483 uuid: None,
484 path: None,
485 printable: None,
486 });
487 }
488 }
489
490 Ok(model)
491 }
492
493 fn build_model_compat(intermediate: ObjIntermediate) -> Result<Model> {
495 let mut model = Model::default();
496
497 let mut mesh = Mesh::default();
499 for &(x, y, z) in &intermediate.global_vertices {
500 mesh.add_vertex(x, y, z);
501 }
502 for group in &intermediate.groups {
503 for face in &group.faces {
504 if face.indices.len() == 3 {
505 mesh.triangles.push(Triangle {
506 v1: face.indices[0],
507 v2: face.indices[1],
508 v3: face.indices[2],
509 ..Default::default()
510 });
511 }
512 }
513 }
514
515 if mesh.vertices.is_empty() && mesh.triangles.is_empty() {
516 }
518
519 let resource_id = ResourceId(1);
520 let object = Object {
521 id: resource_id,
522 object_type: ObjectType::Model,
523 name: Some("OBJ Import".to_string()),
524 part_number: None,
525 uuid: None,
526 pid: None,
527 pindex: None,
528 thumbnail: None,
529 geometry: lib3mf_core::model::Geometry::Mesh(mesh),
530 };
531 let _ = model.resources.add_object(object);
532 model.build.items.push(BuildItem {
533 object_id: resource_id,
534 transform: glam::Mat4::IDENTITY,
535 part_number: None,
536 uuid: None,
537 path: None,
538 printable: None,
539 });
540
541 Ok(model)
542 }
543
544 fn build_mesh_full(
546 global_vertices: &[(f32, f32, f32)],
547 group: &ObjGroup,
548 materials_group_id: Option<ResourceId>,
549 material_index_map: Option<&HashMap<String, u32>>,
550 ) -> Mesh {
551 let mut mesh = Mesh::default();
552 let mut local_map: HashMap<u32, u32> = HashMap::new();
553
554 for face in &group.faces {
555 let mut local_indices = Vec::with_capacity(face.indices.len());
557 for &global_idx in &face.indices {
558 let local_idx = if let Some(&li) = local_map.get(&global_idx) {
559 li
560 } else {
561 let li = mesh.vertices.len() as u32;
562 local_map.insert(global_idx, li);
563 let (x, y, z) = global_vertices[global_idx as usize];
564 mesh.vertices.push(Vertex { x, y, z });
565 li
566 };
567 local_indices.push(local_idx);
568 }
569
570 if local_indices.len() == 3 {
572 let (pid, p1, p2, p3) = if let (Some(group_id), Some(index_map), Some(mat_name)) =
573 (materials_group_id, material_index_map, &face.material_name)
574 {
575 if let Some(&mat_idx) = index_map.get(mat_name.as_str()) {
576 (
577 Some(group_id.0),
578 Some(mat_idx),
579 Some(mat_idx),
580 Some(mat_idx),
581 )
582 } else {
583 (None, None, None, None)
584 }
585 } else {
586 (None, None, None, None)
587 };
588
589 mesh.triangles.push(Triangle {
590 v1: local_indices[0],
591 v2: local_indices[1],
592 v3: local_indices[2],
593 pid,
594 p1,
595 p2,
596 p3,
597 });
598 }
599 }
600
601 mesh
602 }
603}
604
605pub struct ObjExporter;
612
613impl ObjExporter {
614 pub fn write<W: Write>(model: &Model, mut writer: W) -> Result<()> {
663 let mut vertex_offset = 1;
664
665 for item in &model.build.items {
666 if let Some(object) = model.resources.get_object(item.object_id)
667 && let lib3mf_core::model::Geometry::Mesh(mesh) = &object.geometry
668 {
669 let transform = item.transform;
670
671 writeln!(writer, "g {}", object.name.as_deref().unwrap_or("Object"))
672 .map_err(Lib3mfError::Io)?;
673
674 for v in &mesh.vertices {
676 let p = transform.transform_point3(glam::Vec3::new(v.x, v.y, v.z));
677 writeln!(writer, "v {} {} {}", p.x, p.y, p.z).map_err(Lib3mfError::Io)?;
678 }
679
680 for tri in &mesh.triangles {
682 writeln!(
683 writer,
684 "f {} {} {}",
685 tri.v1 + vertex_offset,
686 tri.v2 + vertex_offset,
687 tri.v3 + vertex_offset
688 )
689 .map_err(Lib3mfError::Io)?;
690 }
691
692 vertex_offset += mesh.vertices.len() as u32;
693 }
694 }
695 Ok(())
696 }
697
698 pub fn write_with_resolver<W: Write, A: lib3mf_core::archive::ArchiveReader>(
707 model: &Model,
708 mut resolver: lib3mf_core::model::resolver::PartResolver<A>,
709 mut writer: W,
710 ) -> Result<()> {
711 let mut objects: Vec<(String, glam::Mat4, Mesh)> = Vec::new();
712
713 for item in &model.build.items {
714 let name = model
715 .resources
716 .get_object(item.object_id)
717 .and_then(|o| o.name.clone());
718 collect_obj_objects(
719 &mut resolver,
720 item.object_id,
721 item.transform,
722 None,
723 name,
724 &mut objects,
725 )?;
726 }
727
728 let mut vertex_offset: u32 = 1;
729 for (name, transform, mesh) in &objects {
730 writeln!(writer, "g {name}").map_err(Lib3mfError::Io)?;
731
732 for v in &mesh.vertices {
733 let p = transform.transform_point3(glam::Vec3::new(v.x, v.y, v.z));
734 writeln!(writer, "v {} {} {}", p.x, p.y, p.z).map_err(Lib3mfError::Io)?;
735 }
736
737 for tri in &mesh.triangles {
738 writeln!(
739 writer,
740 "f {} {} {}",
741 tri.v1 + vertex_offset,
742 tri.v2 + vertex_offset,
743 tri.v3 + vertex_offset
744 )
745 .map_err(Lib3mfError::Io)?;
746 }
747
748 vertex_offset += mesh.vertices.len() as u32;
749 }
750 Ok(())
751 }
752}
753
754fn collect_obj_objects<A: lib3mf_core::archive::ArchiveReader>(
755 resolver: &mut lib3mf_core::model::resolver::PartResolver<A>,
756 object_id: ResourceId,
757 transform: glam::Mat4,
758 path: Option<&str>,
759 name: Option<String>,
760 objects: &mut Vec<(String, glam::Mat4, Mesh)>,
761) -> Result<()> {
762 let (resolved_name, geometry) = {
763 let res = resolver.resolve_object(object_id, path)?;
764 if let Some((_, obj)) = res {
765 (obj.name.clone().or(name), Some(obj.geometry.clone()))
766 } else {
767 (name, None)
768 }
769 };
770
771 if let Some(geo) = geometry {
772 match geo {
773 lib3mf_core::model::Geometry::Mesh(mesh) => {
774 objects.push((
775 resolved_name.unwrap_or_else(|| "Object".to_string()),
776 transform,
777 mesh,
778 ));
779 }
780 lib3mf_core::model::Geometry::Components(comps) => {
781 for comp in comps.components {
782 let new_transform = transform * comp.transform;
783 let next_path_store = comp.path.clone();
784 let next_path = next_path_store.as_deref().or(path);
785 collect_obj_objects(
786 resolver,
787 comp.object_id,
788 new_transform,
789 next_path,
790 None,
791 objects,
792 )?;
793 }
794 }
795 _ => {}
796 }
797 }
798
799 Ok(())
800}
801
802#[cfg(test)]
803mod tests {
804 use super::*;
805 use lib3mf_core::model::Geometry;
806
807 fn bare_triangle_obj() -> &'static [u8] {
809 b"v 0.0 0.0 0.0\nv 1.0 0.0 0.0\nv 0.0 1.0 0.0\nf 1 2 3\n"
810 }
811
812 #[test]
813 fn test_backward_compat_bare_obj() {
814 let model = ObjImporter::read(&bare_triangle_obj()[..]).unwrap();
818
819 assert_eq!(model.build.items.len(), 1);
821 assert_eq!(model.build.items[0].object_id, ResourceId(1));
822
823 let obj = model.resources.get_object(ResourceId(1)).unwrap();
824 assert_eq!(obj.name.as_deref(), Some("OBJ Import"));
825 assert_eq!(obj.object_type, ObjectType::Model);
826
827 if let Geometry::Mesh(mesh) = &obj.geometry {
828 assert_eq!(mesh.vertices.len(), 3);
829 assert_eq!(mesh.triangles.len(), 1);
830 assert!(mesh.triangles[0].pid.is_none());
832 assert!(mesh.triangles[0].p1.is_none());
833 assert!(mesh.triangles[0].p2.is_none());
834 assert!(mesh.triangles[0].p3.is_none());
835 } else {
836 panic!("Expected mesh geometry");
837 }
838
839 assert_eq!(model.resources.base_material_groups_count(), 0);
841 }
842
843 #[test]
844 fn test_single_group_with_material() {
845 let obj_data = b"mtllib test.mtl\nv 0 0 0\nv 1 0 0\nv 0 1 0\nusemtl Red\nf 1 2 3\n";
847 let intermediate = ObjImporter::parse_obj(BufReader::new(&obj_data[..])).unwrap();
848
849 let mut materials = HashMap::new();
851 materials.insert(
852 "Red".to_string(),
853 mtl::MtlMaterial {
854 name: "Red".to_string(),
855 display_color: Color::new(255, 0, 0, 255),
856 },
857 );
858
859 let model = ObjImporter::build_model(intermediate, &materials).unwrap();
860
861 assert_eq!(model.resources.base_material_groups_count(), 1);
863 let bmg = model.resources.get_base_materials(ResourceId(1)).unwrap();
864 assert_eq!(bmg.materials.len(), 1);
865 assert_eq!(bmg.materials[0].name, "Red");
866 assert_eq!(bmg.materials[0].display_color, Color::new(255, 0, 0, 255));
867
868 assert_eq!(model.build.items.len(), 1);
870 let obj = model
871 .resources
872 .get_object(model.build.items[0].object_id)
873 .unwrap();
874
875 if let Geometry::Mesh(mesh) = &obj.geometry {
876 assert_eq!(mesh.triangles.len(), 1);
877 let tri = &mesh.triangles[0];
878 assert_eq!(tri.pid, Some(1)); assert_eq!(tri.p1, Some(0)); assert_eq!(tri.p2, Some(0));
881 assert_eq!(tri.p3, Some(0));
882 } else {
883 panic!("Expected mesh geometry");
884 }
885 }
886
887 #[test]
888 fn test_multiple_groups_creates_separate_objects() {
889 let obj_data = b"v 0 0 0\nv 1 0 0\nv 0 1 0\nv 2 0 0\nv 2 1 0\nv 3 0 0\ng GroupA\nf 1 2 3\ng GroupB\nf 4 5 6\n";
890 let intermediate = ObjImporter::parse_obj(BufReader::new(&obj_data[..])).unwrap();
891 let model = ObjImporter::build_model(intermediate, &HashMap::new()).unwrap();
892
893 assert_eq!(model.build.items.len(), 2);
895
896 let obj_a = model
898 .resources
899 .get_object(model.build.items[0].object_id)
900 .unwrap();
901 assert_eq!(obj_a.name.as_deref(), Some("GroupA"));
902
903 let obj_b = model
905 .resources
906 .get_object(model.build.items[1].object_id)
907 .unwrap();
908 assert_eq!(obj_b.name.as_deref(), Some("GroupB"));
909 }
910
911 #[test]
912 fn test_vertex_remapping_per_group() {
913 let obj_data = b"v 0 0 0\nv 1 0 0\nv 0 1 0\nv 10 0 0\nv 11 0 0\nv 10 1 0\ng A\nf 1 2 3\ng B\nf 4 5 6\n";
917 let intermediate = ObjImporter::parse_obj(BufReader::new(&obj_data[..])).unwrap();
918 let model = ObjImporter::build_model(intermediate, &HashMap::new()).unwrap();
919
920 let obj_a = model
922 .resources
923 .get_object(model.build.items[0].object_id)
924 .unwrap();
925 if let Geometry::Mesh(mesh) = &obj_a.geometry {
926 assert_eq!(mesh.vertices.len(), 3);
927 assert_eq!(mesh.triangles[0].v1, 0);
928 assert_eq!(mesh.triangles[0].v2, 1);
929 assert_eq!(mesh.triangles[0].v3, 2);
930 assert!((mesh.vertices[0].x - 0.0).abs() < f32::EPSILON);
932 assert!((mesh.vertices[1].x - 1.0).abs() < f32::EPSILON);
933 } else {
934 panic!("Expected mesh");
935 }
936
937 let obj_b = model
939 .resources
940 .get_object(model.build.items[1].object_id)
941 .unwrap();
942 if let Geometry::Mesh(mesh) = &obj_b.geometry {
943 assert_eq!(mesh.vertices.len(), 3);
944 assert_eq!(mesh.triangles[0].v1, 0);
945 assert_eq!(mesh.triangles[0].v2, 1);
946 assert_eq!(mesh.triangles[0].v3, 2);
947 assert!((mesh.vertices[0].x - 10.0).abs() < f32::EPSILON);
949 assert!((mesh.vertices[1].x - 11.0).abs() < f32::EPSILON);
950 } else {
951 panic!("Expected mesh");
952 }
953 }
954
955 #[test]
956 fn test_empty_groups_are_skipped() {
957 let obj_data = b"v 0 0 0\nv 1 0 0\nv 0 1 0\ng Empty\ng HasFaces\nf 1 2 3\n";
959 let intermediate = ObjImporter::parse_obj(BufReader::new(&obj_data[..])).unwrap();
960 let model = ObjImporter::build_model(intermediate, &HashMap::new()).unwrap();
961
962 assert_eq!(model.build.items.len(), 1);
964 let obj = model
965 .resources
966 .get_object(model.build.items[0].object_id)
967 .unwrap();
968 assert_eq!(obj.name.as_deref(), Some("HasFaces"));
969 }
970
971 #[test]
972 fn test_undefined_material_gets_gray() {
973 let obj_data = b"v 0 0 0\nv 1 0 0\nv 0 1 0\nusemtl Unknown\nf 1 2 3\n";
974 let intermediate = ObjImporter::parse_obj(BufReader::new(&obj_data[..])).unwrap();
975
976 let model = ObjImporter::build_model(intermediate, &HashMap::new()).unwrap();
978
979 assert_eq!(model.resources.base_material_groups_count(), 1);
981 let bmg = model.resources.get_base_materials(ResourceId(1)).unwrap();
982 assert_eq!(bmg.materials.len(), 1);
983 assert_eq!(bmg.materials[0].name, "Unknown");
984 assert_eq!(
985 bmg.materials[0].display_color,
986 Color::new(128, 128, 128, 255)
987 );
988 }
989
990 #[test]
991 fn test_polygon_fan_triangulation() {
992 let obj_data = b"v 0 0 0\nv 1 0 0\nv 1 1 0\nv 0 1 0\nf 1 2 3 4\n";
994 let model = ObjImporter::read(&obj_data[..]).unwrap();
995 let obj = model.resources.get_object(ResourceId(1)).unwrap();
996 if let Geometry::Mesh(mesh) = &obj.geometry {
997 assert_eq!(mesh.triangles.len(), 2);
998 assert_eq!(mesh.triangles[0].v1, 0);
1000 assert_eq!(mesh.triangles[0].v2, 1);
1001 assert_eq!(mesh.triangles[0].v3, 2);
1002 assert_eq!(mesh.triangles[1].v1, 0);
1004 assert_eq!(mesh.triangles[1].v2, 2);
1005 assert_eq!(mesh.triangles[1].v3, 3);
1006 } else {
1007 panic!("Expected mesh");
1008 }
1009 }
1010
1011 #[test]
1012 fn test_face_with_vt_vn_format() {
1013 let obj_data = b"v 0 0 0\nv 1 0 0\nv 0 1 0\nvt 0 0\nvt 1 0\nvt 0 1\nf 1/1/1 2/2/1 3/3/1\n";
1015 let model = ObjImporter::read(&obj_data[..]).unwrap();
1016 let obj = model.resources.get_object(ResourceId(1)).unwrap();
1017 if let Geometry::Mesh(mesh) = &obj.geometry {
1018 assert_eq!(mesh.triangles.len(), 1);
1019 assert_eq!(mesh.triangles[0].v1, 0);
1020 assert_eq!(mesh.triangles[0].v2, 1);
1021 assert_eq!(mesh.triangles[0].v3, 2);
1022 } else {
1023 panic!("Expected mesh");
1024 }
1025 }
1026
1027 #[test]
1028 fn test_multiple_materials_in_one_group() {
1029 let obj_data = b"v 0 0 0\nv 1 0 0\nv 0 1 0\nv 2 0 0\nv 2 1 0\nv 3 0 0\nusemtl Red\nf 1 2 3\nusemtl Blue\nf 4 5 6\n";
1031 let intermediate = ObjImporter::parse_obj(BufReader::new(&obj_data[..])).unwrap();
1032
1033 let mut materials = HashMap::new();
1034 materials.insert(
1035 "Red".to_string(),
1036 mtl::MtlMaterial {
1037 name: "Red".to_string(),
1038 display_color: Color::new(255, 0, 0, 255),
1039 },
1040 );
1041 materials.insert(
1042 "Blue".to_string(),
1043 mtl::MtlMaterial {
1044 name: "Blue".to_string(),
1045 display_color: Color::new(0, 0, 255, 255),
1046 },
1047 );
1048
1049 let model = ObjImporter::build_model(intermediate, &materials).unwrap();
1050
1051 let bmg = model.resources.get_base_materials(ResourceId(1)).unwrap();
1053 assert_eq!(bmg.materials.len(), 2);
1054
1055 assert_eq!(model.build.items.len(), 1);
1057 let obj = model
1058 .resources
1059 .get_object(model.build.items[0].object_id)
1060 .unwrap();
1061
1062 if let Geometry::Mesh(mesh) = &obj.geometry {
1063 assert_eq!(mesh.triangles.len(), 2);
1064 assert_eq!(mesh.triangles[0].pid, Some(1));
1066 assert_eq!(mesh.triangles[0].p1, Some(0));
1067 assert_eq!(mesh.triangles[1].pid, Some(1));
1069 assert_eq!(mesh.triangles[1].p1, Some(1));
1070 } else {
1071 panic!("Expected mesh");
1072 }
1073 }
1074
1075 #[test]
1076 fn test_faces_before_first_group() {
1077 let obj_data =
1079 b"v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\ng Named\nv 2 0 0\nv 2 1 0\nv 3 0 0\nf 4 5 6\n";
1080 let intermediate = ObjImporter::parse_obj(BufReader::new(&obj_data[..])).unwrap();
1081 let model = ObjImporter::build_model(intermediate, &HashMap::new()).unwrap();
1082
1083 assert_eq!(model.build.items.len(), 2);
1084 let obj0 = model
1086 .resources
1087 .get_object(model.build.items[0].object_id)
1088 .unwrap();
1089 assert_eq!(obj0.name.as_deref(), Some("OBJ Import")); let obj1 = model
1093 .resources
1094 .get_object(model.build.items[1].object_id)
1095 .unwrap();
1096 assert_eq!(obj1.name.as_deref(), Some("Named"));
1097 }
1098
1099 #[test]
1100 fn test_o_directive_treated_like_g() {
1101 let obj_data = b"v 0 0 0\nv 1 0 0\nv 0 1 0\no MyObject\nf 1 2 3\n";
1102 let intermediate = ObjImporter::parse_obj(BufReader::new(&obj_data[..])).unwrap();
1103 let model = ObjImporter::build_model(intermediate, &HashMap::new()).unwrap();
1104
1105 assert_eq!(model.build.items.len(), 1);
1106 let obj = model
1107 .resources
1108 .get_object(model.build.items[0].object_id)
1109 .unwrap();
1110 assert_eq!(obj.name.as_deref(), Some("MyObject"));
1111 }
1112}