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