Skip to main content

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