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 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/// 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]
226            .parse::<usize>()
227            .map_err(|_| Error::InvalidData(format!("Invalid vertex index: {}", parts[0])))?;
228
229        // OBJ uses 1-based indexing, convert to 0-based
230        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        // Check if file starts with "#" or "v " (vertex definition)
276        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        // Write header
298        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        // Write vertices
304        for vertex in &mesh.vertices {
305            writeln!(writer, "v {} {} {}", vertex.x, vertex.y, vertex.z)?;
306        }
307        writeln!(writer)?;
308
309        // Write normals if available
310        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        // Write faces
318        if mesh.normals.is_some() {
319            // Write faces with normals
320            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            // Write faces without normals
334            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
347// Keep the legacy trait implementations for backward compatibility
348impl 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
362/// Read an OBJ file and return vertex positions only (useful for point clouds)
363pub 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
368/// Write vertices as an OBJ file (useful for point clouds)
369pub 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    /// Read a complete OBJ file with materials
386    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            // Skip empty lines and comments
411            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                    // Vertex position
423                    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                    // Texture coordinate
444                    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                    // Vertex normal
462                    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                    // Face
483                    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                    // Triangulate if necessary
504                    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                    // Group
514                    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                    // Use material
531                    if parts.len() > 1 {
532                        current_material = Some(parts[1].to_string());
533                    }
534                }
535                "mtllib" => {
536                    // Material library
537                    if parts.len() > 1 {
538                        let mtl_file = parts[1].to_string();
539                        obj_data.mtl_files.push(mtl_file.clone());
540
541                        // Try to load the MTL file
542                        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                    // Ignore unknown commands
555                }
556            }
557        }
558
559        // Add the last group if it has faces
560        if !current_group.faces.is_empty() {
561            obj_data.groups.push(current_group);
562        }
563
564        // If no groups were created, create a default group with all faces
565        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    /// Read an MTL file and return materials
576    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            // Skip empty lines and comments
588            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                    // Save previous material
600                    if let Some(material) = current_material.take() {
601                        materials.insert(material.name.clone(), material);
602                    }
603
604                    // Start new material
605                    if parts.len() > 1 {
606                        current_material = Some(Material::new(parts[1].to_string()));
607                    }
608                }
609                "Ka" => {
610                    // Ambient color
611                    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                    // Diffuse color
625                    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                    // Specular color
639                    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                    // Specular exponent
653                    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                    // Transparency (dissolve)
663                    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                    // Transparency (alternative)
673                    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); // Tr is inverse of d
677                            }
678                        }
679                    }
680                }
681                "illum" => {
682                    // Illumination model
683                    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                    // Diffuse texture map
693                    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                    // Normal/bump map
701                    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                    // Specular map
709                    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                    // Ignore unknown material properties
717                }
718            }
719        }
720
721        // Save the last material
722        if let Some(material) = current_material {
723            materials.insert(material.name.clone(), material);
724        }
725
726        Ok(materials)
727    }
728
729    /// Triangulate a face with arbitrary number of vertices
730    fn triangulate_face(vertices: &[FaceVertex]) -> Vec<Vec<FaceVertex>> {
731        match vertices.len() {
732            3 => {
733                // Already a triangle
734                vec![vertices.to_vec()]
735            }
736            4 => {
737                // Quad - split into two triangles
738                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                // N-gon - fan triangulation from first vertex
745                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                // Degenerate face
753                vec![]
754            }
755        }
756    }
757
758    /// Convert ObjData to TriangleMesh
759    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        // Collect all faces from all groups
765        for group in &obj_data.groups {
766            for face in &group.faces {
767                if face.vertices.len() != 3 {
768                    continue; // Skip non-triangular faces (shouldn't happen after triangulation)
769                }
770
771                // Extract vertex indices
772                let face_indices = [
773                    face.vertices[0].vertex,
774                    face.vertices[1].vertex,
775                    face.vertices[2].vertex,
776                ];
777
778                // Validate vertex indices
779                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                // Handle normals if available
792                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                            // If some vertices have normals, all should have normals
806                            // Use a default normal for consistency
807                            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        // Set normals if available
817        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    /// Write a complete OBJ file with optional MTL material file
827    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        // Write header comments
837        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        // Write MTL library reference if materials should be written
851        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        // Write object name if specified
862        if let Some(ref object_name) = options.object_name {
863            writeln!(writer, "o {}", object_name)?;
864            writeln!(writer)?;
865        }
866
867        // Write vertices
868        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        // Write texture coordinates if available and requested
876        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        // Write vertex normals if available and requested
884        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        // Write groups and faces
892        for group in &obj_data.groups {
893            // Write group name
894            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            // Track current material
907            let mut current_material: Option<&String> = None;
908
909            for face in &group.faces {
910                // Write material change if needed
911                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 face
924                write!(writer, "f")?;
925                for vertex in &face.vertices {
926                    write!(writer, " {}", vertex.vertex + 1)?; // Convert to 1-based indexing
927
928                    // Add texture coordinate if available and requested
929                    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                    // Add normal if available and requested
938                    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        // Write MTL file if requested
959        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    /// Write a TriangleMesh as OBJ with options
973    pub fn write_mesh<P: AsRef<Path>>(
974        mesh: &TriangleMesh,
975        path: P,
976        options: &ObjWriteOptions,
977    ) -> Result<()> {
978        // Convert mesh to ObjData
979        let obj_data = Self::mesh_to_obj_data(mesh, options)?;
980        Self::write_obj_file(&obj_data, path, options)
981    }
982
983    /// Write MTL material file
984    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            // Write ambient color
999            if let Some(ambient) = material.ambient {
1000                writeln!(writer, "Ka {} {} {}", ambient[0], ambient[1], ambient[2])?;
1001            }
1002
1003            // Write diffuse color
1004            if let Some(diffuse) = material.diffuse {
1005                writeln!(writer, "Kd {} {} {}", diffuse[0], diffuse[1], diffuse[2])?;
1006            }
1007
1008            // Write specular color
1009            if let Some(specular) = material.specular {
1010                writeln!(writer, "Ks {} {} {}", specular[0], specular[1], specular[2])?;
1011            }
1012
1013            // Write shininess
1014            if let Some(shininess) = material.shininess {
1015                writeln!(writer, "Ns {}", shininess)?;
1016            }
1017
1018            // Write transparency
1019            if let Some(transparency) = material.transparency {
1020                writeln!(writer, "d {}", transparency)?;
1021            }
1022
1023            // Write illumination model
1024            if let Some(illumination) = material.illumination {
1025                writeln!(writer, "illum {}", illumination)?;
1026            }
1027
1028            // Write texture maps
1029            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    /// Convert TriangleMesh to ObjData
1048    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        // Add normals if available and requested
1059        if options.write_normals {
1060            if let Some(ref normals) = mesh.normals {
1061                obj_data.normals = normals.clone();
1062            }
1063        }
1064
1065        // Create faces with proper indexing
1066        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, // No texture coordinates in TriangleMesh
1073                    normal: if options.write_normals && mesh.normals.is_some() {
1074                        Some(vertex_idx) // Assume per-vertex normals
1075                    } 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        // Create a single group
1089        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        // Add material if specified
1099        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    /// Generate MTL filename from OBJ path
1108    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
1117/// Streaming OBJ reader for point clouds (vertices only)
1118pub 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    /// Create a new streaming OBJ reader for vertices
1129    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        // First pass: count vertices
1135        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        // Reset file position
1149        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        // Read a chunk if buffer is empty
1172        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, // EOF
1181                        Ok(_) => {
1182                            self.current_line += 1;
1183                            let trimmed = line.trim();
1184
1185                            // Skip non-vertex lines
1186                            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        // Process next vertex from buffer
1203        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
1249/// Streaming OBJ reader for mesh faces
1250pub 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>, // We need to store vertices to convert face indices
1258}
1259
1260impl ObjMeshStreamingReader {
1261    /// Create a new streaming OBJ reader for faces
1262    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        // First pass: collect vertices and count faces
1268        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        // Reset file position
1295        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        // Read a chunk if buffer is empty
1319        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, // EOF
1328                        Ok(_) => {
1329                            self.current_line += 1;
1330                            let trimmed = line.trim();
1331
1332                            // Skip non-face lines
1333                            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        // Process next face from buffer
1347        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            // Parse face vertices (only need vertex indices, not texture/normal)
1358            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            // Triangulate if necessary
1373            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            // Return first triangle, store others for later
1389            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            // Validate indices
1397            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            // Store remaining triangles in buffer for next calls
1408            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}