Skip to main content

threecrate_io/
mesh_attributes.rs

1//! Mesh attribute serialization utilities
2//!
3//! This module provides comprehensive mesh attribute handling for serialization,
4//! including normals, tangents, and UV coordinates. It ensures that mesh attributes
5//! survive round-trip across different formats with optional recomputation.
6
7use threecrate_core::{Error, Result, TriangleMesh, Vector3f};
8
9#[cfg(test)]
10use threecrate_core::Point3f;
11
12/// Texture coordinates (UV mapping)
13pub type UV = [f32; 2];
14
15/// Tangent vector with handedness information
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct Tangent {
18    /// Tangent vector
19    pub vector: Vector3f,
20    /// Handedness (-1.0 or 1.0)
21    pub handedness: f32,
22}
23
24/// Extended mesh with full attribute support
25#[derive(Debug, Clone)]
26pub struct ExtendedTriangleMesh {
27    /// Base mesh data
28    pub mesh: TriangleMesh,
29    /// Texture coordinates per vertex
30    pub uvs: Option<Vec<UV>>,
31    /// Tangent vectors per vertex
32    pub tangents: Option<Vec<Tangent>>,
33    /// Metadata for tracking attribute completeness
34    pub metadata: MeshMetadata,
35}
36
37/// Metadata tracking mesh attribute completeness and validation
38#[derive(Debug, Clone, Default)]
39pub struct MeshMetadata {
40    /// Whether normals were computed or loaded
41    pub normals_computed: bool,
42    /// Whether tangents were computed or loaded  
43    pub tangents_computed: bool,
44    /// Whether UVs were loaded from file
45    pub uvs_loaded: bool,
46    /// Validation errors or warnings
47    pub validation_messages: Vec<String>,
48    /// Original format information
49    pub source_format: Option<String>,
50    /// Attribute completeness score (0.0 to 1.0)
51    pub completeness_score: f32,
52}
53
54/// Configuration for mesh attribute processing
55#[derive(Debug, Clone)]
56pub struct MeshAttributeOptions {
57    /// Recompute normals if missing
58    pub recompute_normals: bool,
59    /// Recompute tangents if missing (requires UVs)
60    pub recompute_tangents: bool,
61    /// Generate default UVs if missing
62    pub generate_default_uvs: bool,
63    /// Validate attribute consistency
64    pub validate_attributes: bool,
65    /// Normalize vectors after computation
66    pub normalize_vectors: bool,
67    /// Smooth normals across shared vertices
68    pub smooth_normals: bool,
69}
70
71impl Default for MeshAttributeOptions {
72    fn default() -> Self {
73        Self {
74            recompute_normals: true,
75            recompute_tangents: false, // Requires UVs, so disabled by default
76            generate_default_uvs: false,
77            validate_attributes: true,
78            normalize_vectors: true,
79            smooth_normals: true,
80        }
81    }
82}
83
84impl MeshAttributeOptions {
85    /// Create options with all recomputation enabled
86    pub fn recompute_all() -> Self {
87        Self {
88            recompute_normals: true,
89            recompute_tangents: true,
90            generate_default_uvs: true,
91            validate_attributes: true,
92            normalize_vectors: true,
93            smooth_normals: true,
94        }
95    }
96
97    /// Create options for read-only validation
98    pub fn validate_only() -> Self {
99        Self {
100            recompute_normals: false,
101            recompute_tangents: false,
102            generate_default_uvs: false,
103            validate_attributes: true,
104            normalize_vectors: false,
105            smooth_normals: false,
106        }
107    }
108}
109
110impl Tangent {
111    /// Create a new tangent with vector and handedness
112    pub fn new(vector: Vector3f, handedness: f32) -> Self {
113        Self { vector, handedness }
114    }
115
116    /// Create a tangent from a vector (handedness = 1.0)
117    pub fn from_vector(vector: Vector3f) -> Self {
118        Self::new(vector, 1.0)
119    }
120}
121
122impl ExtendedTriangleMesh {
123    /// Create from a base TriangleMesh
124    pub fn from_mesh(mesh: TriangleMesh) -> Self {
125        Self {
126            mesh,
127            uvs: None,
128            tangents: None,
129            metadata: MeshMetadata::default(),
130        }
131    }
132
133    /// Create with full attributes
134    pub fn new(mesh: TriangleMesh, uvs: Option<Vec<UV>>, tangents: Option<Vec<Tangent>>) -> Self {
135        let mut extended = Self {
136            mesh,
137            uvs,
138            tangents,
139            metadata: MeshMetadata::default(),
140        };
141        extended.update_metadata();
142        extended
143    }
144
145    /// Get vertex count
146    pub fn vertex_count(&self) -> usize {
147        self.mesh.vertex_count()
148    }
149
150    /// Get face count
151    pub fn face_count(&self) -> usize {
152        self.mesh.face_count()
153    }
154
155    /// Check if mesh is empty
156    pub fn is_empty(&self) -> bool {
157        self.mesh.is_empty()
158    }
159
160    /// Set UV coordinates
161    pub fn set_uvs(&mut self, uvs: Vec<UV>) {
162        if uvs.len() == self.vertex_count() {
163            self.uvs = Some(uvs);
164            self.metadata.uvs_loaded = true;
165            self.update_metadata();
166        }
167    }
168
169    /// Set tangent vectors
170    pub fn set_tangents(&mut self, tangents: Vec<Tangent>) {
171        if tangents.len() == self.vertex_count() {
172            self.tangents = Some(tangents);
173            self.metadata.tangents_computed = true;
174            self.update_metadata();
175        }
176    }
177
178    /// Process mesh attributes with given options
179    pub fn process_attributes(&mut self, options: &MeshAttributeOptions) -> Result<()> {
180        if options.validate_attributes {
181            self.validate_attributes()?;
182        }
183
184        if options.recompute_normals && self.mesh.normals.is_none() {
185            self.compute_normals(options.smooth_normals, options.normalize_vectors)?;
186        }
187
188        if options.generate_default_uvs && self.uvs.is_none() {
189            self.generate_default_uvs()?;
190        }
191
192        if options.recompute_tangents && self.tangents.is_none() && self.uvs.is_some() {
193            self.compute_tangents(options.normalize_vectors)?;
194        }
195
196        self.update_metadata();
197        Ok(())
198    }
199
200    /// Validate attribute consistency
201    pub fn validate_attributes(&mut self) -> Result<()> {
202        let vertex_count = self.vertex_count();
203        self.metadata.validation_messages.clear();
204
205        // Check normals
206        if let Some(ref normals) = self.mesh.normals {
207            if normals.len() != vertex_count {
208                let msg = format!(
209                    "Normal count mismatch: {} normals for {} vertices",
210                    normals.len(),
211                    vertex_count
212                );
213                self.metadata.validation_messages.push(msg.clone());
214                return Err(Error::InvalidData(msg));
215            }
216
217            // Check for zero-length normals
218            for (i, normal) in normals.iter().enumerate() {
219                let length_sq = normal.x * normal.x + normal.y * normal.y + normal.z * normal.z;
220                if length_sq < 1e-6 {
221                    let msg = format!("Zero-length normal at vertex {}", i);
222                    self.metadata.validation_messages.push(msg);
223                }
224            }
225        }
226
227        // Check UVs
228        if let Some(ref uvs) = self.uvs {
229            if uvs.len() != vertex_count {
230                let msg = format!(
231                    "UV count mismatch: {} UVs for {} vertices",
232                    uvs.len(),
233                    vertex_count
234                );
235                self.metadata.validation_messages.push(msg.clone());
236                return Err(Error::InvalidData(msg));
237            }
238
239            // Check for invalid UV coordinates
240            for (i, uv) in uvs.iter().enumerate() {
241                if !uv[0].is_finite() || !uv[1].is_finite() {
242                    let msg = format!(
243                        "Invalid UV coordinates at vertex {}: [{}, {}]",
244                        i, uv[0], uv[1]
245                    );
246                    self.metadata.validation_messages.push(msg);
247                }
248            }
249        }
250
251        // Check tangents
252        if let Some(ref tangents) = self.tangents {
253            if tangents.len() != vertex_count {
254                let msg = format!(
255                    "Tangent count mismatch: {} tangents for {} vertices",
256                    tangents.len(),
257                    vertex_count
258                );
259                self.metadata.validation_messages.push(msg.clone());
260                return Err(Error::InvalidData(msg));
261            }
262
263            // Check for zero-length tangents and valid handedness
264            for (i, tangent) in tangents.iter().enumerate() {
265                let length_sq = tangent.vector.x * tangent.vector.x
266                    + tangent.vector.y * tangent.vector.y
267                    + tangent.vector.z * tangent.vector.z;
268                if length_sq < 1e-6 {
269                    let msg = format!("Zero-length tangent at vertex {}", i);
270                    self.metadata.validation_messages.push(msg);
271                }
272
273                if tangent.handedness.abs() != 1.0 {
274                    let msg = format!(
275                        "Invalid tangent handedness at vertex {}: {}",
276                        i, tangent.handedness
277                    );
278                    self.metadata.validation_messages.push(msg);
279                }
280            }
281        }
282
283        Ok(())
284    }
285
286    /// Compute vertex normals
287    pub fn compute_normals(&mut self, smooth: bool, normalize: bool) -> Result<()> {
288        let vertex_count = self.vertex_count();
289        let mut normals = vec![Vector3f::new(0.0, 0.0, 0.0); vertex_count];
290
291        // Compute face normals and accumulate to vertices
292        for face in &self.mesh.faces {
293            let v0 = self.mesh.vertices[face[0]];
294            let v1 = self.mesh.vertices[face[1]];
295            let v2 = self.mesh.vertices[face[2]];
296
297            let edge1 = v1 - v0;
298            let edge2 = v2 - v0;
299            let face_normal = edge1.cross(&edge2);
300
301            if smooth {
302                // Smooth shading: accumulate face normals to vertices
303                normals[face[0]] = normals[face[0]] + face_normal;
304                normals[face[1]] = normals[face[1]] + face_normal;
305                normals[face[2]] = normals[face[2]] + face_normal;
306            } else {
307                // Flat shading: use face normal for all vertices
308                normals[face[0]] = face_normal;
309                normals[face[1]] = face_normal;
310                normals[face[2]] = face_normal;
311            }
312        }
313
314        // Normalize if requested
315        if normalize {
316            for normal in &mut normals {
317                let length =
318                    (normal.x * normal.x + normal.y * normal.y + normal.z * normal.z).sqrt();
319                if length > 1e-6 {
320                    *normal =
321                        Vector3f::new(normal.x / length, normal.y / length, normal.z / length);
322                } else {
323                    *normal = Vector3f::new(0.0, 0.0, 1.0); // Default up vector
324                }
325            }
326        }
327
328        self.mesh.set_normals(normals);
329        self.metadata.normals_computed = true;
330        Ok(())
331    }
332
333    /// Compute tangent vectors using Lengyel's method
334    pub fn compute_tangents(&mut self, normalize: bool) -> Result<()> {
335        let uvs = self.uvs.as_ref().ok_or_else(|| {
336            Error::InvalidData("UV coordinates required for tangent computation".to_string())
337        })?;
338
339        let vertex_count = self.vertex_count();
340        let mut tan1 = vec![Vector3f::new(0.0, 0.0, 0.0); vertex_count];
341        let mut tan2 = vec![Vector3f::new(0.0, 0.0, 0.0); vertex_count];
342
343        // Compute tangents per face
344        for face in &self.mesh.faces {
345            let i1 = face[0];
346            let i2 = face[1];
347            let i3 = face[2];
348
349            let v1 = self.mesh.vertices[i1];
350            let v2 = self.mesh.vertices[i2];
351            let v3 = self.mesh.vertices[i3];
352
353            let w1 = uvs[i1];
354            let w2 = uvs[i2];
355            let w3 = uvs[i3];
356
357            let x1 = v2.x - v1.x;
358            let x2 = v3.x - v1.x;
359            let y1 = v2.y - v1.y;
360            let y2 = v3.y - v1.y;
361            let z1 = v2.z - v1.z;
362            let z2 = v3.z - v1.z;
363
364            let s1 = w2[0] - w1[0];
365            let s2 = w3[0] - w1[0];
366            let t1 = w2[1] - w1[1];
367            let t2 = w3[1] - w1[1];
368
369            let det = s1 * t2 - s2 * t1;
370            let r = if det.abs() < 1e-6 { 1.0 } else { 1.0 / det };
371
372            let sdir = Vector3f::new(
373                (t2 * x1 - t1 * x2) * r,
374                (t2 * y1 - t1 * y2) * r,
375                (t2 * z1 - t1 * z2) * r,
376            );
377
378            let tdir = Vector3f::new(
379                (s1 * x2 - s2 * x1) * r,
380                (s1 * y2 - s2 * y1) * r,
381                (s1 * z2 - s2 * z1) * r,
382            );
383
384            tan1[i1] = tan1[i1] + sdir;
385            tan1[i2] = tan1[i2] + sdir;
386            tan1[i3] = tan1[i3] + sdir;
387
388            tan2[i1] = tan2[i1] + tdir;
389            tan2[i2] = tan2[i2] + tdir;
390            tan2[i3] = tan2[i3] + tdir;
391        }
392
393        let normals = self.mesh.normals.as_ref().ok_or_else(|| {
394            Error::InvalidData("Normals required for tangent computation".to_string())
395        })?;
396
397        // Compute final tangents with Gram-Schmidt orthogonalization
398        let mut tangents = Vec::with_capacity(vertex_count);
399
400        for i in 0..vertex_count {
401            let n = normals[i];
402            let t = tan1[i];
403
404            // Gram-Schmidt orthogonalize
405            let tangent_vec = t - n * (n.x * t.x + n.y * t.y + n.z * t.z);
406
407            let tangent_vec = if normalize {
408                let length = (tangent_vec.x * tangent_vec.x
409                    + tangent_vec.y * tangent_vec.y
410                    + tangent_vec.z * tangent_vec.z)
411                    .sqrt();
412                if length > 1e-6 {
413                    Vector3f::new(
414                        tangent_vec.x / length,
415                        tangent_vec.y / length,
416                        tangent_vec.z / length,
417                    )
418                } else {
419                    Vector3f::new(1.0, 0.0, 0.0) // Default tangent
420                }
421            } else {
422                tangent_vec
423            };
424
425            // Calculate handedness
426            let cross = n.cross(&tangent_vec);
427            let handedness =
428                if cross.x * tan2[i].x + cross.y * tan2[i].y + cross.z * tan2[i].z < 0.0 {
429                    -1.0
430                } else {
431                    1.0
432                };
433
434            tangents.push(Tangent::new(tangent_vec, handedness));
435        }
436
437        self.tangents = Some(tangents);
438        self.metadata.tangents_computed = true;
439        Ok(())
440    }
441
442    /// Generate default UV coordinates (planar projection)
443    pub fn generate_default_uvs(&mut self) -> Result<()> {
444        let vertex_count = self.vertex_count();
445
446        // Find bounding box
447        let mut min_x = f32::INFINITY;
448        let mut max_x = f32::NEG_INFINITY;
449        let mut min_y = f32::INFINITY;
450        let mut max_y = f32::NEG_INFINITY;
451        let mut min_z = f32::INFINITY;
452        let mut max_z = f32::NEG_INFINITY;
453
454        for vertex in &self.mesh.vertices {
455            min_x = min_x.min(vertex.x);
456            max_x = max_x.max(vertex.x);
457            min_y = min_y.min(vertex.y);
458            max_y = max_y.max(vertex.y);
459            min_z = min_z.min(vertex.z);
460            max_z = max_z.max(vertex.z);
461        }
462
463        let size_x = max_x - min_x;
464        let size_y = max_y - min_y;
465        let size_z = max_z - min_z;
466
467        // Choose projection plane based on largest dimension
468        let mut uvs = Vec::with_capacity(vertex_count);
469
470        if size_x >= size_y && size_x >= size_z {
471            // Project onto YZ plane
472            for vertex in &self.mesh.vertices {
473                let u = if size_y > 1e-6 {
474                    (vertex.y - min_y) / size_y
475                } else {
476                    0.5
477                };
478                let v = if size_z > 1e-6 {
479                    (vertex.z - min_z) / size_z
480                } else {
481                    0.5
482                };
483                uvs.push([u, v]);
484            }
485        } else if size_y >= size_x && size_y >= size_z {
486            // Project onto XZ plane
487            for vertex in &self.mesh.vertices {
488                let u = if size_x > 1e-6 {
489                    (vertex.x - min_x) / size_x
490                } else {
491                    0.5
492                };
493                let v = if size_z > 1e-6 {
494                    (vertex.z - min_z) / size_z
495                } else {
496                    0.5
497                };
498                uvs.push([u, v]);
499            }
500        } else {
501            // Project onto XY plane
502            for vertex in &self.mesh.vertices {
503                let u = if size_x > 1e-6 {
504                    (vertex.x - min_x) / size_x
505                } else {
506                    0.5
507                };
508                let v = if size_y > 1e-6 {
509                    (vertex.y - min_y) / size_y
510                } else {
511                    0.5
512                };
513                uvs.push([u, v]);
514            }
515        }
516
517        self.uvs = Some(uvs);
518        Ok(())
519    }
520
521    /// Update metadata based on current state
522    fn update_metadata(&mut self) {
523        let vertex_count = self.vertex_count();
524        if vertex_count == 0 {
525            self.metadata.completeness_score = 0.0;
526            return;
527        }
528
529        let mut score = 1.0; // Base score for having vertices
530
531        // Check normals
532        if let Some(ref normals) = self.mesh.normals {
533            if normals.len() == vertex_count {
534                score += 1.0;
535            } else {
536                score += 0.5; // Partial credit
537            }
538        }
539
540        // Check UVs
541        if let Some(ref uvs) = self.uvs {
542            if uvs.len() == vertex_count {
543                score += 1.0;
544            } else {
545                score += 0.5; // Partial credit
546            }
547        }
548
549        // Check tangents
550        if let Some(ref tangents) = self.tangents {
551            if tangents.len() == vertex_count {
552                score += 1.0;
553            } else {
554                score += 0.5; // Partial credit
555            }
556        }
557
558        self.metadata.completeness_score = score / 4.0; // Normalize to 0-1 range
559    }
560
561    /// Convert back to base TriangleMesh (loses extended attributes)
562    pub fn to_triangle_mesh(self) -> TriangleMesh {
563        self.mesh
564    }
565}
566
567impl MeshMetadata {
568    /// Create metadata for a loaded mesh
569    pub fn from_loaded(
570        format: &str,
571        _has_normals: bool,
572        has_uvs: bool,
573        _has_tangents: bool,
574    ) -> Self {
575        Self {
576            normals_computed: false,
577            tangents_computed: false,
578            uvs_loaded: has_uvs,
579            validation_messages: Vec::new(),
580            source_format: Some(format.to_string()),
581            completeness_score: 0.0, // Will be updated by mesh
582        }
583    }
584
585    /// Check if mesh has complete attributes
586    pub fn is_complete(&self) -> bool {
587        self.completeness_score >= 0.75 // 75% completeness threshold
588    }
589
590    /// Get a summary of missing attributes
591    pub fn missing_attributes(&self) -> Vec<&'static str> {
592        let mut missing = Vec::new();
593
594        if !self.normals_computed && self.completeness_score < 0.5 {
595            missing.push("normals");
596        }
597        if !self.uvs_loaded {
598            missing.push("uvs");
599        }
600        if !self.tangents_computed {
601            missing.push("tangents");
602        }
603
604        missing
605    }
606}
607
608/// Utility functions for mesh attribute processing
609pub mod utils {
610    use super::*;
611
612    /// Convert TriangleMesh to ExtendedTriangleMesh with attribute processing
613    pub fn extend_mesh(
614        mesh: TriangleMesh,
615        options: &MeshAttributeOptions,
616    ) -> Result<ExtendedTriangleMesh> {
617        let mut extended = ExtendedTriangleMesh::from_mesh(mesh);
618        extended.process_attributes(options)?;
619        Ok(extended)
620    }
621
622    /// Ensure mesh has all basic attributes (normals at minimum)
623    pub fn ensure_basic_attributes(mesh: &mut ExtendedTriangleMesh) -> Result<()> {
624        let options = MeshAttributeOptions {
625            recompute_normals: true,
626            recompute_tangents: false,
627            generate_default_uvs: false,
628            validate_attributes: true,
629            normalize_vectors: true,
630            smooth_normals: true,
631        };
632        mesh.process_attributes(&options)
633    }
634
635    /// Prepare mesh for serialization with full attributes
636    pub fn prepare_for_serialization(mesh: &mut ExtendedTriangleMesh, format: &str) -> Result<()> {
637        let options = match format.to_lowercase().as_str() {
638            "obj" => MeshAttributeOptions {
639                recompute_normals: true,
640                recompute_tangents: false, // OBJ doesn't typically store tangents
641                generate_default_uvs: true, // OBJ commonly has UVs
642                validate_attributes: true,
643                normalize_vectors: true,
644                smooth_normals: true,
645            },
646            "ply" => MeshAttributeOptions {
647                recompute_normals: true,
648                recompute_tangents: false, // PLY can store custom attributes
649                generate_default_uvs: false, // PLY is more flexible
650                validate_attributes: true,
651                normalize_vectors: true,
652                smooth_normals: true,
653            },
654            _ => MeshAttributeOptions::default(),
655        };
656
657        mesh.process_attributes(&options)?;
658        mesh.metadata.source_format = Some(format.to_string());
659        Ok(())
660    }
661
662    /// Validate mesh for round-trip compatibility
663    pub fn validate_round_trip(mesh: &ExtendedTriangleMesh) -> Result<Vec<String>> {
664        let mut warnings = Vec::new();
665
666        if mesh.vertex_count() == 0 {
667            warnings.push("Empty mesh - no vertices".to_string());
668        }
669
670        if mesh.face_count() == 0 {
671            warnings.push("Mesh has no faces".to_string());
672        }
673
674        if mesh.mesh.normals.is_none() {
675            warnings.push("Missing normals - may be recomputed on load".to_string());
676        }
677
678        if mesh.uvs.is_none() {
679            warnings.push("Missing UV coordinates - texture mapping not available".to_string());
680        }
681
682        if mesh.tangents.is_none() && mesh.uvs.is_some() {
683            warnings.push("Missing tangents - normal mapping may not work correctly".to_string());
684        }
685
686        if !mesh.metadata.validation_messages.is_empty() {
687            warnings.extend(mesh.metadata.validation_messages.iter().cloned());
688        }
689
690        Ok(warnings)
691    }
692}
693
694#[cfg(test)]
695mod tests {
696    use super::*;
697
698    fn create_test_triangle() -> TriangleMesh {
699        let vertices = vec![
700            Point3f::new(0.0, 0.0, 0.0),
701            Point3f::new(1.0, 0.0, 0.0),
702            Point3f::new(0.5, 1.0, 0.0),
703        ];
704        let faces = vec![[0, 1, 2]];
705        TriangleMesh::from_vertices_and_faces(vertices, faces)
706    }
707
708    #[test]
709    fn test_extended_mesh_creation() {
710        let base_mesh = create_test_triangle();
711        let extended = ExtendedTriangleMesh::from_mesh(base_mesh);
712
713        assert_eq!(extended.vertex_count(), 3);
714        assert_eq!(extended.face_count(), 1);
715        assert!(extended.uvs.is_none());
716        assert!(extended.tangents.is_none());
717    }
718
719    #[test]
720    fn test_normal_computation() {
721        let base_mesh = create_test_triangle();
722        let mut extended = ExtendedTriangleMesh::from_mesh(base_mesh);
723
724        extended.compute_normals(true, true).unwrap();
725
726        assert!(extended.mesh.normals.is_some());
727        let normals = extended.mesh.normals.unwrap();
728        assert_eq!(normals.len(), 3);
729
730        // All normals should point in +Z direction for this triangle
731        for normal in &normals {
732            assert!((normal.z - 1.0).abs() < 1e-5);
733        }
734    }
735
736    #[test]
737    fn test_uv_generation() {
738        let base_mesh = create_test_triangle();
739        let mut extended = ExtendedTriangleMesh::from_mesh(base_mesh);
740
741        extended.generate_default_uvs().unwrap();
742
743        assert!(extended.uvs.is_some());
744        let uvs = extended.uvs.unwrap();
745        assert_eq!(uvs.len(), 3);
746
747        // UVs should be in [0, 1] range
748        for uv in &uvs {
749            assert!(uv[0] >= 0.0 && uv[0] <= 1.0);
750            assert!(uv[1] >= 0.0 && uv[1] <= 1.0);
751        }
752    }
753
754    #[test]
755    fn test_tangent_computation() {
756        let base_mesh = create_test_triangle();
757        let mut extended = ExtendedTriangleMesh::from_mesh(base_mesh);
758
759        // Need normals and UVs for tangent computation
760        extended.compute_normals(true, true).unwrap();
761        extended.generate_default_uvs().unwrap();
762        extended.compute_tangents(true).unwrap();
763
764        assert!(extended.tangents.is_some());
765        let tangents = extended.tangents.unwrap();
766        assert_eq!(tangents.len(), 3);
767
768        // Check tangent properties
769        for tangent in &tangents {
770            let length_sq = tangent.vector.x * tangent.vector.x
771                + tangent.vector.y * tangent.vector.y
772                + tangent.vector.z * tangent.vector.z;
773            assert!((length_sq - 1.0).abs() < 1e-5); // Should be normalized
774            assert!(tangent.handedness.abs() == 1.0); // Should be ±1
775        }
776    }
777
778    #[test]
779    fn test_attribute_validation() {
780        let base_mesh = create_test_triangle();
781        let mut extended = ExtendedTriangleMesh::from_mesh(base_mesh);
782
783        // Should pass validation initially
784        assert!(extended.validate_attributes().is_ok());
785
786        // Add mismatched UVs
787        extended.uvs = Some(vec![[0.0, 0.0]]); // Wrong count
788        assert!(extended.validate_attributes().is_err());
789    }
790
791    #[test]
792    fn test_process_attributes() {
793        let base_mesh = create_test_triangle();
794        let mut extended = ExtendedTriangleMesh::from_mesh(base_mesh);
795
796        let options = MeshAttributeOptions::recompute_all();
797        extended.process_attributes(&options).unwrap();
798
799        assert!(extended.mesh.normals.is_some());
800        assert!(extended.uvs.is_some());
801        assert!(extended.tangents.is_some());
802        assert!(extended.metadata.normals_computed);
803        assert!(extended.metadata.tangents_computed);
804    }
805
806    #[test]
807    fn test_metadata_completeness() {
808        let base_mesh = create_test_triangle();
809        let mut extended = ExtendedTriangleMesh::from_mesh(base_mesh);
810
811        // Initially incomplete
812        assert!(!extended.metadata.is_complete());
813
814        // Add all attributes
815        let options = MeshAttributeOptions::recompute_all();
816        extended.process_attributes(&options).unwrap();
817
818        // Should be complete now
819        assert!(extended.metadata.is_complete());
820        assert!(extended.metadata.completeness_score > 0.75);
821    }
822}