Skip to main content

threecrate_io/
obj.rs

1//! OBJ format support with MTL material linking
2//!
3//! This module provides comprehensive OBJ (Wavefront OBJ) reading and writing
4//! capabilities including:
5//! - Vertices (v), texture coordinates (vt), normals (vn), faces (f)
6//! - Groups/materials (usemtl, mtllib) with MTL file parsing
7//! - Polygon triangulation for faces with more than 3 vertices
8//! - Optional vertex-color support via conventions
9//! - Robust error handling and streaming read capabilities
10
11use crate::{MeshReader, MeshWriter};
12use threecrate_core::{TriangleMesh, Result, Point3f, Vector3f, Error};
13use std::path::{Path, PathBuf};
14use std::fs::File;
15use std::io::{BufRead, BufReader, BufWriter, Write};
16use std::collections::HashMap;
17
18/// Material properties from MTL files
19#[derive(Debug, Clone)]
20pub struct Material {
21    /// Material name
22    pub name: String,
23    /// Ambient color (Ka)
24    pub ambient: Option<[f32; 3]>,
25    /// Diffuse color (Kd)
26    pub diffuse: Option<[f32; 3]>,
27    /// Specular color (Ks)
28    pub specular: Option<[f32; 3]>,
29    /// Specular exponent (Ns)
30    pub shininess: Option<f32>,
31    /// Transparency (d or Tr)
32    pub transparency: Option<f32>,
33    /// Illumination model (illum)
34    pub illumination: Option<u32>,
35    /// Diffuse texture map (map_Kd)
36    pub diffuse_map: Option<String>,
37    /// Normal map (map_Bump or bump)
38    pub normal_map: Option<String>,
39    /// Specular map (map_Ks)
40    pub specular_map: Option<String>,
41}
42
43/// Face vertex with indices for position, texture coordinate, and normal
44#[derive(Debug, Clone, Copy)]
45pub struct FaceVertex {
46    /// Vertex position index (required)
47    pub vertex: usize,
48    /// Texture coordinate index (optional)
49    pub texture: Option<usize>,
50    /// Normal index (optional)
51    pub normal: Option<usize>,
52}
53
54/// Face definition
55#[derive(Debug, Clone)]
56pub struct Face {
57    /// Vertices of the face
58    pub vertices: Vec<FaceVertex>,
59    /// Material used for this face (if any)
60    pub material: Option<String>,
61}
62
63/// Group definition
64#[derive(Debug, Clone)]
65pub struct Group {
66    /// Group name
67    pub name: String,
68    /// Faces in this group
69    pub faces: Vec<Face>,
70}
71
72/// Complete OBJ file data
73#[derive(Debug)]
74pub struct ObjData {
75    /// Vertex positions
76    pub vertices: Vec<Point3f>,
77    /// Texture coordinates
78    pub texture_coords: Vec<[f32; 2]>,
79    /// Vertex normals
80    pub normals: Vec<Vector3f>,
81    /// Groups
82    pub groups: Vec<Group>,
83    /// Materials referenced by this OBJ
84    pub materials: HashMap<String, Material>,
85    /// MTL file paths
86    pub mtl_files: Vec<String>,
87}
88
89/// Enhanced OBJ reader with comprehensive format support
90pub struct RobustObjReader;
91
92/// OBJ reader (legacy interface)
93pub struct ObjReader;
94
95/// OBJ writer options
96#[derive(Debug, Clone)]
97pub struct ObjWriteOptions {
98    /// Include vertex normals in output
99    pub write_normals: bool,
100    /// Include texture coordinates in output
101    pub write_texcoords: bool,
102    /// Generate and write MTL material file
103    pub write_materials: bool,
104    /// Comments to include in the header
105    pub comments: Vec<String>,
106    /// Object name
107    pub object_name: Option<String>,
108    /// Group name
109    pub group_name: Option<String>,
110    /// Material name for the mesh
111    pub material_name: Option<String>,
112    /// MTL file name (if different from OBJ name)
113    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    /// Create new options with defaults
133    pub fn new() -> Self {
134        Self::default()
135    }
136    
137    /// Enable/disable normal writing
138    pub fn with_normals(mut self, write_normals: bool) -> Self {
139        self.write_normals = write_normals;
140        self
141    }
142    
143    /// Enable/disable texture coordinate writing
144    pub fn with_texcoords(mut self, write_texcoords: bool) -> Self {
145        self.write_texcoords = write_texcoords;
146        self
147    }
148    
149    /// Enable/disable material file generation
150    pub fn with_materials(mut self, write_materials: bool) -> Self {
151        self.write_materials = write_materials;
152        self
153    }
154    
155    /// Add a comment to the header
156    pub fn with_comment<S: Into<String>>(mut self, comment: S) -> Self {
157        self.comments.push(comment.into());
158        self
159    }
160    
161    /// Set object name
162    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    /// Set group name
168    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    /// Set material name
174    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    /// Set MTL filename
180    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
186/// Enhanced OBJ writer with comprehensive format support
187pub struct RobustObjWriter;
188
189/// OBJ writer (legacy interface)
190pub struct ObjWriter;
191
192impl Material {
193    /// Create a new material with default properties
194    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    /// Parse face vertex from OBJ format string (e.g., "1", "1/2", "1/2/3", "1//3")
218    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].parse::<usize>()
226            .map_err(|_| Error::InvalidData(format!("Invalid vertex index: {}", parts[0])))?;
227        
228        // OBJ uses 1-based indexing, convert to 0-based
229        let vertex = vertex.checked_sub(1)
230            .ok_or_else(|| Error::InvalidData("Vertex index cannot be 0".to_string()))?;
231        
232        let texture = if parts.len() > 1 && !parts[1].is_empty() {
233            let tex_idx = parts[1].parse::<usize>()
234                .map_err(|_| Error::InvalidData(format!("Invalid texture index: {}", parts[1])))?;
235            Some(tex_idx.checked_sub(1)
236                .ok_or_else(|| Error::InvalidData("Texture index cannot be 0".to_string()))?)
237        } else {
238            None
239        };
240        
241        let normal = if parts.len() > 2 && !parts[2].is_empty() {
242            let norm_idx = parts[2].parse::<usize>()
243                .map_err(|_| Error::InvalidData(format!("Invalid normal index: {}", parts[2])))?;
244            Some(norm_idx.checked_sub(1)
245                .ok_or_else(|| Error::InvalidData("Normal index cannot be 0".to_string()))?)
246        } else {
247            None
248        };
249        
250        Ok(FaceVertex { vertex, texture, normal })
251    }
252}
253
254impl crate::registry::MeshReader for ObjReader {
255    fn read_mesh(&self, path: &Path) -> Result<TriangleMesh> {
256        let obj_data = RobustObjReader::read_obj_file(path)?;
257        RobustObjReader::obj_data_to_mesh(&obj_data)
258    }
259    
260    fn can_read(&self, path: &Path) -> bool {
261        // Check if file starts with "#" or "v " (vertex definition)
262        if let Ok(file) = File::open(path) {
263            let mut reader = BufReader::new(file);
264            let mut line = String::new();
265            if let Ok(_) = reader.read_line(&mut line) {
266                let trimmed = line.trim();
267                return trimmed.starts_with("#") || trimmed.starts_with("v ");
268            }
269        }
270        false
271    }
272    
273    fn format_name(&self) -> &'static str {
274        "obj"
275    }
276}
277
278impl crate::registry::MeshWriter for ObjWriter {
279    fn write_mesh(&self, mesh: &TriangleMesh, path: &Path) -> Result<()> {
280        let file = File::create(path)?;
281        let mut writer = BufWriter::new(file);
282        
283        // Write header
284        writeln!(writer, "# OBJ file generated by ThreeCrate")?;
285        writeln!(writer, "# Vertices: {}", mesh.vertices.len())?;
286        writeln!(writer, "# Faces: {}", mesh.faces.len())?;
287        writeln!(writer)?;
288        
289        // Write vertices
290        for vertex in &mesh.vertices {
291            writeln!(writer, "v {} {} {}", vertex.x, vertex.y, vertex.z)?;
292        }
293        writeln!(writer)?;
294        
295        // Write normals if available
296        if let Some(normals) = &mesh.normals {
297            for normal in normals {
298                writeln!(writer, "vn {} {} {}", normal.x, normal.y, normal.z)?;
299            }
300            writeln!(writer)?;
301        }
302        
303        // Write faces
304        if mesh.normals.is_some() {
305            // Write faces with normals
306            for face in &mesh.faces {
307                writeln!(
308                    writer,
309                    "f {}//{} {}//{} {}//{}",
310                    face[0] + 1, face[0] + 1,
311                    face[1] + 1, face[1] + 1,
312                    face[2] + 1, face[2] + 1
313                )?;
314            }
315        } else {
316            // Write faces without normals
317            for face in &mesh.faces {
318                writeln!(
319                    writer,
320                    "f {} {} {}",
321                    face[0] + 1,
322                    face[1] + 1,
323                    face[2] + 1
324                )?;
325            }
326        }
327        
328        Ok(())
329    }
330    
331    fn format_name(&self) -> &'static str {
332        "obj"
333    }
334}
335
336// Keep the legacy trait implementations for backward compatibility
337impl MeshReader for ObjReader {
338    fn read_mesh<P: AsRef<Path>>(path: P) -> Result<TriangleMesh> {
339        let reader = ObjReader;
340        crate::registry::MeshReader::read_mesh(&reader, path.as_ref())
341    }
342}
343
344impl MeshWriter for ObjWriter {
345    fn write_mesh<P: AsRef<Path>>(mesh: &TriangleMesh, path: P) -> Result<()> {
346        let writer = ObjWriter;
347        crate::registry::MeshWriter::write_mesh(&writer, mesh, path.as_ref())
348    }
349}
350
351/// Read an OBJ file and return vertex positions only (useful for point clouds)
352pub fn read_obj_vertices<P: AsRef<Path>>(path: P) -> Result<Vec<Point3f>> {
353    let obj_data = RobustObjReader::read_obj_file(path)?;
354    Ok(obj_data.vertices)
355}
356
357/// Write vertices as an OBJ file (useful for point clouds)
358pub fn write_obj_vertices<P: AsRef<Path>>(vertices: &[Point3f], path: P) -> Result<()> {
359    let file = File::create(path)?;
360    let mut writer = BufWriter::new(file);
361    
362    writeln!(writer, "# OBJ vertices file generated by ThreeCrate")?;
363    writeln!(writer, "# Vertices: {}", vertices.len())?;
364    writeln!(writer)?;
365    
366    for vertex in vertices {
367        writeln!(writer, "v {} {} {}", vertex.x, vertex.y, vertex.z)?;
368    }
369    
370    Ok(())
371}
372
373impl RobustObjReader {
374    /// Read a complete OBJ file with materials
375    pub fn read_obj_file<P: AsRef<Path>>(path: P) -> Result<ObjData> {
376        let path = path.as_ref();
377        let file = File::open(path)
378            .map_err(|e| Error::Io(e))?;
379        let reader = BufReader::new(file);
380        
381        let mut obj_data = ObjData {
382            vertices: Vec::new(),
383            texture_coords: Vec::new(),
384            normals: Vec::new(),
385            groups: Vec::new(),
386            materials: HashMap::new(),
387            mtl_files: Vec::new(),
388        };
389        
390        let mut current_group = Group {
391            name: "default".to_string(),
392            faces: Vec::new(),
393        };
394        let mut current_material: Option<String> = None;
395        
396        for (line_num, line) in reader.lines().enumerate() {
397            let line = line.map_err(|e| Error::Io(e))?;
398            let line = line.trim();
399            
400            // Skip empty lines and comments
401            if line.is_empty() || line.starts_with('#') {
402                continue;
403            }
404            
405            let parts: Vec<&str> = line.split_whitespace().collect();
406            if parts.is_empty() {
407                continue;
408            }
409            
410            match parts[0] {
411                "v" => {
412                    // Vertex position
413                    if parts.len() < 4 {
414                        return Err(Error::InvalidData(
415                            format!("Invalid vertex at line {}: expected 3 coordinates", line_num + 1)
416                        ));
417                    }
418                    
419                    let x = parts[1].parse::<f32>()
420                        .map_err(|_| Error::InvalidData(format!("Invalid x coordinate at line {}", line_num + 1)))?;
421                    let y = parts[2].parse::<f32>()
422                        .map_err(|_| Error::InvalidData(format!("Invalid y coordinate at line {}", line_num + 1)))?;
423                    let z = parts[3].parse::<f32>()
424                        .map_err(|_| Error::InvalidData(format!("Invalid z coordinate at line {}", line_num + 1)))?;
425                    
426                    obj_data.vertices.push(Point3f::new(x, y, z));
427                }
428                "vt" => {
429                    // Texture coordinate
430                    if parts.len() < 3 {
431                        return Err(Error::InvalidData(
432                            format!("Invalid texture coordinate at line {}: expected 2 coordinates", line_num + 1)
433                        ));
434                    }
435                    
436                    let u = parts[1].parse::<f32>()
437                        .map_err(|_| Error::InvalidData(format!("Invalid u coordinate at line {}", line_num + 1)))?;
438                    let v = parts[2].parse::<f32>()
439                        .map_err(|_| Error::InvalidData(format!("Invalid v coordinate at line {}", line_num + 1)))?;
440                    
441                    obj_data.texture_coords.push([u, v]);
442                }
443                "vn" => {
444                    // Vertex normal
445                    if parts.len() < 4 {
446                        return Err(Error::InvalidData(
447                            format!("Invalid normal at line {}: expected 3 components", line_num + 1)
448                        ));
449                    }
450                    
451                    let x = parts[1].parse::<f32>()
452                        .map_err(|_| Error::InvalidData(format!("Invalid normal x at line {}", line_num + 1)))?;
453                    let y = parts[2].parse::<f32>()
454                        .map_err(|_| Error::InvalidData(format!("Invalid normal y at line {}", line_num + 1)))?;
455                    let z = parts[3].parse::<f32>()
456                        .map_err(|_| Error::InvalidData(format!("Invalid normal z at line {}", line_num + 1)))?;
457                    
458                    obj_data.normals.push(Vector3f::new(x, y, z));
459                }
460                "f" => {
461                    // Face
462                    if parts.len() < 4 {
463                        return Err(Error::InvalidData(
464                            format!("Invalid face at line {}: expected at least 3 vertices", line_num + 1)
465                        ));
466                    }
467                    
468                    let mut face_vertices = Vec::new();
469                    for vertex_str in &parts[1..] {
470                        let face_vertex = FaceVertex::parse(vertex_str)
471                            .map_err(|e| Error::InvalidData(format!("Invalid face vertex '{}' at line {}: {}", vertex_str, line_num + 1, e)))?;
472                        face_vertices.push(face_vertex);
473                    }
474                    
475                    // Triangulate if necessary
476                    let triangulated_faces = Self::triangulate_face(&face_vertices);
477                    for triangle in triangulated_faces {
478                        current_group.faces.push(Face {
479                            vertices: triangle,
480                            material: current_material.clone(),
481                        });
482                    }
483                }
484                "g" => {
485                    // Group
486                    if !current_group.faces.is_empty() || current_group.name != "default" {
487                        obj_data.groups.push(current_group);
488                    }
489                    
490                    let group_name = if parts.len() > 1 {
491                        parts[1..].join(" ")
492                    } else {
493                        format!("group_{}", obj_data.groups.len())
494                    };
495                    
496                    current_group = Group {
497                        name: group_name,
498                        faces: Vec::new(),
499                    };
500                }
501                "usemtl" => {
502                    // Use material
503                    if parts.len() > 1 {
504                        current_material = Some(parts[1].to_string());
505                    }
506                }
507                "mtllib" => {
508                    // Material library
509                    if parts.len() > 1 {
510                        let mtl_file = parts[1].to_string();
511                        obj_data.mtl_files.push(mtl_file.clone());
512                        
513                        // Try to load the MTL file
514                        let mtl_path = if let Some(parent) = path.parent() {
515                            parent.join(&mtl_file)
516                        } else {
517                            PathBuf::from(&mtl_file)
518                        };
519                        
520                        if let Ok(materials) = Self::read_mtl_file(&mtl_path) {
521                            obj_data.materials.extend(materials);
522                        }
523                    }
524                }
525                _ => {
526                    // Ignore unknown commands
527                }
528            }
529        }
530        
531        // Add the last group if it has faces
532        if !current_group.faces.is_empty() {
533            obj_data.groups.push(current_group);
534        }
535        
536        // If no groups were created, create a default group with all faces
537        if obj_data.groups.is_empty() {
538            obj_data.groups.push(Group {
539                name: "default".to_string(),
540                faces: Vec::new(),
541            });
542        }
543        
544        Ok(obj_data)
545    }
546
547    /// Read an MTL file and return materials
548    pub fn read_mtl_file<P: AsRef<Path>>(path: P) -> Result<HashMap<String, Material>> {
549        let file = File::open(path)
550            .map_err(|e| Error::Io(e))?;
551        let reader = BufReader::new(file);
552        
553        let mut materials = HashMap::new();
554        let mut current_material: Option<Material> = None;
555        
556        for (_line_num, line) in reader.lines().enumerate() {
557            let line = line.map_err(|e| Error::Io(e))?;
558            let line = line.trim();
559            
560            // Skip empty lines and comments
561            if line.is_empty() || line.starts_with('#') {
562                continue;
563            }
564            
565            let parts: Vec<&str> = line.split_whitespace().collect();
566            if parts.is_empty() {
567                continue;
568            }
569            
570            match parts[0] {
571                "newmtl" => {
572                    // Save previous material
573                    if let Some(material) = current_material.take() {
574                        materials.insert(material.name.clone(), material);
575                    }
576                    
577                    // Start new material
578                    if parts.len() > 1 {
579                        current_material = Some(Material::new(parts[1].to_string()));
580                    }
581                }
582                "Ka" => {
583                    // Ambient color
584                    if let Some(ref mut material) = current_material {
585                        if parts.len() >= 4 {
586                            if let (Ok(r), Ok(g), Ok(b)) = (
587                                parts[1].parse::<f32>(),
588                                parts[2].parse::<f32>(),
589                                parts[3].parse::<f32>()
590                            ) {
591                                material.ambient = Some([r, g, b]);
592                            }
593                        }
594                    }
595                }
596                "Kd" => {
597                    // Diffuse color
598                    if let Some(ref mut material) = current_material {
599                        if parts.len() >= 4 {
600                            if let (Ok(r), Ok(g), Ok(b)) = (
601                                parts[1].parse::<f32>(),
602                                parts[2].parse::<f32>(),
603                                parts[3].parse::<f32>()
604                            ) {
605                                material.diffuse = Some([r, g, b]);
606                            }
607                        }
608                    }
609                }
610                "Ks" => {
611                    // Specular color
612                    if let Some(ref mut material) = current_material {
613                        if parts.len() >= 4 {
614                            if let (Ok(r), Ok(g), Ok(b)) = (
615                                parts[1].parse::<f32>(),
616                                parts[2].parse::<f32>(),
617                                parts[3].parse::<f32>()
618                            ) {
619                                material.specular = Some([r, g, b]);
620                            }
621                        }
622                    }
623                }
624                "Ns" => {
625                    // Specular exponent
626                    if let Some(ref mut material) = current_material {
627                        if parts.len() > 1 {
628                            if let Ok(ns) = parts[1].parse::<f32>() {
629                                material.shininess = Some(ns);
630                            }
631                        }
632                    }
633                }
634                "d" => {
635                    // Transparency (dissolve)
636                    if let Some(ref mut material) = current_material {
637                        if parts.len() > 1 {
638                            if let Ok(d) = parts[1].parse::<f32>() {
639                                material.transparency = Some(d);
640                            }
641                        }
642                    }
643                }
644                "Tr" => {
645                    // Transparency (alternative)
646                    if let Some(ref mut material) = current_material {
647                        if parts.len() > 1 {
648                            if let Ok(tr) = parts[1].parse::<f32>() {
649                                material.transparency = Some(1.0 - tr); // Tr is inverse of d
650                            }
651                        }
652                    }
653                }
654                "illum" => {
655                    // Illumination model
656                    if let Some(ref mut material) = current_material {
657                        if parts.len() > 1 {
658                            if let Ok(illum) = parts[1].parse::<u32>() {
659                                material.illumination = Some(illum);
660                            }
661                        }
662                    }
663                }
664                "map_Kd" => {
665                    // Diffuse texture map
666                    if let Some(ref mut material) = current_material {
667                        if parts.len() > 1 {
668                            material.diffuse_map = Some(parts[1..].join(" "));
669                        }
670                    }
671                }
672                "map_Bump" | "bump" => {
673                    // Normal/bump map
674                    if let Some(ref mut material) = current_material {
675                        if parts.len() > 1 {
676                            material.normal_map = Some(parts[1..].join(" "));
677                        }
678                    }
679                }
680                "map_Ks" => {
681                    // Specular map
682                    if let Some(ref mut material) = current_material {
683                        if parts.len() > 1 {
684                            material.specular_map = Some(parts[1..].join(" "));
685                        }
686                    }
687                }
688                _ => {
689                    // Ignore unknown material properties
690                }
691            }
692        }
693        
694        // Save the last material
695        if let Some(material) = current_material {
696            materials.insert(material.name.clone(), material);
697        }
698        
699        Ok(materials)
700    }
701    
702    /// Triangulate a face with arbitrary number of vertices
703    fn triangulate_face(vertices: &[FaceVertex]) -> Vec<Vec<FaceVertex>> {
704        match vertices.len() {
705            3 => {
706                // Already a triangle
707                vec![vertices.to_vec()]
708            }
709            4 => {
710                // Quad - split into two triangles
711                vec![
712                    vec![vertices[0], vertices[1], vertices[2]],
713                    vec![vertices[0], vertices[2], vertices[3]],
714                ]
715            }
716            n if n > 4 => {
717                // N-gon - fan triangulation from first vertex
718                let mut triangles = Vec::new();
719                for i in 1..(n - 1) {
720                    triangles.push(vec![vertices[0], vertices[i], vertices[i + 1]]);
721                }
722                triangles
723            }
724            _ => {
725                // Degenerate face
726                vec![]
727            }
728        }
729    }
730    
731    /// Convert ObjData to TriangleMesh
732    pub fn obj_data_to_mesh(obj_data: &ObjData) -> Result<TriangleMesh> {
733        let mut mesh_faces = Vec::new();
734        let mut mesh_normals = Vec::new();
735        let mut has_normals = false;
736        
737        // Collect all faces from all groups
738        for group in &obj_data.groups {
739            for face in &group.faces {
740                if face.vertices.len() != 3 {
741                    continue; // Skip non-triangular faces (shouldn't happen after triangulation)
742                }
743                
744                // Extract vertex indices
745                let face_indices = [
746                    face.vertices[0].vertex,
747                    face.vertices[1].vertex,
748                    face.vertices[2].vertex,
749                ];
750                
751                // Validate vertex indices
752                for &idx in &face_indices {
753                    if idx >= obj_data.vertices.len() {
754                        return Err(Error::InvalidData(
755                            format!("Vertex index {} out of range (max: {})", idx, obj_data.vertices.len() - 1)
756                        ));
757                    }
758                }
759                
760                mesh_faces.push(face_indices);
761                
762                // Handle normals if available
763                if !obj_data.normals.is_empty() {
764                    for vertex in &face.vertices {
765                        if let Some(normal_idx) = vertex.normal {
766                            if normal_idx >= obj_data.normals.len() {
767                                return Err(Error::InvalidData(
768                                    format!("Normal index {} out of range (max: {})", normal_idx, obj_data.normals.len() - 1)
769                                ));
770                            }
771                            mesh_normals.push(obj_data.normals[normal_idx]);
772                            has_normals = true;
773                        } else if has_normals {
774                            // If some vertices have normals, all should have normals
775                            // Use a default normal for consistency
776                            mesh_normals.push(Vector3f::new(0.0, 0.0, 1.0));
777                        }
778                    }
779                }
780            }
781        }
782        
783        let mut mesh = TriangleMesh::from_vertices_and_faces(obj_data.vertices.clone(), mesh_faces);
784        
785        // Set normals if available
786        if has_normals && mesh_normals.len() == mesh.vertices.len() {
787            mesh.set_normals(mesh_normals);
788        }
789        
790        Ok(mesh)
791    }
792}
793
794impl RobustObjWriter {
795    /// Write a complete OBJ file with optional MTL material file
796    pub fn write_obj_file<P: AsRef<Path>>(
797        obj_data: &ObjData,
798        path: P,
799        options: &ObjWriteOptions,
800    ) -> Result<()> {
801        let path = path.as_ref();
802        let file = File::create(path)?;
803        let mut writer = BufWriter::new(file);
804        
805        // Write header comments
806        for comment in &options.comments {
807            writeln!(writer, "# {}", comment)?;
808        }
809        writeln!(writer, "# Vertices: {}", obj_data.vertices.len())?;
810        writeln!(writer, "# Texture coordinates: {}", obj_data.texture_coords.len())?;
811        writeln!(writer, "# Normals: {}", obj_data.normals.len())?;
812        writeln!(writer, "# Groups: {}", obj_data.groups.len())?;
813        writeln!(writer)?;
814        
815        // Write MTL library reference if materials should be written
816        if options.write_materials && !obj_data.materials.is_empty() {
817            let default_mtl_filename = Self::get_mtl_filename(path);
818            let mtl_filename = options.mtl_filename.as_ref()
819                .unwrap_or(&default_mtl_filename);
820            writeln!(writer, "mtllib {}", mtl_filename)?;
821            writeln!(writer)?;
822        }
823        
824        // Write object name if specified
825        if let Some(ref object_name) = options.object_name {
826            writeln!(writer, "o {}", object_name)?;
827            writeln!(writer)?;
828        }
829        
830        // Write vertices
831        for vertex in &obj_data.vertices {
832            writeln!(writer, "v {} {} {}", vertex.x, vertex.y, vertex.z)?;
833        }
834        if !obj_data.vertices.is_empty() {
835            writeln!(writer)?;
836        }
837        
838        // Write texture coordinates if available and requested
839        if options.write_texcoords && !obj_data.texture_coords.is_empty() {
840            for tex_coord in &obj_data.texture_coords {
841                writeln!(writer, "vt {} {}", tex_coord[0], tex_coord[1])?;
842            }
843            writeln!(writer)?;
844        }
845        
846        // Write vertex normals if available and requested
847        if options.write_normals && !obj_data.normals.is_empty() {
848            for normal in &obj_data.normals {
849                writeln!(writer, "vn {} {} {}", normal.x, normal.y, normal.z)?;
850            }
851            writeln!(writer)?;
852        }
853        
854        // Write groups and faces
855        for group in &obj_data.groups {
856            // Write group name
857            let group_name = if group.name == "default" && options.group_name.is_some() {
858                options.group_name.as_ref().unwrap()
859            } else if group.name != "default" {
860                &group.name
861            } else {
862                "default"
863            };
864            
865            if group_name != "default" || options.group_name.is_some() {
866                writeln!(writer, "g {}", group_name)?;
867            }
868            
869            // Track current material
870            let mut current_material: Option<&String> = None;
871            
872            for face in &group.faces {
873                // Write material change if needed
874                if let Some(ref material) = face.material {
875                    if current_material != Some(material) {
876                        writeln!(writer, "usemtl {}", material)?;
877                        current_material = Some(material);
878                    }
879                } else if let Some(ref material_name) = options.material_name {
880                    if current_material.is_none() {
881                        writeln!(writer, "usemtl {}", material_name)?;
882                        current_material = Some(material_name);
883                    }
884                }
885                
886                // Write face
887                write!(writer, "f")?;
888                for vertex in &face.vertices {
889                    write!(writer, " {}", vertex.vertex + 1)?; // Convert to 1-based indexing
890                    
891                    // Add texture coordinate if available and requested
892                    if options.write_texcoords && !obj_data.texture_coords.is_empty() {
893                        if let Some(tex_idx) = vertex.texture {
894                            write!(writer, "/{}", tex_idx + 1)?;
895                        } else {
896                            write!(writer, "/")?;
897                        }
898                    }
899                    
900                    // Add normal if available and requested
901                    if options.write_normals && !obj_data.normals.is_empty() {
902                        if !options.write_texcoords || obj_data.texture_coords.is_empty() {
903                            write!(writer, "//")?;
904                        } else {
905                            write!(writer, "/")?;
906                        }
907                        
908                        if let Some(norm_idx) = vertex.normal {
909                            write!(writer, "{}", norm_idx + 1)?;
910                        }
911                    }
912                }
913                writeln!(writer)?;
914            }
915            
916            if !group.faces.is_empty() {
917                writeln!(writer)?;
918            }
919        }
920        
921        // Write MTL file if requested
922        if options.write_materials && !obj_data.materials.is_empty() {
923            let default_mtl_filename = Self::get_mtl_filename(path);
924            let mtl_filename = options.mtl_filename.as_ref()
925                .unwrap_or(&default_mtl_filename);
926            let mtl_path = path.parent().unwrap_or(Path::new(".")).join(mtl_filename);
927            Self::write_mtl_file(&obj_data.materials, &mtl_path)?;
928        }
929        
930        Ok(())
931    }
932    
933    /// Write a TriangleMesh as OBJ with options
934    pub fn write_mesh<P: AsRef<Path>>(
935        mesh: &TriangleMesh,
936        path: P,
937        options: &ObjWriteOptions,
938    ) -> Result<()> {
939        // Convert mesh to ObjData
940        let obj_data = Self::mesh_to_obj_data(mesh, options)?;
941        Self::write_obj_file(&obj_data, path, options)
942    }
943    
944    /// Write MTL material file
945    pub fn write_mtl_file<P: AsRef<Path>>(
946        materials: &HashMap<String, Material>,
947        path: P,
948    ) -> Result<()> {
949        let file = File::create(path)?;
950        let mut writer = BufWriter::new(file);
951        
952        writeln!(writer, "# MTL file generated by ThreeCrate")?;
953        writeln!(writer, "# Materials: {}", materials.len())?;
954        writeln!(writer)?;
955        
956        for (name, material) in materials {
957            writeln!(writer, "newmtl {}", name)?;
958            
959            // Write ambient color
960            if let Some(ambient) = material.ambient {
961                writeln!(writer, "Ka {} {} {}", ambient[0], ambient[1], ambient[2])?;
962            }
963            
964            // Write diffuse color
965            if let Some(diffuse) = material.diffuse {
966                writeln!(writer, "Kd {} {} {}", diffuse[0], diffuse[1], diffuse[2])?;
967            }
968            
969            // Write specular color
970            if let Some(specular) = material.specular {
971                writeln!(writer, "Ks {} {} {}", specular[0], specular[1], specular[2])?;
972            }
973            
974            // Write shininess
975            if let Some(shininess) = material.shininess {
976                writeln!(writer, "Ns {}", shininess)?;
977            }
978            
979            // Write transparency
980            if let Some(transparency) = material.transparency {
981                writeln!(writer, "d {}", transparency)?;
982            }
983            
984            // Write illumination model
985            if let Some(illumination) = material.illumination {
986                writeln!(writer, "illum {}", illumination)?;
987            }
988            
989            // Write texture maps
990            if let Some(ref diffuse_map) = material.diffuse_map {
991                writeln!(writer, "map_Kd {}", diffuse_map)?;
992            }
993            
994            if let Some(ref normal_map) = material.normal_map {
995                writeln!(writer, "map_Bump {}", normal_map)?;
996            }
997            
998            if let Some(ref specular_map) = material.specular_map {
999                writeln!(writer, "map_Ks {}", specular_map)?;
1000            }
1001            
1002            writeln!(writer)?;
1003        }
1004        
1005        Ok(())
1006    }
1007    
1008    /// Convert TriangleMesh to ObjData
1009    fn mesh_to_obj_data(mesh: &TriangleMesh, options: &ObjWriteOptions) -> Result<ObjData> {
1010        let mut obj_data = ObjData {
1011            vertices: mesh.vertices.clone(),
1012            texture_coords: Vec::new(),
1013            normals: Vec::new(),
1014            groups: Vec::new(),
1015            materials: HashMap::new(),
1016            mtl_files: Vec::new(),
1017        };
1018        
1019        // Add normals if available and requested
1020        if options.write_normals {
1021            if let Some(ref normals) = mesh.normals {
1022                obj_data.normals = normals.clone();
1023            }
1024        }
1025        
1026        // Create faces with proper indexing
1027        let mut faces = Vec::new();
1028        for face_indices in &mesh.faces {
1029            let mut face_vertices = Vec::new();
1030            for &vertex_idx in face_indices {
1031                let face_vertex = FaceVertex {
1032                    vertex: vertex_idx,
1033                    texture: None, // No texture coordinates in TriangleMesh
1034                    normal: if options.write_normals && mesh.normals.is_some() {
1035                        Some(vertex_idx) // Assume per-vertex normals
1036                    } else {
1037                        None
1038                    },
1039                };
1040                face_vertices.push(face_vertex);
1041            }
1042            
1043            faces.push(Face {
1044                vertices: face_vertices,
1045                material: options.material_name.clone(),
1046            });
1047        }
1048        
1049        // Create a single group
1050        let group_name = options.group_name.clone().unwrap_or_else(|| "default".to_string());
1051        obj_data.groups.push(Group {
1052            name: group_name,
1053            faces,
1054        });
1055        
1056        // Add material if specified
1057        if let Some(ref material_name) = options.material_name {
1058            let material = Material::new(material_name.clone());
1059            obj_data.materials.insert(material_name.clone(), material);
1060        }
1061        
1062        Ok(obj_data)
1063    }
1064    
1065    /// Generate MTL filename from OBJ path
1066    fn get_mtl_filename(obj_path: &Path) -> String {
1067        obj_path
1068            .file_stem()
1069            .and_then(|s| s.to_str())
1070            .map(|s| format!("{}.mtl", s))
1071            .unwrap_or_else(|| "materials.mtl".to_string())
1072    }
1073}
1074
1075/// Streaming OBJ reader for point clouds (vertices only)
1076pub struct ObjStreamingReader {
1077    reader: BufReader<File>,
1078    current_line: usize,
1079    total_vertices: usize,
1080    vertices_read: usize,
1081    chunk_size: usize,
1082    buffer: Vec<String>,
1083}
1084
1085impl ObjStreamingReader {
1086    /// Create a new streaming OBJ reader for vertices
1087    pub fn new<P: AsRef<Path>>(path: P, chunk_size: usize) -> Result<Self> {
1088        let path = path.as_ref();
1089        let file = File::open(path)?;
1090        let mut reader = BufReader::new(file);
1091        
1092        // First pass: count vertices
1093        let mut total_vertices = 0;
1094        let mut line = String::new();
1095        while reader.read_line(&mut line)? > 0 {
1096            let trimmed = line.trim();
1097            if trimmed.starts_with("v ") && !trimmed.starts_with("vt ") && !trimmed.starts_with("vn ") {
1098                total_vertices += 1;
1099            }
1100            line.clear();
1101        }
1102        
1103        // Reset file position
1104        let file = File::open(path)?;
1105        let reader = BufReader::new(file);
1106        
1107        Ok(Self {
1108            reader,
1109            current_line: 0,
1110            total_vertices,
1111            vertices_read: 0,
1112            chunk_size,
1113            buffer: Vec::with_capacity(chunk_size),
1114        })
1115    }
1116}
1117
1118impl Iterator for ObjStreamingReader {
1119    type Item = Result<Point3f>;
1120    
1121    fn next(&mut self) -> Option<Self::Item> {
1122        if self.vertices_read >= self.total_vertices {
1123            return None;
1124        }
1125        
1126        // Read a chunk if buffer is empty
1127        if self.buffer.is_empty() {
1128            let remaining = self.total_vertices - self.vertices_read;
1129            let to_read = std::cmp::min(remaining, self.chunk_size);
1130            
1131            for _ in 0..to_read {
1132                let mut line = String::new();
1133                loop {
1134                    match self.reader.read_line(&mut line) {
1135                        Ok(0) => return None, // EOF
1136                        Ok(_) => {
1137                            self.current_line += 1;
1138                            let trimmed = line.trim();
1139                            
1140                            // Skip non-vertex lines
1141                            if trimmed.starts_with("v ") && !trimmed.starts_with("vt ") && !trimmed.starts_with("vn ") {
1142                                self.buffer.push(line.clone());
1143                                line.clear();
1144                                break;
1145                            }
1146                            line.clear();
1147                        }
1148                        Err(e) => return Some(Err(Error::Io(e))),
1149                    }
1150                }
1151            }
1152        }
1153        
1154        // Process next vertex from buffer
1155        if !self.buffer.is_empty() {
1156            let vertex_line = self.buffer.remove(0);
1157            let parts: Vec<&str> = vertex_line.trim().split_whitespace().collect();
1158            if parts.len() < 4 {
1159                return Some(Err(Error::InvalidData(
1160                    format!("Invalid vertex at line {}: expected 3 coordinates", self.current_line)
1161                )));
1162            }
1163            
1164            let x = match parts[1].parse::<f32>() {
1165                Ok(v) => v,
1166                Err(_) => return Some(Err(Error::InvalidData(
1167                    format!("Invalid x coordinate at line {}", self.current_line)
1168                ))),
1169            };
1170            let y = match parts[2].parse::<f32>() {
1171                Ok(v) => v,
1172                Err(_) => return Some(Err(Error::InvalidData(
1173                    format!("Invalid y coordinate at line {}", self.current_line)
1174                ))),
1175            };
1176            let z = match parts[3].parse::<f32>() {
1177                Ok(v) => v,
1178                Err(_) => return Some(Err(Error::InvalidData(
1179                    format!("Invalid z coordinate at line {}", self.current_line)
1180                ))),
1181            };
1182            
1183            self.vertices_read += 1;
1184            Some(Ok(Point3f::new(x, y, z)))
1185        } else {
1186            None
1187        }
1188    }
1189}
1190
1191/// Streaming OBJ reader for mesh faces
1192pub struct ObjMeshStreamingReader {
1193    reader: BufReader<File>,
1194    current_line: usize,
1195    total_faces: usize,
1196    faces_read: usize,
1197    chunk_size: usize,
1198    buffer: Vec<String>,
1199    vertices: Vec<Point3f>, // We need to store vertices to convert face indices
1200}
1201
1202impl ObjMeshStreamingReader {
1203    /// Create a new streaming OBJ reader for faces
1204    pub fn new<P: AsRef<Path>>(path: P, chunk_size: usize) -> Result<Self> {
1205        let path = path.as_ref();
1206        let file = File::open(path)?;
1207        let mut reader = BufReader::new(file);
1208        
1209        // First pass: collect vertices and count faces
1210        let mut vertices = Vec::new();
1211        let mut total_faces = 0;
1212        let mut line = String::new();
1213        
1214        while reader.read_line(&mut line)? > 0 {
1215            let trimmed = line.trim();
1216            if trimmed.starts_with("v ") && !trimmed.starts_with("vt ") && !trimmed.starts_with("vn ") {
1217                let parts: Vec<&str> = trimmed.split_whitespace().collect();
1218                if parts.len() >= 4 {
1219                    if let (Ok(x), Ok(y), Ok(z)) = (
1220                        parts[1].parse::<f32>(),
1221                        parts[2].parse::<f32>(),
1222                        parts[3].parse::<f32>()
1223                    ) {
1224                        vertices.push(Point3f::new(x, y, z));
1225                    }
1226                }
1227            } else if trimmed.starts_with("f ") {
1228                total_faces += 1;
1229            }
1230            line.clear();
1231        }
1232        
1233        // Reset file position
1234        let file = File::open(path)?;
1235        let reader = BufReader::new(file);
1236        
1237        Ok(Self {
1238            reader,
1239            current_line: 0,
1240            total_faces,
1241            faces_read: 0,
1242            chunk_size,
1243            buffer: Vec::with_capacity(chunk_size),
1244            vertices,
1245        })
1246    }
1247}
1248
1249impl Iterator for ObjMeshStreamingReader {
1250    type Item = Result<[usize; 3]>;
1251    
1252    fn next(&mut self) -> Option<Self::Item> {
1253        if self.faces_read >= self.total_faces {
1254            return None;
1255        }
1256        
1257        // Read a chunk if buffer is empty
1258        if self.buffer.is_empty() {
1259            let remaining = self.total_faces - self.faces_read;
1260            let to_read = std::cmp::min(remaining, self.chunk_size);
1261            
1262            for _ in 0..to_read {
1263                let mut line = String::new();
1264                loop {
1265                    match self.reader.read_line(&mut line) {
1266                        Ok(0) => return None, // EOF
1267                        Ok(_) => {
1268                            self.current_line += 1;
1269                            let trimmed = line.trim();
1270                            
1271                            // Skip non-face lines
1272                            if trimmed.starts_with("f ") {
1273                                self.buffer.push(line.clone());
1274                                line.clear();
1275                                break;
1276                            }
1277                            line.clear();
1278                        }
1279                        Err(e) => return Some(Err(Error::Io(e))),
1280                    }
1281                }
1282            }
1283        }
1284        
1285        // Process next face from buffer
1286        if !self.buffer.is_empty() {
1287            let face_line = self.buffer.remove(0);
1288            let parts: Vec<&str> = face_line.trim().split_whitespace().collect();
1289            if parts.len() < 4 {
1290                return Some(Err(Error::InvalidData(
1291                    format!("Invalid face at line {}: expected at least 3 vertices", self.current_line)
1292                )));
1293            }
1294            
1295            // Parse face vertices (only need vertex indices, not texture/normal)
1296            let mut face_vertices = Vec::new();
1297            for vertex_str in &parts[1..] {
1298                let face_vertex = match FaceVertex::parse(vertex_str) {
1299                    Ok(fv) => fv.vertex,
1300                    Err(e) => return Some(Err(Error::InvalidData(
1301                        format!("Invalid face vertex '{}' at line {}: {}", vertex_str, self.current_line, e)
1302                    ))),
1303                };
1304                face_vertices.push(face_vertex);
1305            }
1306            
1307            // Triangulate if necessary
1308            let triangulated_faces = RobustObjReader::triangulate_face(
1309                &face_vertices.iter().map(|&v| FaceVertex { vertex: v, texture: None, normal: None }).collect::<Vec<_>>()
1310            );
1311            
1312            if triangulated_faces.is_empty() {
1313                return Some(Err(Error::InvalidData("Degenerate face".to_string())));
1314            }
1315            
1316            // Return first triangle, store others for later
1317            let first_triangle = &triangulated_faces[0];
1318            let face_indices = [first_triangle[0].vertex, first_triangle[1].vertex, first_triangle[2].vertex];
1319            
1320            // Validate indices
1321            for &idx in &face_indices {
1322                if idx >= self.vertices.len() {
1323                    return Some(Err(Error::InvalidData(
1324                        format!("Face index {} out of range (max: {})", idx, self.vertices.len() - 1)
1325                    )));
1326                }
1327            }
1328            
1329            // Store remaining triangles in buffer for next calls
1330            for triangle in triangulated_faces.into_iter().skip(1) {
1331                let face_str = format!("f {} {} {}", 
1332                    triangle[0].vertex + 1, 
1333                    triangle[1].vertex + 1, 
1334                    triangle[2].vertex + 1
1335                );
1336                self.buffer.push(face_str);
1337            }
1338            
1339            self.faces_read += 1;
1340            Some(Ok(face_indices))
1341        } else {
1342            None
1343        }
1344    }
1345}