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