threecrate_io/
serialization.rs

1//! Mesh serialization utilities with attribute preservation
2//!
3//! This module provides high-level serialization utilities that ensure mesh attributes
4//! survive round-trip across different formats. It integrates with the existing I/O
5//! system and provides opt-in attribute recomputation and validation.
6
7use crate::mesh_attributes::{ExtendedTriangleMesh, MeshAttributeOptions, Tangent};
8use crate::{obj, ply};
9use threecrate_core::{TriangleMesh, Point3f, Vector3f, Result, Error};
10use std::path::Path;
11use std::collections::HashMap;
12
13/// Configuration for mesh serialization
14#[derive(Debug, Clone)]
15pub struct SerializationOptions {
16    /// Options for attribute processing
17    pub attributes: MeshAttributeOptions,
18    /// Whether to preserve custom properties during serialization
19    pub preserve_custom_properties: bool,
20    /// Whether to validate mesh before writing
21    pub validate_before_write: bool,
22    /// Whether to attach metadata to the mesh
23    pub attach_metadata: bool,
24    /// Custom properties to include in serialization
25    pub custom_properties: HashMap<String, Vec<f32>>,
26}
27
28impl Default for SerializationOptions {
29    fn default() -> Self {
30        Self {
31            attributes: MeshAttributeOptions::default(),
32            preserve_custom_properties: true,
33            validate_before_write: true,
34            attach_metadata: true,
35            custom_properties: HashMap::new(),
36        }
37    }
38}
39
40impl SerializationOptions {
41    /// Create options optimized for round-trip preservation
42    pub fn preserve_all() -> Self {
43        Self {
44            attributes: MeshAttributeOptions::recompute_all(),
45            preserve_custom_properties: true,
46            validate_before_write: true,
47            attach_metadata: true,
48            custom_properties: HashMap::new(),
49        }
50    }
51    
52    /// Create options for fast serialization (minimal processing)
53    pub fn fast() -> Self {
54        Self {
55            attributes: MeshAttributeOptions::validate_only(),
56            preserve_custom_properties: false,
57            validate_before_write: false,
58            attach_metadata: false,
59            custom_properties: HashMap::new(),
60        }
61    }
62    
63    /// Add a custom property
64    pub fn with_custom_property<S: Into<String>>(mut self, name: S, values: Vec<f32>) -> Self {
65        self.custom_properties.insert(name.into(), values);
66        self
67    }
68}
69
70/// Enhanced mesh reader with attribute preservation
71pub struct AttributePreservingReader;
72
73/// Enhanced mesh writer with attribute preservation  
74pub struct AttributePreservingWriter;
75
76impl AttributePreservingReader {
77    /// Read mesh with full attribute extraction
78    pub fn read_extended_mesh<P: AsRef<Path>>(
79        path: P, 
80        options: &SerializationOptions
81    ) -> Result<ExtendedTriangleMesh> {
82        let path = path.as_ref();
83        let extension = path.extension()
84            .and_then(|s| s.to_str())
85            .ok_or_else(|| Error::UnsupportedFormat("No file extension found".to_string()))?;
86        
87        match extension.to_lowercase().as_str() {
88            "obj" => Self::read_obj_extended(path, options),
89            "ply" => Self::read_ply_extended(path, options),
90            _ => {
91                // Fall back to basic mesh reading
92                let mesh = crate::read_mesh(path)?;
93                let mut extended = ExtendedTriangleMesh::from_mesh(mesh);
94                extended.metadata.source_format = Some(extension.to_string());
95                extended.process_attributes(&options.attributes)?;
96                Ok(extended)
97            }
98        }
99    }
100    
101    /// Read OBJ file with UV and normal extraction
102    fn read_obj_extended<P: AsRef<Path>>(
103        path: P, 
104        options: &SerializationOptions
105    ) -> Result<ExtendedTriangleMesh> {
106        let obj_data = obj::RobustObjReader::read_obj_file(path)?;
107        let mut extended = Self::obj_data_to_extended_mesh(&obj_data)?;
108        
109        extended.metadata.source_format = Some("obj".to_string());
110        extended.metadata.uvs_loaded = !obj_data.texture_coords.is_empty();
111        
112        if options.attributes.validate_attributes {
113            extended.validate_attributes()?;
114        }
115        
116        // Process attributes if requested
117        extended.process_attributes(&options.attributes)?;
118        
119        Ok(extended)
120    }
121    
122    /// Read PLY file with custom attribute extraction
123    fn read_ply_extended<P: AsRef<Path>>(
124        path: P, 
125        options: &SerializationOptions
126    ) -> Result<ExtendedTriangleMesh> {
127        let ply_data = ply::RobustPlyReader::read_ply_file(path)?;
128        let mut extended = Self::ply_data_to_extended_mesh(&ply_data)?;
129        
130        extended.metadata.source_format = Some("ply".to_string());
131        
132        if options.attributes.validate_attributes {
133            extended.validate_attributes()?;
134        }
135        
136        // Process attributes if requested
137        extended.process_attributes(&options.attributes)?;
138        
139        Ok(extended)
140    }
141    
142    /// Convert OBJ data to extended mesh
143    fn obj_data_to_extended_mesh(obj_data: &obj::ObjData) -> Result<ExtendedTriangleMesh> {
144        // Convert to base mesh first
145        let base_mesh = obj::RobustObjReader::obj_data_to_mesh(obj_data)?;
146        let mut extended = ExtendedTriangleMesh::from_mesh(base_mesh);
147        
148        // Extract UVs if available
149        if !obj_data.texture_coords.is_empty() {
150            let mut uvs = vec![[0.0, 0.0]; extended.vertex_count()];
151            let mut uv_extracted = false;
152            
153            // Map texture coordinates from faces to vertices
154            for group in &obj_data.groups {
155                for face in &group.faces {
156                    for face_vertex in &face.vertices {
157                        if let Some(tex_idx) = face_vertex.texture {
158                            if tex_idx < obj_data.texture_coords.len() {
159                                uvs[face_vertex.vertex] = obj_data.texture_coords[tex_idx];
160                                uv_extracted = true;
161                            }
162                        }
163                    }
164                }
165            }
166            
167            if uv_extracted {
168                extended.set_uvs(uvs);
169            }
170        }
171        
172        Ok(extended)
173    }
174    
175    /// Convert PLY data to extended mesh
176    fn ply_data_to_extended_mesh(ply_data: &ply::PlyData) -> Result<ExtendedTriangleMesh> {
177        // Extract vertices
178        let mut vertices = Vec::new();
179        let mut normals = Vec::new();
180        let mut uvs = Vec::new();
181        let mut tangents = Vec::new();
182        let mut has_normals = false;
183        let mut has_uvs = false;
184        let mut has_tangents = false;
185        
186        if let Some(vertex_elements) = ply_data.elements.get("vertex") {
187            for vertex in vertex_elements {
188                // Extract position
189                let x = vertex.get("x")
190                    .ok_or_else(|| Error::InvalidData("Missing x coordinate".to_string()))?
191                    .as_f32()?;
192                let y = vertex.get("y")
193                    .ok_or_else(|| Error::InvalidData("Missing y coordinate".to_string()))?
194                    .as_f32()?;
195                let z = vertex.get("z")
196                    .ok_or_else(|| Error::InvalidData("Missing z coordinate".to_string()))?
197                    .as_f32()?;
198                
199                vertices.push(Point3f::new(x, y, z));
200                
201                // Extract normals if available
202                if let (Some(nx), Some(ny), Some(nz)) = (
203                    vertex.get("nx"),
204                    vertex.get("ny"),
205                    vertex.get("nz")
206                ) {
207                    normals.push(Vector3f::new(
208                        nx.as_f32()?,
209                        ny.as_f32()?,
210                        nz.as_f32()?,
211                    ));
212                    has_normals = true;
213                }
214                
215                // Extract UVs if available (various common names)
216                if let Some(u) = vertex.get("u").or_else(|| vertex.get("texture_u")) {
217                    if let Some(v) = vertex.get("v").or_else(|| vertex.get("texture_v")) {
218                        uvs.push([u.as_f32()?, v.as_f32()?]);
219                        has_uvs = true;
220                    }
221                } else if let Some(s) = vertex.get("s") {
222                    if let Some(t) = vertex.get("t") {
223                        uvs.push([s.as_f32()?, t.as_f32()?]);
224                        has_uvs = true;
225                    }
226                }
227                
228                // Extract tangents if available
229                if let (Some(tx), Some(ty), Some(tz)) = (
230                    vertex.get("tx").or_else(|| vertex.get("tangent_x")),
231                    vertex.get("ty").or_else(|| vertex.get("tangent_y")),
232                    vertex.get("tz").or_else(|| vertex.get("tangent_z"))
233                ) {
234                    let handedness = vertex.get("th")
235                        .or_else(|| vertex.get("tangent_handedness"))
236                        .map(|h| h.as_f32().unwrap_or(1.0))
237                        .unwrap_or(1.0);
238                    
239                    tangents.push(Tangent::new(
240                        Vector3f::new(tx.as_f32()?, ty.as_f32()?, tz.as_f32()?),
241                        handedness
242                    ));
243                    has_tangents = true;
244                }
245            }
246        }
247        
248        // Extract faces
249        let mut faces = Vec::new();
250        if let Some(face_elements) = ply_data.elements.get("face") {
251            for face in face_elements {
252                let indices = if let Some(vertex_indices) = face.get("vertex_indices") {
253                    vertex_indices.as_usize_list()?
254                } else if let Some(vertex_index) = face.get("vertex_index") {
255                    vertex_index.as_usize_list()?
256                } else {
257                    return Err(Error::InvalidData("Face missing vertex indices".to_string()));
258                };
259                
260                // Convert to triangles
261                if indices.len() >= 3 {
262                    faces.push([indices[0], indices[1], indices[2]]);
263                }
264                if indices.len() == 4 {
265                    faces.push([indices[0], indices[2], indices[3]]);
266                }
267            }
268        }
269        
270        // Create base mesh
271        let mut base_mesh = TriangleMesh::from_vertices_and_faces(vertices, faces);
272        if has_normals && normals.len() == base_mesh.vertex_count() {
273            base_mesh.set_normals(normals);
274        }
275        
276        // Create extended mesh
277        let mut extended = ExtendedTriangleMesh::from_mesh(base_mesh);
278        
279        if has_uvs && uvs.len() == extended.vertex_count() {
280            extended.set_uvs(uvs);
281        }
282        
283        if has_tangents && tangents.len() == extended.vertex_count() {
284            extended.set_tangents(tangents);
285        }
286        
287        Ok(extended)
288    }
289}
290
291impl AttributePreservingWriter {
292    /// Write extended mesh with full attribute preservation
293    pub fn write_extended_mesh<P: AsRef<Path>>(
294        mesh: &ExtendedTriangleMesh,
295        path: P,
296        options: &SerializationOptions,
297    ) -> Result<()> {
298        let path = path.as_ref();
299        let extension = path.extension()
300            .and_then(|s| s.to_str())
301            .ok_or_else(|| Error::UnsupportedFormat("No file extension found".to_string()))?;
302        
303        // Validate mesh if requested
304        if options.validate_before_write {
305            let warnings = crate::mesh_attributes::utils::validate_round_trip(mesh)?;
306            if !warnings.is_empty() {
307                eprintln!("Mesh validation warnings: {:#?}", warnings);
308            }
309        }
310        
311        match extension.to_lowercase().as_str() {
312            "obj" => Self::write_obj_extended(mesh, path, options),
313            "ply" => Self::write_ply_extended(mesh, path, options),
314            _ => {
315                // Fall back to basic mesh writing
316                crate::write_mesh(&mesh.mesh, path)
317            }
318        }
319    }
320    
321    /// Write OBJ file with UV and normal preservation
322    fn write_obj_extended<P: AsRef<Path>>(
323        mesh: &ExtendedTriangleMesh,
324        path: P,
325        options: &SerializationOptions,
326    ) -> Result<()> {
327        let obj_data = Self::extended_mesh_to_obj_data(mesh, options)?;
328        let obj_options = obj::ObjWriteOptions::new()
329            .with_normals(mesh.mesh.normals.is_some())
330            .with_texcoords(mesh.uvs.is_some())
331            .with_comment("Generated by ThreeCrate with attribute preservation");
332        
333        obj::RobustObjWriter::write_obj_file(&obj_data, path, &obj_options)
334    }
335    
336    /// Write PLY file with custom attribute preservation
337    fn write_ply_extended<P: AsRef<Path>>(
338        mesh: &ExtendedTriangleMesh,
339        path: P,
340        options: &SerializationOptions,
341    ) -> Result<()> {
342        let mut ply_options = ply::PlyWriteOptions::ascii()
343            .with_normals(mesh.mesh.normals.is_some())
344            .with_comment("Generated by ThreeCrate with attribute preservation");
345        
346        // Add UV coordinates as custom properties if available
347        if let Some(ref uvs) = mesh.uvs {
348            let u_values: Vec<ply::PlyValue> = uvs.iter().map(|uv| ply::PlyValue::Float(uv[0])).collect();
349            let v_values: Vec<ply::PlyValue> = uvs.iter().map(|uv| ply::PlyValue::Float(uv[1])).collect();
350            ply_options = ply_options
351                .with_custom_vertex_property("u", u_values)
352                .with_custom_vertex_property("v", v_values);
353        }
354        
355        // Add tangents as custom properties if available
356        if let Some(ref tangents) = mesh.tangents {
357            let tx_values: Vec<ply::PlyValue> = tangents.iter().map(|t| ply::PlyValue::Float(t.vector.x)).collect();
358            let ty_values: Vec<ply::PlyValue> = tangents.iter().map(|t| ply::PlyValue::Float(t.vector.y)).collect();
359            let tz_values: Vec<ply::PlyValue> = tangents.iter().map(|t| ply::PlyValue::Float(t.vector.z)).collect();
360            let th_values: Vec<ply::PlyValue> = tangents.iter().map(|t| ply::PlyValue::Float(t.handedness)).collect();
361            
362            ply_options = ply_options
363                .with_custom_vertex_property("tx", tx_values)
364                .with_custom_vertex_property("ty", ty_values)
365                .with_custom_vertex_property("tz", tz_values)
366                .with_custom_vertex_property("th", th_values);
367        }
368        
369        // Add custom properties from options
370        for (name, values) in &options.custom_properties {
371            let ply_values: Vec<ply::PlyValue> = values.iter().map(|&v| ply::PlyValue::Float(v)).collect();
372            ply_options = ply_options.with_custom_vertex_property(name, ply_values);
373        }
374        
375        ply::RobustPlyWriter::write_mesh(&mesh.mesh, path, &ply_options)
376    }
377    
378    /// Convert extended mesh to OBJ data
379    fn extended_mesh_to_obj_data(
380        mesh: &ExtendedTriangleMesh,
381        _options: &SerializationOptions,
382    ) -> Result<obj::ObjData> {
383        let mut obj_data = obj::ObjData {
384            vertices: mesh.mesh.vertices.clone(),
385            texture_coords: Vec::new(),
386            normals: mesh.mesh.normals.clone().unwrap_or_default(),
387            groups: Vec::new(),
388            materials: HashMap::new(),
389            mtl_files: Vec::new(),
390        };
391        
392        // Add texture coordinates if available
393        if let Some(ref uvs) = mesh.uvs {
394            obj_data.texture_coords = uvs.iter().map(|&uv| uv).collect();
395        }
396        
397        // Create faces with proper indexing
398        let mut faces = Vec::new();
399        for &face_indices in &mesh.mesh.faces {
400            let mut face_vertices = Vec::new();
401            for &vertex_idx in &face_indices {
402                let face_vertex = obj::FaceVertex {
403                    vertex: vertex_idx,
404                    texture: if mesh.uvs.is_some() { Some(vertex_idx) } else { None },
405                    normal: if mesh.mesh.normals.is_some() { Some(vertex_idx) } else { None },
406                };
407                face_vertices.push(face_vertex);
408            }
409            
410            faces.push(obj::Face {
411                vertices: face_vertices,
412                material: None,
413            });
414        }
415        
416        // Create default group
417        obj_data.groups.push(obj::Group {
418            name: "default".to_string(),
419            faces,
420        });
421        
422        Ok(obj_data)
423    }
424}
425
426/// High-level utility functions for mesh serialization
427pub mod utils {
428    use super::*;
429    
430    /// Read mesh with attribute preservation
431    pub fn read_mesh_with_attributes<P: AsRef<Path>>(
432        path: P,
433        options: Option<SerializationOptions>,
434    ) -> Result<ExtendedTriangleMesh> {
435        let options = options.unwrap_or_default();
436        AttributePreservingReader::read_extended_mesh(path, &options)
437    }
438    
439    /// Write mesh with attribute preservation
440    pub fn write_mesh_with_attributes<P: AsRef<Path>>(
441        mesh: &ExtendedTriangleMesh,
442        path: P,
443        options: Option<SerializationOptions>,
444    ) -> Result<()> {
445        let options = options.unwrap_or_default();
446        AttributePreservingWriter::write_extended_mesh(mesh, path, &options)
447    }
448    
449    /// Perform a round-trip test for a mesh
450    pub fn test_round_trip<P: AsRef<Path>>(
451        input_path: P,
452        output_path: P,
453        options: Option<SerializationOptions>,
454    ) -> Result<(ExtendedTriangleMesh, Vec<String>)> {
455        let options = options.unwrap_or(SerializationOptions::preserve_all());
456        
457        // Read mesh
458        let mesh = read_mesh_with_attributes(input_path, Some(options.clone()))?;
459        
460        // Write mesh
461        write_mesh_with_attributes(&mesh, output_path, Some(options))?;
462        
463        // Validate round-trip
464        let warnings = crate::mesh_attributes::utils::validate_round_trip(&mesh)?;
465        
466        Ok((mesh, warnings))
467    }
468    
469    /// Ensure mesh is ready for serialization in a specific format
470    pub fn prepare_mesh_for_format(
471        mesh: &mut ExtendedTriangleMesh,
472        format: &str,
473    ) -> Result<()> {
474        crate::mesh_attributes::utils::prepare_for_serialization(mesh, format)
475    }
476    
477    /// Compare two meshes for attribute preservation
478    pub fn compare_meshes(
479        original: &ExtendedTriangleMesh,
480        loaded: &ExtendedTriangleMesh,
481    ) -> Result<Vec<String>> {
482        let mut differences = Vec::new();
483        
484        if original.vertex_count() != loaded.vertex_count() {
485            differences.push(format!(
486                "Vertex count mismatch: {} vs {}",
487                original.vertex_count(),
488                loaded.vertex_count()
489            ));
490        }
491        
492        if original.face_count() != loaded.face_count() {
493            differences.push(format!(
494                "Face count mismatch: {} vs {}",
495                original.face_count(),
496                loaded.face_count()
497            ));
498        }
499        
500        // Compare vertices
501        if original.vertex_count() == loaded.vertex_count() {
502            for (i, (v1, v2)) in original.mesh.vertices.iter()
503                .zip(loaded.mesh.vertices.iter())
504                .enumerate() 
505            {
506                let diff = (v1.x - v2.x).abs() + (v1.y - v2.y).abs() + (v1.z - v2.z).abs();
507                if diff > 1e-5 {
508                    differences.push(format!("Vertex {} differs by {}", i, diff));
509                    break; // Don't spam with too many vertex differences
510                }
511            }
512        }
513        
514        // Compare normals
515        match (&original.mesh.normals, &loaded.mesh.normals) {
516            (Some(n1), Some(n2)) => {
517                if n1.len() != n2.len() {
518                    differences.push("Normal count mismatch".to_string());
519                }
520            }
521            (Some(_), None) => differences.push("Normals lost during round-trip".to_string()),
522            (None, Some(_)) => differences.push("Normals added during round-trip".to_string()),
523            (None, None) => {} // Both missing, that's fine
524        }
525        
526        // Compare UVs
527        match (&original.uvs, &loaded.uvs) {
528            (Some(uv1), Some(uv2)) => {
529                if uv1.len() != uv2.len() {
530                    differences.push("UV count mismatch".to_string());
531                }
532            }
533            (Some(_), None) => differences.push("UVs lost during round-trip".to_string()),
534            (None, Some(_)) => differences.push("UVs added during round-trip".to_string()),
535            (None, None) => {} // Both missing, that's fine
536        }
537        
538        // Compare tangents
539        match (&original.tangents, &loaded.tangents) {
540            (Some(t1), Some(t2)) => {
541                if t1.len() != t2.len() {
542                    differences.push("Tangent count mismatch".to_string());
543                }
544            }
545            (Some(_), None) => differences.push("Tangents lost during round-trip".to_string()),
546            (None, Some(_)) => differences.push("Tangents added during round-trip".to_string()),
547            (None, None) => {} // Both missing, that's fine
548        }
549        
550        Ok(differences)
551    }
552}
553
554#[cfg(test)]
555mod tests {
556    use super::*;
557    use std::fs;
558    
559    fn create_test_extended_mesh() -> ExtendedTriangleMesh {
560        let vertices = vec![
561            Point3f::new(0.0, 0.0, 0.0),
562            Point3f::new(1.0, 0.0, 0.0),
563            Point3f::new(0.5, 1.0, 0.0),
564        ];
565        let faces = vec![[0, 1, 2]];
566        let base_mesh = TriangleMesh::from_vertices_and_faces(vertices, faces);
567        
568        let mut extended = ExtendedTriangleMesh::from_mesh(base_mesh);
569        
570        // Add attributes
571        extended.compute_normals(true, true).unwrap();
572        extended.generate_default_uvs().unwrap();
573        extended.compute_tangents(true).unwrap();
574        
575        extended
576    }
577    
578    #[test]
579    fn test_serialization_options() {
580        let options = SerializationOptions::preserve_all();
581        assert!(options.attributes.recompute_normals);
582        assert!(options.attributes.recompute_tangents);
583        assert!(options.validate_before_write);
584        
585        let fast_options = SerializationOptions::fast();
586        assert!(!fast_options.validate_before_write);
587        assert!(!fast_options.preserve_custom_properties);
588    }
589    
590    #[test]
591    fn test_obj_round_trip() {
592        let mesh = create_test_extended_mesh();
593        let temp_file = "test_obj_round_trip.obj";
594        
595        let options = SerializationOptions::preserve_all();
596        
597        // Write mesh
598        AttributePreservingWriter::write_extended_mesh(&mesh, temp_file, &options).unwrap();
599        
600        // Read mesh back
601        let loaded_mesh = AttributePreservingReader::read_extended_mesh(temp_file, &options).unwrap();
602        
603        // Compare
604        let differences = utils::compare_meshes(&mesh, &loaded_mesh).unwrap();
605        assert!(differences.is_empty(), "Round-trip differences: {:#?}", differences);
606        
607        // Cleanup
608        let _ = fs::remove_file(temp_file);
609    }
610    
611    #[test]
612    fn test_ply_round_trip() {
613        let mesh = create_test_extended_mesh();
614        let temp_file = "test_ply_round_trip.ply";
615        
616        let options = SerializationOptions::preserve_all();
617        
618        // Write mesh
619        AttributePreservingWriter::write_extended_mesh(&mesh, temp_file, &options).unwrap();
620        
621        // Read mesh back
622        let loaded_mesh = AttributePreservingReader::read_extended_mesh(temp_file, &options).unwrap();
623        
624        // Compare
625        let differences = utils::compare_meshes(&mesh, &loaded_mesh).unwrap();
626        // Note: PLY round-trip might have some differences due to custom property handling
627        println!("PLY round-trip differences: {:#?}", differences);
628        
629        // Cleanup
630        let _ = fs::remove_file(temp_file);
631    }
632    
633    #[test]
634    fn test_attribute_validation() {
635        let mesh = create_test_extended_mesh();
636        let warnings = crate::mesh_attributes::utils::validate_round_trip(&mesh).unwrap();
637        
638        // Should have minimal warnings for a complete mesh
639        assert!(warnings.len() <= 1, "Too many warnings: {:#?}", warnings);
640    }
641    
642    #[test]
643    fn test_mesh_comparison() {
644        let mesh1 = create_test_extended_mesh();
645        let mesh2 = create_test_extended_mesh();
646        
647        let differences = utils::compare_meshes(&mesh1, &mesh2).unwrap();
648        assert!(differences.is_empty(), "Identical meshes should have no differences");
649    }
650}