1use crate::{MeshReader, MeshWriter};
12use std::collections::HashMap;
13use std::fs::File;
14use std::io::{BufRead, BufReader, BufWriter, Write};
15use std::path::{Path, PathBuf};
16use threecrate_core::{Error, Point3f, Result, TriangleMesh, Vector3f};
17
18#[derive(Debug, Clone)]
20pub struct Material {
21 pub name: String,
23 pub ambient: Option<[f32; 3]>,
25 pub diffuse: Option<[f32; 3]>,
27 pub specular: Option<[f32; 3]>,
29 pub shininess: Option<f32>,
31 pub transparency: Option<f32>,
33 pub illumination: Option<u32>,
35 pub diffuse_map: Option<String>,
37 pub normal_map: Option<String>,
39 pub specular_map: Option<String>,
41}
42
43#[derive(Debug, Clone, Copy)]
45pub struct FaceVertex {
46 pub vertex: usize,
48 pub texture: Option<usize>,
50 pub normal: Option<usize>,
52}
53
54#[derive(Debug, Clone)]
56pub struct Face {
57 pub vertices: Vec<FaceVertex>,
59 pub material: Option<String>,
61}
62
63#[derive(Debug, Clone)]
65pub struct Group {
66 pub name: String,
68 pub faces: Vec<Face>,
70}
71
72#[derive(Debug)]
74pub struct ObjData {
75 pub vertices: Vec<Point3f>,
77 pub texture_coords: Vec<[f32; 2]>,
79 pub normals: Vec<Vector3f>,
81 pub groups: Vec<Group>,
83 pub materials: HashMap<String, Material>,
85 pub mtl_files: Vec<String>,
87}
88
89pub struct RobustObjReader;
91
92pub struct ObjReader;
94
95#[derive(Debug, Clone)]
97pub struct ObjWriteOptions {
98 pub write_normals: bool,
100 pub write_texcoords: bool,
102 pub write_materials: bool,
104 pub comments: Vec<String>,
106 pub object_name: Option<String>,
108 pub group_name: Option<String>,
110 pub material_name: Option<String>,
112 pub mtl_filename: Option<String>,
114}
115
116impl Default for ObjWriteOptions {
117 fn default() -> Self {
118 Self {
119 write_normals: true,
120 write_texcoords: false,
121 write_materials: false,
122 comments: vec!["Generated by ThreeCrate".to_string()],
123 object_name: None,
124 group_name: None,
125 material_name: None,
126 mtl_filename: None,
127 }
128 }
129}
130
131impl ObjWriteOptions {
132 pub fn new() -> Self {
134 Self::default()
135 }
136
137 pub fn with_normals(mut self, write_normals: bool) -> Self {
139 self.write_normals = write_normals;
140 self
141 }
142
143 pub fn with_texcoords(mut self, write_texcoords: bool) -> Self {
145 self.write_texcoords = write_texcoords;
146 self
147 }
148
149 pub fn with_materials(mut self, write_materials: bool) -> Self {
151 self.write_materials = write_materials;
152 self
153 }
154
155 pub fn with_comment<S: Into<String>>(mut self, comment: S) -> Self {
157 self.comments.push(comment.into());
158 self
159 }
160
161 pub fn with_object_name<S: Into<String>>(mut self, name: S) -> Self {
163 self.object_name = Some(name.into());
164 self
165 }
166
167 pub fn with_group_name<S: Into<String>>(mut self, name: S) -> Self {
169 self.group_name = Some(name.into());
170 self
171 }
172
173 pub fn with_material_name<S: Into<String>>(mut self, name: S) -> Self {
175 self.material_name = Some(name.into());
176 self
177 }
178
179 pub fn with_mtl_filename<S: Into<String>>(mut self, filename: S) -> Self {
181 self.mtl_filename = Some(filename.into());
182 self
183 }
184}
185
186pub struct RobustObjWriter;
188
189pub struct ObjWriter;
191
192impl Material {
193 pub fn new(name: String) -> Self {
195 Self {
196 name,
197 ambient: None,
198 diffuse: None,
199 specular: None,
200 shininess: None,
201 transparency: None,
202 illumination: None,
203 diffuse_map: None,
204 normal_map: None,
205 specular_map: None,
206 }
207 }
208}
209
210impl Default for Material {
211 fn default() -> Self {
212 Self::new("default".to_string())
213 }
214}
215
216impl FaceVertex {
217 fn parse(s: &str) -> Result<Self> {
219 let parts: Vec<&str> = s.split('/').collect();
220
221 if parts.is_empty() {
222 return Err(Error::InvalidData("Empty face vertex".to_string()));
223 }
224
225 let vertex = parts[0]
226 .parse::<usize>()
227 .map_err(|_| Error::InvalidData(format!("Invalid vertex index: {}", parts[0])))?;
228
229 let vertex = vertex
231 .checked_sub(1)
232 .ok_or_else(|| Error::InvalidData("Vertex index cannot be 0".to_string()))?;
233
234 let texture = if parts.len() > 1 && !parts[1].is_empty() {
235 let tex_idx = parts[1]
236 .parse::<usize>()
237 .map_err(|_| Error::InvalidData(format!("Invalid texture index: {}", parts[1])))?;
238 Some(
239 tex_idx
240 .checked_sub(1)
241 .ok_or_else(|| Error::InvalidData("Texture index cannot be 0".to_string()))?,
242 )
243 } else {
244 None
245 };
246
247 let normal = if parts.len() > 2 && !parts[2].is_empty() {
248 let norm_idx = parts[2]
249 .parse::<usize>()
250 .map_err(|_| Error::InvalidData(format!("Invalid normal index: {}", parts[2])))?;
251 Some(
252 norm_idx
253 .checked_sub(1)
254 .ok_or_else(|| Error::InvalidData("Normal index cannot be 0".to_string()))?,
255 )
256 } else {
257 None
258 };
259
260 Ok(FaceVertex {
261 vertex,
262 texture,
263 normal,
264 })
265 }
266}
267
268impl crate::registry::MeshReader for ObjReader {
269 fn read_mesh(&self, path: &Path) -> Result<TriangleMesh> {
270 let obj_data = RobustObjReader::read_obj_file(path)?;
271 RobustObjReader::obj_data_to_mesh(&obj_data)
272 }
273
274 fn can_read(&self, path: &Path) -> bool {
275 if let Ok(file) = File::open(path) {
277 let mut reader = BufReader::new(file);
278 let mut line = String::new();
279 if let Ok(_) = reader.read_line(&mut line) {
280 let trimmed = line.trim();
281 return trimmed.starts_with("#") || trimmed.starts_with("v ");
282 }
283 }
284 false
285 }
286
287 fn format_name(&self) -> &'static str {
288 "obj"
289 }
290}
291
292impl crate::registry::MeshWriter for ObjWriter {
293 fn write_mesh(&self, mesh: &TriangleMesh, path: &Path) -> Result<()> {
294 let file = File::create(path)?;
295 let mut writer = BufWriter::new(file);
296
297 writeln!(writer, "# OBJ file generated by ThreeCrate")?;
299 writeln!(writer, "# Vertices: {}", mesh.vertices.len())?;
300 writeln!(writer, "# Faces: {}", mesh.faces.len())?;
301 writeln!(writer)?;
302
303 for vertex in &mesh.vertices {
305 writeln!(writer, "v {} {} {}", vertex.x, vertex.y, vertex.z)?;
306 }
307 writeln!(writer)?;
308
309 if let Some(normals) = &mesh.normals {
311 for normal in normals {
312 writeln!(writer, "vn {} {} {}", normal.x, normal.y, normal.z)?;
313 }
314 writeln!(writer)?;
315 }
316
317 if mesh.normals.is_some() {
319 for face in &mesh.faces {
321 writeln!(
322 writer,
323 "f {}//{} {}//{} {}//{}",
324 face[0] + 1,
325 face[0] + 1,
326 face[1] + 1,
327 face[1] + 1,
328 face[2] + 1,
329 face[2] + 1
330 )?;
331 }
332 } else {
333 for face in &mesh.faces {
335 writeln!(writer, "f {} {} {}", face[0] + 1, face[1] + 1, face[2] + 1)?;
336 }
337 }
338
339 Ok(())
340 }
341
342 fn format_name(&self) -> &'static str {
343 "obj"
344 }
345}
346
347impl MeshReader for ObjReader {
349 fn read_mesh<P: AsRef<Path>>(path: P) -> Result<TriangleMesh> {
350 let reader = ObjReader;
351 crate::registry::MeshReader::read_mesh(&reader, path.as_ref())
352 }
353}
354
355impl MeshWriter for ObjWriter {
356 fn write_mesh<P: AsRef<Path>>(mesh: &TriangleMesh, path: P) -> Result<()> {
357 let writer = ObjWriter;
358 crate::registry::MeshWriter::write_mesh(&writer, mesh, path.as_ref())
359 }
360}
361
362pub fn read_obj_vertices<P: AsRef<Path>>(path: P) -> Result<Vec<Point3f>> {
364 let obj_data = RobustObjReader::read_obj_file(path)?;
365 Ok(obj_data.vertices)
366}
367
368pub fn write_obj_vertices<P: AsRef<Path>>(vertices: &[Point3f], path: P) -> Result<()> {
370 let file = File::create(path)?;
371 let mut writer = BufWriter::new(file);
372
373 writeln!(writer, "# OBJ vertices file generated by ThreeCrate")?;
374 writeln!(writer, "# Vertices: {}", vertices.len())?;
375 writeln!(writer)?;
376
377 for vertex in vertices {
378 writeln!(writer, "v {} {} {}", vertex.x, vertex.y, vertex.z)?;
379 }
380
381 Ok(())
382}
383
384impl RobustObjReader {
385 pub fn read_obj_file<P: AsRef<Path>>(path: P) -> Result<ObjData> {
387 let path = path.as_ref();
388 let file = File::open(path).map_err(|e| Error::Io(e))?;
389 let reader = BufReader::new(file);
390
391 let mut obj_data = ObjData {
392 vertices: Vec::new(),
393 texture_coords: Vec::new(),
394 normals: Vec::new(),
395 groups: Vec::new(),
396 materials: HashMap::new(),
397 mtl_files: Vec::new(),
398 };
399
400 let mut current_group = Group {
401 name: "default".to_string(),
402 faces: Vec::new(),
403 };
404 let mut current_material: Option<String> = None;
405
406 for (line_num, line) in reader.lines().enumerate() {
407 let line = line.map_err(|e| Error::Io(e))?;
408 let line = line.trim();
409
410 if line.is_empty() || line.starts_with('#') {
412 continue;
413 }
414
415 let parts: Vec<&str> = line.split_whitespace().collect();
416 if parts.is_empty() {
417 continue;
418 }
419
420 match parts[0] {
421 "v" => {
422 if parts.len() < 4 {
424 return Err(Error::InvalidData(format!(
425 "Invalid vertex at line {}: expected 3 coordinates",
426 line_num + 1
427 )));
428 }
429
430 let x = parts[1].parse::<f32>().map_err(|_| {
431 Error::InvalidData(format!("Invalid x coordinate at line {}", line_num + 1))
432 })?;
433 let y = parts[2].parse::<f32>().map_err(|_| {
434 Error::InvalidData(format!("Invalid y coordinate at line {}", line_num + 1))
435 })?;
436 let z = parts[3].parse::<f32>().map_err(|_| {
437 Error::InvalidData(format!("Invalid z coordinate at line {}", line_num + 1))
438 })?;
439
440 obj_data.vertices.push(Point3f::new(x, y, z));
441 }
442 "vt" => {
443 if parts.len() < 3 {
445 return Err(Error::InvalidData(format!(
446 "Invalid texture coordinate at line {}: expected 2 coordinates",
447 line_num + 1
448 )));
449 }
450
451 let u = parts[1].parse::<f32>().map_err(|_| {
452 Error::InvalidData(format!("Invalid u coordinate at line {}", line_num + 1))
453 })?;
454 let v = parts[2].parse::<f32>().map_err(|_| {
455 Error::InvalidData(format!("Invalid v coordinate at line {}", line_num + 1))
456 })?;
457
458 obj_data.texture_coords.push([u, v]);
459 }
460 "vn" => {
461 if parts.len() < 4 {
463 return Err(Error::InvalidData(format!(
464 "Invalid normal at line {}: expected 3 components",
465 line_num + 1
466 )));
467 }
468
469 let x = parts[1].parse::<f32>().map_err(|_| {
470 Error::InvalidData(format!("Invalid normal x at line {}", line_num + 1))
471 })?;
472 let y = parts[2].parse::<f32>().map_err(|_| {
473 Error::InvalidData(format!("Invalid normal y at line {}", line_num + 1))
474 })?;
475 let z = parts[3].parse::<f32>().map_err(|_| {
476 Error::InvalidData(format!("Invalid normal z at line {}", line_num + 1))
477 })?;
478
479 obj_data.normals.push(Vector3f::new(x, y, z));
480 }
481 "f" => {
482 if parts.len() < 4 {
484 return Err(Error::InvalidData(format!(
485 "Invalid face at line {}: expected at least 3 vertices",
486 line_num + 1
487 )));
488 }
489
490 let mut face_vertices = Vec::new();
491 for vertex_str in &parts[1..] {
492 let face_vertex = FaceVertex::parse(vertex_str).map_err(|e| {
493 Error::InvalidData(format!(
494 "Invalid face vertex '{}' at line {}: {}",
495 vertex_str,
496 line_num + 1,
497 e
498 ))
499 })?;
500 face_vertices.push(face_vertex);
501 }
502
503 let triangulated_faces = Self::triangulate_face(&face_vertices);
505 for triangle in triangulated_faces {
506 current_group.faces.push(Face {
507 vertices: triangle,
508 material: current_material.clone(),
509 });
510 }
511 }
512 "g" => {
513 if !current_group.faces.is_empty() || current_group.name != "default" {
515 obj_data.groups.push(current_group);
516 }
517
518 let group_name = if parts.len() > 1 {
519 parts[1..].join(" ")
520 } else {
521 format!("group_{}", obj_data.groups.len())
522 };
523
524 current_group = Group {
525 name: group_name,
526 faces: Vec::new(),
527 };
528 }
529 "usemtl" => {
530 if parts.len() > 1 {
532 current_material = Some(parts[1].to_string());
533 }
534 }
535 "mtllib" => {
536 if parts.len() > 1 {
538 let mtl_file = parts[1].to_string();
539 obj_data.mtl_files.push(mtl_file.clone());
540
541 let mtl_path = if let Some(parent) = path.parent() {
543 parent.join(&mtl_file)
544 } else {
545 PathBuf::from(&mtl_file)
546 };
547
548 if let Ok(materials) = Self::read_mtl_file(&mtl_path) {
549 obj_data.materials.extend(materials);
550 }
551 }
552 }
553 _ => {
554 }
556 }
557 }
558
559 if !current_group.faces.is_empty() {
561 obj_data.groups.push(current_group);
562 }
563
564 if obj_data.groups.is_empty() {
566 obj_data.groups.push(Group {
567 name: "default".to_string(),
568 faces: Vec::new(),
569 });
570 }
571
572 Ok(obj_data)
573 }
574
575 pub fn read_mtl_file<P: AsRef<Path>>(path: P) -> Result<HashMap<String, Material>> {
577 let file = File::open(path).map_err(|e| Error::Io(e))?;
578 let reader = BufReader::new(file);
579
580 let mut materials = HashMap::new();
581 let mut current_material: Option<Material> = None;
582
583 for (_line_num, line) in reader.lines().enumerate() {
584 let line = line.map_err(|e| Error::Io(e))?;
585 let line = line.trim();
586
587 if line.is_empty() || line.starts_with('#') {
589 continue;
590 }
591
592 let parts: Vec<&str> = line.split_whitespace().collect();
593 if parts.is_empty() {
594 continue;
595 }
596
597 match parts[0] {
598 "newmtl" => {
599 if let Some(material) = current_material.take() {
601 materials.insert(material.name.clone(), material);
602 }
603
604 if parts.len() > 1 {
606 current_material = Some(Material::new(parts[1].to_string()));
607 }
608 }
609 "Ka" => {
610 if let Some(ref mut material) = current_material {
612 if parts.len() >= 4 {
613 if let (Ok(r), Ok(g), Ok(b)) = (
614 parts[1].parse::<f32>(),
615 parts[2].parse::<f32>(),
616 parts[3].parse::<f32>(),
617 ) {
618 material.ambient = Some([r, g, b]);
619 }
620 }
621 }
622 }
623 "Kd" => {
624 if let Some(ref mut material) = current_material {
626 if parts.len() >= 4 {
627 if let (Ok(r), Ok(g), Ok(b)) = (
628 parts[1].parse::<f32>(),
629 parts[2].parse::<f32>(),
630 parts[3].parse::<f32>(),
631 ) {
632 material.diffuse = Some([r, g, b]);
633 }
634 }
635 }
636 }
637 "Ks" => {
638 if let Some(ref mut material) = current_material {
640 if parts.len() >= 4 {
641 if let (Ok(r), Ok(g), Ok(b)) = (
642 parts[1].parse::<f32>(),
643 parts[2].parse::<f32>(),
644 parts[3].parse::<f32>(),
645 ) {
646 material.specular = Some([r, g, b]);
647 }
648 }
649 }
650 }
651 "Ns" => {
652 if let Some(ref mut material) = current_material {
654 if parts.len() > 1 {
655 if let Ok(ns) = parts[1].parse::<f32>() {
656 material.shininess = Some(ns);
657 }
658 }
659 }
660 }
661 "d" => {
662 if let Some(ref mut material) = current_material {
664 if parts.len() > 1 {
665 if let Ok(d) = parts[1].parse::<f32>() {
666 material.transparency = Some(d);
667 }
668 }
669 }
670 }
671 "Tr" => {
672 if let Some(ref mut material) = current_material {
674 if parts.len() > 1 {
675 if let Ok(tr) = parts[1].parse::<f32>() {
676 material.transparency = Some(1.0 - tr); }
678 }
679 }
680 }
681 "illum" => {
682 if let Some(ref mut material) = current_material {
684 if parts.len() > 1 {
685 if let Ok(illum) = parts[1].parse::<u32>() {
686 material.illumination = Some(illum);
687 }
688 }
689 }
690 }
691 "map_Kd" => {
692 if let Some(ref mut material) = current_material {
694 if parts.len() > 1 {
695 material.diffuse_map = Some(parts[1..].join(" "));
696 }
697 }
698 }
699 "map_Bump" | "bump" => {
700 if let Some(ref mut material) = current_material {
702 if parts.len() > 1 {
703 material.normal_map = Some(parts[1..].join(" "));
704 }
705 }
706 }
707 "map_Ks" => {
708 if let Some(ref mut material) = current_material {
710 if parts.len() > 1 {
711 material.specular_map = Some(parts[1..].join(" "));
712 }
713 }
714 }
715 _ => {
716 }
718 }
719 }
720
721 if let Some(material) = current_material {
723 materials.insert(material.name.clone(), material);
724 }
725
726 Ok(materials)
727 }
728
729 fn triangulate_face(vertices: &[FaceVertex]) -> Vec<Vec<FaceVertex>> {
731 match vertices.len() {
732 3 => {
733 vec![vertices.to_vec()]
735 }
736 4 => {
737 vec![
739 vec![vertices[0], vertices[1], vertices[2]],
740 vec![vertices[0], vertices[2], vertices[3]],
741 ]
742 }
743 n if n > 4 => {
744 let mut triangles = Vec::new();
746 for i in 1..(n - 1) {
747 triangles.push(vec![vertices[0], vertices[i], vertices[i + 1]]);
748 }
749 triangles
750 }
751 _ => {
752 vec![]
754 }
755 }
756 }
757
758 pub fn obj_data_to_mesh(obj_data: &ObjData) -> Result<TriangleMesh> {
760 let mut mesh_faces = Vec::new();
761 let mut mesh_normals = Vec::new();
762 let mut has_normals = false;
763
764 for group in &obj_data.groups {
766 for face in &group.faces {
767 if face.vertices.len() != 3 {
768 continue; }
770
771 let face_indices = [
773 face.vertices[0].vertex,
774 face.vertices[1].vertex,
775 face.vertices[2].vertex,
776 ];
777
778 for &idx in &face_indices {
780 if idx >= obj_data.vertices.len() {
781 return Err(Error::InvalidData(format!(
782 "Vertex index {} out of range (max: {})",
783 idx,
784 obj_data.vertices.len() - 1
785 )));
786 }
787 }
788
789 mesh_faces.push(face_indices);
790
791 if !obj_data.normals.is_empty() {
793 for vertex in &face.vertices {
794 if let Some(normal_idx) = vertex.normal {
795 if normal_idx >= obj_data.normals.len() {
796 return Err(Error::InvalidData(format!(
797 "Normal index {} out of range (max: {})",
798 normal_idx,
799 obj_data.normals.len() - 1
800 )));
801 }
802 mesh_normals.push(obj_data.normals[normal_idx]);
803 has_normals = true;
804 } else if has_normals {
805 mesh_normals.push(Vector3f::new(0.0, 0.0, 1.0));
808 }
809 }
810 }
811 }
812 }
813
814 let mut mesh = TriangleMesh::from_vertices_and_faces(obj_data.vertices.clone(), mesh_faces);
815
816 if has_normals && mesh_normals.len() == mesh.vertices.len() {
818 mesh.set_normals(mesh_normals);
819 }
820
821 Ok(mesh)
822 }
823}
824
825impl RobustObjWriter {
826 pub fn write_obj_file<P: AsRef<Path>>(
828 obj_data: &ObjData,
829 path: P,
830 options: &ObjWriteOptions,
831 ) -> Result<()> {
832 let path = path.as_ref();
833 let file = File::create(path)?;
834 let mut writer = BufWriter::new(file);
835
836 for comment in &options.comments {
838 writeln!(writer, "# {}", comment)?;
839 }
840 writeln!(writer, "# Vertices: {}", obj_data.vertices.len())?;
841 writeln!(
842 writer,
843 "# Texture coordinates: {}",
844 obj_data.texture_coords.len()
845 )?;
846 writeln!(writer, "# Normals: {}", obj_data.normals.len())?;
847 writeln!(writer, "# Groups: {}", obj_data.groups.len())?;
848 writeln!(writer)?;
849
850 if options.write_materials && !obj_data.materials.is_empty() {
852 let default_mtl_filename = Self::get_mtl_filename(path);
853 let mtl_filename = options
854 .mtl_filename
855 .as_ref()
856 .unwrap_or(&default_mtl_filename);
857 writeln!(writer, "mtllib {}", mtl_filename)?;
858 writeln!(writer)?;
859 }
860
861 if let Some(ref object_name) = options.object_name {
863 writeln!(writer, "o {}", object_name)?;
864 writeln!(writer)?;
865 }
866
867 for vertex in &obj_data.vertices {
869 writeln!(writer, "v {} {} {}", vertex.x, vertex.y, vertex.z)?;
870 }
871 if !obj_data.vertices.is_empty() {
872 writeln!(writer)?;
873 }
874
875 if options.write_texcoords && !obj_data.texture_coords.is_empty() {
877 for tex_coord in &obj_data.texture_coords {
878 writeln!(writer, "vt {} {}", tex_coord[0], tex_coord[1])?;
879 }
880 writeln!(writer)?;
881 }
882
883 if options.write_normals && !obj_data.normals.is_empty() {
885 for normal in &obj_data.normals {
886 writeln!(writer, "vn {} {} {}", normal.x, normal.y, normal.z)?;
887 }
888 writeln!(writer)?;
889 }
890
891 for group in &obj_data.groups {
893 let group_name = if group.name == "default" && options.group_name.is_some() {
895 options.group_name.as_ref().unwrap()
896 } else if group.name != "default" {
897 &group.name
898 } else {
899 "default"
900 };
901
902 if group_name != "default" || options.group_name.is_some() {
903 writeln!(writer, "g {}", group_name)?;
904 }
905
906 let mut current_material: Option<&String> = None;
908
909 for face in &group.faces {
910 if let Some(ref material) = face.material {
912 if current_material != Some(material) {
913 writeln!(writer, "usemtl {}", material)?;
914 current_material = Some(material);
915 }
916 } else if let Some(ref material_name) = options.material_name {
917 if current_material.is_none() {
918 writeln!(writer, "usemtl {}", material_name)?;
919 current_material = Some(material_name);
920 }
921 }
922
923 write!(writer, "f")?;
925 for vertex in &face.vertices {
926 write!(writer, " {}", vertex.vertex + 1)?; if options.write_texcoords && !obj_data.texture_coords.is_empty() {
930 if let Some(tex_idx) = vertex.texture {
931 write!(writer, "/{}", tex_idx + 1)?;
932 } else {
933 write!(writer, "/")?;
934 }
935 }
936
937 if options.write_normals && !obj_data.normals.is_empty() {
939 if !options.write_texcoords || obj_data.texture_coords.is_empty() {
940 write!(writer, "//")?;
941 } else {
942 write!(writer, "/")?;
943 }
944
945 if let Some(norm_idx) = vertex.normal {
946 write!(writer, "{}", norm_idx + 1)?;
947 }
948 }
949 }
950 writeln!(writer)?;
951 }
952
953 if !group.faces.is_empty() {
954 writeln!(writer)?;
955 }
956 }
957
958 if options.write_materials && !obj_data.materials.is_empty() {
960 let default_mtl_filename = Self::get_mtl_filename(path);
961 let mtl_filename = options
962 .mtl_filename
963 .as_ref()
964 .unwrap_or(&default_mtl_filename);
965 let mtl_path = path.parent().unwrap_or(Path::new(".")).join(mtl_filename);
966 Self::write_mtl_file(&obj_data.materials, &mtl_path)?;
967 }
968
969 Ok(())
970 }
971
972 pub fn write_mesh<P: AsRef<Path>>(
974 mesh: &TriangleMesh,
975 path: P,
976 options: &ObjWriteOptions,
977 ) -> Result<()> {
978 let obj_data = Self::mesh_to_obj_data(mesh, options)?;
980 Self::write_obj_file(&obj_data, path, options)
981 }
982
983 pub fn write_mtl_file<P: AsRef<Path>>(
985 materials: &HashMap<String, Material>,
986 path: P,
987 ) -> Result<()> {
988 let file = File::create(path)?;
989 let mut writer = BufWriter::new(file);
990
991 writeln!(writer, "# MTL file generated by ThreeCrate")?;
992 writeln!(writer, "# Materials: {}", materials.len())?;
993 writeln!(writer)?;
994
995 for (name, material) in materials {
996 writeln!(writer, "newmtl {}", name)?;
997
998 if let Some(ambient) = material.ambient {
1000 writeln!(writer, "Ka {} {} {}", ambient[0], ambient[1], ambient[2])?;
1001 }
1002
1003 if let Some(diffuse) = material.diffuse {
1005 writeln!(writer, "Kd {} {} {}", diffuse[0], diffuse[1], diffuse[2])?;
1006 }
1007
1008 if let Some(specular) = material.specular {
1010 writeln!(writer, "Ks {} {} {}", specular[0], specular[1], specular[2])?;
1011 }
1012
1013 if let Some(shininess) = material.shininess {
1015 writeln!(writer, "Ns {}", shininess)?;
1016 }
1017
1018 if let Some(transparency) = material.transparency {
1020 writeln!(writer, "d {}", transparency)?;
1021 }
1022
1023 if let Some(illumination) = material.illumination {
1025 writeln!(writer, "illum {}", illumination)?;
1026 }
1027
1028 if let Some(ref diffuse_map) = material.diffuse_map {
1030 writeln!(writer, "map_Kd {}", diffuse_map)?;
1031 }
1032
1033 if let Some(ref normal_map) = material.normal_map {
1034 writeln!(writer, "map_Bump {}", normal_map)?;
1035 }
1036
1037 if let Some(ref specular_map) = material.specular_map {
1038 writeln!(writer, "map_Ks {}", specular_map)?;
1039 }
1040
1041 writeln!(writer)?;
1042 }
1043
1044 Ok(())
1045 }
1046
1047 fn mesh_to_obj_data(mesh: &TriangleMesh, options: &ObjWriteOptions) -> Result<ObjData> {
1049 let mut obj_data = ObjData {
1050 vertices: mesh.vertices.clone(),
1051 texture_coords: Vec::new(),
1052 normals: Vec::new(),
1053 groups: Vec::new(),
1054 materials: HashMap::new(),
1055 mtl_files: Vec::new(),
1056 };
1057
1058 if options.write_normals {
1060 if let Some(ref normals) = mesh.normals {
1061 obj_data.normals = normals.clone();
1062 }
1063 }
1064
1065 let mut faces = Vec::new();
1067 for face_indices in &mesh.faces {
1068 let mut face_vertices = Vec::new();
1069 for &vertex_idx in face_indices {
1070 let face_vertex = FaceVertex {
1071 vertex: vertex_idx,
1072 texture: None, normal: if options.write_normals && mesh.normals.is_some() {
1074 Some(vertex_idx) } else {
1076 None
1077 },
1078 };
1079 face_vertices.push(face_vertex);
1080 }
1081
1082 faces.push(Face {
1083 vertices: face_vertices,
1084 material: options.material_name.clone(),
1085 });
1086 }
1087
1088 let group_name = options
1090 .group_name
1091 .clone()
1092 .unwrap_or_else(|| "default".to_string());
1093 obj_data.groups.push(Group {
1094 name: group_name,
1095 faces,
1096 });
1097
1098 if let Some(ref material_name) = options.material_name {
1100 let material = Material::new(material_name.clone());
1101 obj_data.materials.insert(material_name.clone(), material);
1102 }
1103
1104 Ok(obj_data)
1105 }
1106
1107 fn get_mtl_filename(obj_path: &Path) -> String {
1109 obj_path
1110 .file_stem()
1111 .and_then(|s| s.to_str())
1112 .map(|s| format!("{}.mtl", s))
1113 .unwrap_or_else(|| "materials.mtl".to_string())
1114 }
1115}
1116
1117pub struct ObjStreamingReader {
1119 reader: BufReader<File>,
1120 current_line: usize,
1121 total_vertices: usize,
1122 vertices_read: usize,
1123 chunk_size: usize,
1124 buffer: Vec<String>,
1125}
1126
1127impl ObjStreamingReader {
1128 pub fn new<P: AsRef<Path>>(path: P, chunk_size: usize) -> Result<Self> {
1130 let path = path.as_ref();
1131 let file = File::open(path)?;
1132 let mut reader = BufReader::new(file);
1133
1134 let mut total_vertices = 0;
1136 let mut line = String::new();
1137 while reader.read_line(&mut line)? > 0 {
1138 let trimmed = line.trim();
1139 if trimmed.starts_with("v ")
1140 && !trimmed.starts_with("vt ")
1141 && !trimmed.starts_with("vn ")
1142 {
1143 total_vertices += 1;
1144 }
1145 line.clear();
1146 }
1147
1148 let file = File::open(path)?;
1150 let reader = BufReader::new(file);
1151
1152 Ok(Self {
1153 reader,
1154 current_line: 0,
1155 total_vertices,
1156 vertices_read: 0,
1157 chunk_size,
1158 buffer: Vec::with_capacity(chunk_size),
1159 })
1160 }
1161}
1162
1163impl Iterator for ObjStreamingReader {
1164 type Item = Result<Point3f>;
1165
1166 fn next(&mut self) -> Option<Self::Item> {
1167 if self.vertices_read >= self.total_vertices {
1168 return None;
1169 }
1170
1171 if self.buffer.is_empty() {
1173 let remaining = self.total_vertices - self.vertices_read;
1174 let to_read = std::cmp::min(remaining, self.chunk_size);
1175
1176 for _ in 0..to_read {
1177 let mut line = String::new();
1178 loop {
1179 match self.reader.read_line(&mut line) {
1180 Ok(0) => return None, Ok(_) => {
1182 self.current_line += 1;
1183 let trimmed = line.trim();
1184
1185 if trimmed.starts_with("v ")
1187 && !trimmed.starts_with("vt ")
1188 && !trimmed.starts_with("vn ")
1189 {
1190 self.buffer.push(line.clone());
1191 line.clear();
1192 break;
1193 }
1194 line.clear();
1195 }
1196 Err(e) => return Some(Err(Error::Io(e))),
1197 }
1198 }
1199 }
1200 }
1201
1202 if !self.buffer.is_empty() {
1204 let vertex_line = self.buffer.remove(0);
1205 let parts: Vec<&str> = vertex_line.trim().split_whitespace().collect();
1206 if parts.len() < 4 {
1207 return Some(Err(Error::InvalidData(format!(
1208 "Invalid vertex at line {}: expected 3 coordinates",
1209 self.current_line
1210 ))));
1211 }
1212
1213 let x = match parts[1].parse::<f32>() {
1214 Ok(v) => v,
1215 Err(_) => {
1216 return Some(Err(Error::InvalidData(format!(
1217 "Invalid x coordinate at line {}",
1218 self.current_line
1219 ))))
1220 }
1221 };
1222 let y = match parts[2].parse::<f32>() {
1223 Ok(v) => v,
1224 Err(_) => {
1225 return Some(Err(Error::InvalidData(format!(
1226 "Invalid y coordinate at line {}",
1227 self.current_line
1228 ))))
1229 }
1230 };
1231 let z = match parts[3].parse::<f32>() {
1232 Ok(v) => v,
1233 Err(_) => {
1234 return Some(Err(Error::InvalidData(format!(
1235 "Invalid z coordinate at line {}",
1236 self.current_line
1237 ))))
1238 }
1239 };
1240
1241 self.vertices_read += 1;
1242 Some(Ok(Point3f::new(x, y, z)))
1243 } else {
1244 None
1245 }
1246 }
1247}
1248
1249pub struct ObjMeshStreamingReader {
1251 reader: BufReader<File>,
1252 current_line: usize,
1253 total_faces: usize,
1254 faces_read: usize,
1255 chunk_size: usize,
1256 buffer: Vec<String>,
1257 vertices: Vec<Point3f>, }
1259
1260impl ObjMeshStreamingReader {
1261 pub fn new<P: AsRef<Path>>(path: P, chunk_size: usize) -> Result<Self> {
1263 let path = path.as_ref();
1264 let file = File::open(path)?;
1265 let mut reader = BufReader::new(file);
1266
1267 let mut vertices = Vec::new();
1269 let mut total_faces = 0;
1270 let mut line = String::new();
1271
1272 while reader.read_line(&mut line)? > 0 {
1273 let trimmed = line.trim();
1274 if trimmed.starts_with("v ")
1275 && !trimmed.starts_with("vt ")
1276 && !trimmed.starts_with("vn ")
1277 {
1278 let parts: Vec<&str> = trimmed.split_whitespace().collect();
1279 if parts.len() >= 4 {
1280 if let (Ok(x), Ok(y), Ok(z)) = (
1281 parts[1].parse::<f32>(),
1282 parts[2].parse::<f32>(),
1283 parts[3].parse::<f32>(),
1284 ) {
1285 vertices.push(Point3f::new(x, y, z));
1286 }
1287 }
1288 } else if trimmed.starts_with("f ") {
1289 total_faces += 1;
1290 }
1291 line.clear();
1292 }
1293
1294 let file = File::open(path)?;
1296 let reader = BufReader::new(file);
1297
1298 Ok(Self {
1299 reader,
1300 current_line: 0,
1301 total_faces,
1302 faces_read: 0,
1303 chunk_size,
1304 buffer: Vec::with_capacity(chunk_size),
1305 vertices,
1306 })
1307 }
1308}
1309
1310impl Iterator for ObjMeshStreamingReader {
1311 type Item = Result<[usize; 3]>;
1312
1313 fn next(&mut self) -> Option<Self::Item> {
1314 if self.faces_read >= self.total_faces {
1315 return None;
1316 }
1317
1318 if self.buffer.is_empty() {
1320 let remaining = self.total_faces - self.faces_read;
1321 let to_read = std::cmp::min(remaining, self.chunk_size);
1322
1323 for _ in 0..to_read {
1324 let mut line = String::new();
1325 loop {
1326 match self.reader.read_line(&mut line) {
1327 Ok(0) => return None, Ok(_) => {
1329 self.current_line += 1;
1330 let trimmed = line.trim();
1331
1332 if trimmed.starts_with("f ") {
1334 self.buffer.push(line.clone());
1335 line.clear();
1336 break;
1337 }
1338 line.clear();
1339 }
1340 Err(e) => return Some(Err(Error::Io(e))),
1341 }
1342 }
1343 }
1344 }
1345
1346 if !self.buffer.is_empty() {
1348 let face_line = self.buffer.remove(0);
1349 let parts: Vec<&str> = face_line.trim().split_whitespace().collect();
1350 if parts.len() < 4 {
1351 return Some(Err(Error::InvalidData(format!(
1352 "Invalid face at line {}: expected at least 3 vertices",
1353 self.current_line
1354 ))));
1355 }
1356
1357 let mut face_vertices = Vec::new();
1359 for vertex_str in &parts[1..] {
1360 let face_vertex = match FaceVertex::parse(vertex_str) {
1361 Ok(fv) => fv.vertex,
1362 Err(e) => {
1363 return Some(Err(Error::InvalidData(format!(
1364 "Invalid face vertex '{}' at line {}: {}",
1365 vertex_str, self.current_line, e
1366 ))))
1367 }
1368 };
1369 face_vertices.push(face_vertex);
1370 }
1371
1372 let triangulated_faces = RobustObjReader::triangulate_face(
1374 &face_vertices
1375 .iter()
1376 .map(|&v| FaceVertex {
1377 vertex: v,
1378 texture: None,
1379 normal: None,
1380 })
1381 .collect::<Vec<_>>(),
1382 );
1383
1384 if triangulated_faces.is_empty() {
1385 return Some(Err(Error::InvalidData("Degenerate face".to_string())));
1386 }
1387
1388 let first_triangle = &triangulated_faces[0];
1390 let face_indices = [
1391 first_triangle[0].vertex,
1392 first_triangle[1].vertex,
1393 first_triangle[2].vertex,
1394 ];
1395
1396 for &idx in &face_indices {
1398 if idx >= self.vertices.len() {
1399 return Some(Err(Error::InvalidData(format!(
1400 "Face index {} out of range (max: {})",
1401 idx,
1402 self.vertices.len() - 1
1403 ))));
1404 }
1405 }
1406
1407 for triangle in triangulated_faces.into_iter().skip(1) {
1409 let face_str = format!(
1410 "f {} {} {}",
1411 triangle[0].vertex + 1,
1412 triangle[1].vertex + 1,
1413 triangle[2].vertex + 1
1414 );
1415 self.buffer.push(face_str);
1416 }
1417
1418 self.faces_read += 1;
1419 Some(Ok(face_indices))
1420 } else {
1421 None
1422 }
1423 }
1424}