1use 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#[derive(Debug, Clone)]
15pub struct SerializationOptions {
16 pub attributes: MeshAttributeOptions,
18 pub preserve_custom_properties: bool,
20 pub validate_before_write: bool,
22 pub attach_metadata: bool,
24 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 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 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 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
70pub struct AttributePreservingReader;
72
73pub struct AttributePreservingWriter;
75
76impl AttributePreservingReader {
77 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 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 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 extended.process_attributes(&options.attributes)?;
119
120 Ok(extended)
121 }
122
123 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 extended.process_attributes(&options.attributes)?;
139
140 Ok(extended)
141 }
142
143 fn obj_data_to_extended_mesh(obj_data: &obj::ObjData) -> Result<ExtendedTriangleMesh> {
145 let base_mesh = obj::RobustObjReader::obj_data_to_mesh(obj_data)?;
147 let mut extended = ExtendedTriangleMesh::from_mesh(base_mesh);
148
149 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 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 fn ply_data_to_extended_mesh(ply_data: &ply::PlyData) -> Result<ExtendedTriangleMesh> {
178 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 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 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 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 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 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 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 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 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 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 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 crate::write_mesh(&mesh.mesh, path)
319 }
320 }
321 }
322
323 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 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 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 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 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 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 if let Some(ref uvs) = mesh.uvs {
411 obj_data.texture_coords = uvs.iter().map(|&uv| uv).collect();
412 }
413
414 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 obj_data.groups.push(obj::Group {
443 name: "default".to_string(),
444 faces,
445 });
446
447 Ok(obj_data)
448 }
449}
450
451pub mod utils {
453 use super::*;
454
455 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 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 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 let mesh = read_mesh_with_attributes(input_path, Some(options.clone()))?;
484
485 write_mesh_with_attributes(&mesh, output_path, Some(options))?;
487
488 let warnings = crate::mesh_attributes::utils::validate_round_trip(&mesh)?;
490
491 Ok((mesh, warnings))
492 }
493
494 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 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 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; }
536 }
537 }
538
539 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) => {} }
550
551 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) => {} }
562
563 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) => {} }
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 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 AttributePreservingWriter::write_extended_mesh(&mesh, temp_file, &options).unwrap();
624
625 let loaded_mesh =
627 AttributePreservingReader::read_extended_mesh(temp_file, &options).unwrap();
628
629 let differences = utils::compare_meshes(&mesh, &loaded_mesh).unwrap();
631 assert!(
632 differences.is_empty(),
633 "Round-trip differences: {:#?}",
634 differences
635 );
636
637 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 AttributePreservingWriter::write_extended_mesh(&mesh, temp_file, &options).unwrap();
650
651 let loaded_mesh =
653 AttributePreservingReader::read_extended_mesh(temp_file, &options).unwrap();
654
655 let differences = utils::compare_meshes(&mesh, &loaded_mesh).unwrap();
657 println!("PLY round-trip differences: {:#?}", differences);
659
660 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 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}