Skip to main content

asset_importer/
material.rs

1//! Material representation and properties
2
3#![allow(clippy::unnecessary_cast)]
4
5use crate::{
6    error::{Error, Result},
7    ffi,
8    ptr::SharedPtr,
9    scene::Scene,
10    sys,
11    types::{
12        Color3D, Color4D, Vector2D, Vector3D, Vector4D, ai_string_to_str, ai_string_to_string,
13    },
14};
15use std::borrow::Cow;
16use std::ffi::CStr;
17use std::ffi::CString;
18
19/// Standard material property keys as defined by Assimp
20pub mod material_keys {
21    use std::ffi::CStr;
22
23    macro_rules! cstr {
24        ($lit:literal) => {
25            unsafe { CStr::from_bytes_with_nul_unchecked(concat!($lit, "\0").as_bytes()) }
26        };
27    }
28
29    /// Material name
30    pub const NAME: &CStr = cstr!("?mat.name");
31    /// Diffuse color
32    pub const COLOR_DIFFUSE: &CStr = cstr!("$clr.diffuse");
33    /// Ambient color
34    pub const COLOR_AMBIENT: &CStr = cstr!("$clr.ambient");
35    /// Specular color
36    pub const COLOR_SPECULAR: &CStr = cstr!("$clr.specular");
37    /// Emissive color
38    pub const COLOR_EMISSIVE: &CStr = cstr!("$clr.emissive");
39    /// Transparent color
40    pub const COLOR_TRANSPARENT: &CStr = cstr!("$clr.transparent");
41    /// Reflective color
42    pub const COLOR_REFLECTIVE: &CStr = cstr!("$clr.reflective");
43    /// Shininess factor
44    pub const SHININESS: &CStr = cstr!("$mat.shininess");
45    /// Shininess strength
46    pub const SHININESS_STRENGTH: &CStr = cstr!("$mat.shinpercent");
47    /// Opacity
48    pub const OPACITY: &CStr = cstr!("$mat.opacity");
49    /// Transparency factor
50    pub const TRANSPARENCYFACTOR: &CStr = cstr!("$mat.transparencyfactor");
51    /// Bump scaling
52    pub const BUMPSCALING: &CStr = cstr!("$mat.bumpscaling");
53    /// Refraction index
54    pub const REFRACTI: &CStr = cstr!("$mat.refracti");
55    /// Reflectivity
56    pub const REFLECTIVITY: &CStr = cstr!("$mat.reflectivity");
57    /// Shading model
58    pub const SHADING_MODEL: &CStr = cstr!("$mat.shadingm");
59    /// Blend function
60    pub const BLEND_FUNC: &CStr = cstr!("$mat.blend");
61    /// Two sided
62    pub const TWOSIDED: &CStr = cstr!("$mat.twosided");
63
64    // PBR-related keys (from material.h)
65    /// Base color factor (RGBA)
66    pub const BASE_COLOR: &CStr = cstr!("$clr.base");
67    /// Metallic factor
68    pub const METALLIC_FACTOR: &CStr = cstr!("$mat.metallicFactor");
69    /// Roughness factor
70    pub const ROUGHNESS_FACTOR: &CStr = cstr!("$mat.roughnessFactor");
71    /// Specular factor
72    pub const SPECULAR_FACTOR: &CStr = cstr!("$mat.specularFactor");
73    /// Glossiness factor (spec/gloss workflow)
74    pub const GLOSSINESS_FACTOR: &CStr = cstr!("$mat.glossinessFactor");
75    /// Sheen color factor
76    pub const SHEEN_COLOR_FACTOR: &CStr = cstr!("$clr.sheen.factor");
77    /// Sheen roughness factor
78    pub const SHEEN_ROUGHNESS_FACTOR: &CStr = cstr!("$mat.sheen.roughnessFactor");
79    /// Clearcoat factor
80    pub const CLEARCOAT_FACTOR: &CStr = cstr!("$mat.clearcoat.factor");
81    /// Clearcoat roughness factor
82    pub const CLEARCOAT_ROUGHNESS_FACTOR: &CStr = cstr!("$mat.clearcoat.roughnessFactor");
83    /// Transmission factor
84    pub const TRANSMISSION_FACTOR: &CStr = cstr!("$mat.transmission.factor");
85    /// Volume thickness factor
86    pub const VOLUME_THICKNESS_FACTOR: &CStr = cstr!("$mat.volume.thicknessFactor");
87    /// Volume attenuation distance
88    pub const VOLUME_ATTENUATION_DISTANCE: &CStr = cstr!("$mat.volume.attenuationDistance");
89    /// Volume attenuation color
90    pub const VOLUME_ATTENUATION_COLOR: &CStr = cstr!("$mat.volume.attenuationColor");
91    /// Emissive intensity
92    pub const EMISSIVE_INTENSITY: &CStr = cstr!("$mat.emissiveIntensity");
93    /// Anisotropy factor
94    pub const ANISOTROPY_FACTOR: &CStr = cstr!("$mat.anisotropyFactor");
95    /// Anisotropy rotation
96    pub const ANISOTROPY_ROTATION: &CStr = cstr!("$mat.anisotropyRotation");
97}
98
99/// A material containing properties like colors, textures, and shading parameters
100#[derive(Clone)]
101pub struct Material {
102    scene: Scene,
103    material_ptr: SharedPtr<sys::aiMaterial>,
104}
105
106/// A borrowed-ish string result backed by an owned `aiString` (no heap allocation).
107#[derive(Debug, Clone)]
108pub struct MaterialStringRef {
109    value: sys::aiString,
110}
111
112impl MaterialStringRef {
113    /// Access as UTF-8 (lossy) without allocation.
114    pub fn as_str(&self) -> Cow<'_, str> {
115        ai_string_to_str(&self.value)
116    }
117
118    /// Raw bytes (without assuming NUL-termination).
119    pub fn as_bytes(&self) -> &[u8] {
120        let len = (self.value.length as usize).min(self.value.data.len());
121        ffi::slice_from_ptr_len(self, self.value.data.as_ptr() as *const u8, len)
122    }
123
124    /// Borrow the underlying Assimp `aiString`.
125    #[cfg(feature = "raw-sys")]
126    pub fn as_raw(&self) -> &sys::aiString {
127        &self.value
128    }
129
130    /// Convert to an owned `String` (allocates).
131    pub fn to_string_lossy(&self) -> String {
132        ai_string_to_string(&self.value)
133    }
134}
135
136impl std::fmt::Display for MaterialStringRef {
137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138        f.write_str(&ai_string_to_string(&self.value))
139    }
140}
141
142impl Material {
143    pub(crate) fn from_sys_ptr(scene: Scene, material_ptr: *mut sys::aiMaterial) -> Option<Self> {
144        let material_ptr = SharedPtr::new(material_ptr as *const sys::aiMaterial)?;
145        Some(Self {
146            scene,
147            material_ptr,
148        })
149    }
150
151    #[allow(dead_code)]
152    pub(crate) fn as_raw_sys(&self) -> *const sys::aiMaterial {
153        self.material_ptr.as_ptr()
154    }
155
156    /// Get the raw material pointer (requires `raw-sys`).
157    #[cfg(feature = "raw-sys")]
158    pub fn as_raw(&self) -> *const sys::aiMaterial {
159        self.as_raw_sys()
160    }
161
162    #[inline]
163    fn raw(&self) -> &sys::aiMaterial {
164        self.material_ptr.as_ref()
165    }
166
167    /// Get the name of the material
168    pub fn name(&self) -> String {
169        self.name_ref().map(|s| s.to_string()).unwrap_or_default()
170    }
171
172    /// Get the material name (no heap allocation).
173    pub fn name_ref(&self) -> Option<MaterialStringRef> {
174        self.get_string_property_ref(material_keys::NAME)
175    }
176
177    /// Get a string property from the material (no heap allocation).
178    pub fn get_string_property_ref(&self, key: &CStr) -> Option<MaterialStringRef> {
179        let mut ai_string = sys::aiString::default();
180
181        let result = unsafe {
182            sys::aiGetMaterialString(
183                self.as_raw_sys(),
184                key.as_ptr(),
185                0, // type
186                0, // index
187                &mut ai_string,
188            )
189        };
190
191        if result == sys::aiReturn::aiReturn_SUCCESS {
192            Some(MaterialStringRef { value: ai_string })
193        } else {
194            None
195        }
196    }
197
198    /// Get a string property from the material (allocates).
199    pub fn get_string_property(&self, key: &CStr) -> Option<String> {
200        self.get_string_property_ref(key).map(|s| s.to_string())
201    }
202
203    /// Get a string property from the material (allocates, convenience).
204    pub fn get_string_property_str(&self, key: &str) -> Result<Option<String>> {
205        let c_key = CString::new(key)
206            .map_err(|_| Error::invalid_parameter("material key contains NUL byte".to_string()))?;
207        Ok(self.get_string_property(c_key.as_c_str()))
208    }
209
210    /// Get a float property from the material
211    pub fn get_float_property(&self, key: &CStr) -> Option<f32> {
212        let mut value = 0.0f32;
213        let mut max = 1u32;
214
215        let result = unsafe {
216            sys::aiGetMaterialFloatArray(
217                self.as_raw_sys(),
218                key.as_ptr(),
219                0, // type
220                0, // index
221                &mut value,
222                &mut max,
223            )
224        };
225
226        if result == sys::aiReturn::aiReturn_SUCCESS && max > 0 {
227            Some(value)
228        } else {
229            None
230        }
231    }
232
233    /// Get a float property from the material (allocates, convenience).
234    pub fn get_float_property_str(&self, key: &str) -> Result<Option<f32>> {
235        let c_key = CString::new(key)
236            .map_err(|_| Error::invalid_parameter("material key contains NUL byte".to_string()))?;
237        Ok(self.get_float_property(c_key.as_c_str()))
238    }
239
240    /// Get an integer property from the material
241    pub fn get_integer_property(&self, key: &CStr) -> Option<i32> {
242        let mut value = 0i32;
243        let mut max = 1u32;
244
245        let result = unsafe {
246            sys::aiGetMaterialIntegerArray(
247                self.as_raw_sys(),
248                key.as_ptr(),
249                0, // type
250                0, // index
251                &mut value,
252                &mut max,
253            )
254        };
255
256        if result == sys::aiReturn::aiReturn_SUCCESS && max > 0 {
257            Some(value)
258        } else {
259            None
260        }
261    }
262
263    /// Get an integer property from the material (allocates, convenience).
264    pub fn get_integer_property_str(&self, key: &str) -> Result<Option<i32>> {
265        let c_key = CString::new(key)
266            .map_err(|_| Error::invalid_parameter("material key contains NUL byte".to_string()))?;
267        Ok(self.get_integer_property(c_key.as_c_str()))
268    }
269
270    /// Get a color property from the material
271    pub fn get_color_property(&self, key: &CStr) -> Option<Color4D> {
272        let mut color = sys::aiColor4D {
273            r: 0.0,
274            g: 0.0,
275            b: 0.0,
276            a: 1.0,
277        };
278
279        let result = unsafe {
280            sys::aiGetMaterialColor(
281                self.as_raw_sys(),
282                key.as_ptr(),
283                0, // type
284                0, // index
285                &mut color,
286            )
287        };
288
289        if result == sys::aiReturn::aiReturn_SUCCESS {
290            Some(Color4D::new(color.r, color.g, color.b, color.a))
291        } else {
292            None
293        }
294    }
295
296    /// Get a color property from the material (allocates, convenience).
297    pub fn get_color_property_str(&self, key: &str) -> Result<Option<Color4D>> {
298        let c_key = CString::new(key)
299            .map_err(|_| Error::invalid_parameter("material key contains NUL byte".to_string()))?;
300        Ok(self.get_color_property(c_key.as_c_str()))
301    }
302
303    /// Get the diffuse color
304    pub fn diffuse_color(&self) -> Option<Color3D> {
305        self.get_color_property(material_keys::COLOR_DIFFUSE)
306            .map(|c| Color3D::new(c.x, c.y, c.z))
307    }
308
309    /// Get the specular color
310    pub fn specular_color(&self) -> Option<Color3D> {
311        self.get_color_property(material_keys::COLOR_SPECULAR)
312            .map(|c| Color3D::new(c.x, c.y, c.z))
313    }
314
315    /// Get the ambient color
316    pub fn ambient_color(&self) -> Option<Color3D> {
317        self.get_color_property(material_keys::COLOR_AMBIENT)
318            .map(|c| Color3D::new(c.x, c.y, c.z))
319    }
320
321    /// Get the emissive color
322    pub fn emissive_color(&self) -> Option<Color3D> {
323        self.get_color_property(material_keys::COLOR_EMISSIVE)
324            .map(|c| Color3D::new(c.x, c.y, c.z))
325    }
326
327    /// Get the transparent color
328    pub fn transparent_color(&self) -> Option<Color3D> {
329        self.get_color_property(material_keys::COLOR_TRANSPARENT)
330            .map(|c| Color3D::new(c.x, c.y, c.z))
331    }
332
333    /// Get the reflective color
334    pub fn reflective_color(&self) -> Option<Color3D> {
335        self.get_color_property(material_keys::COLOR_REFLECTIVE)
336            .map(|c| Color3D::new(c.x, c.y, c.z))
337    }
338
339    /// Get the shininess factor
340    pub fn shininess(&self) -> Option<f32> {
341        self.get_float_property(material_keys::SHININESS)
342    }
343
344    /// Get the shininess strength
345    pub fn shininess_strength(&self) -> Option<f32> {
346        self.get_float_property(material_keys::SHININESS_STRENGTH)
347    }
348
349    /// Base color factor (RGBA)
350    pub fn base_color(&self) -> Option<Color4D> {
351        self.get_color_property(material_keys::BASE_COLOR)
352    }
353
354    /// Metallic factor
355    pub fn metallic_factor(&self) -> Option<f32> {
356        self.get_float_property(material_keys::METALLIC_FACTOR)
357    }
358
359    /// Roughness factor
360    pub fn roughness_factor(&self) -> Option<f32> {
361        self.get_float_property(material_keys::ROUGHNESS_FACTOR)
362    }
363
364    /// Glossiness factor (spec/gloss workflow)
365    pub fn glossiness_factor(&self) -> Option<f32> {
366        self.get_float_property(material_keys::GLOSSINESS_FACTOR)
367    }
368
369    /// Specular factor
370    pub fn specular_factor(&self) -> Option<f32> {
371        self.get_float_property(material_keys::SPECULAR_FACTOR)
372    }
373
374    /// Sheen color factor
375    pub fn sheen_color_factor(&self) -> Option<Color4D> {
376        self.get_color_property(material_keys::SHEEN_COLOR_FACTOR)
377    }
378
379    /// Sheen roughness factor
380    pub fn sheen_roughness_factor(&self) -> Option<f32> {
381        self.get_float_property(material_keys::SHEEN_ROUGHNESS_FACTOR)
382    }
383
384    /// Clearcoat factor
385    pub fn clearcoat_factor(&self) -> Option<f32> {
386        self.get_float_property(material_keys::CLEARCOAT_FACTOR)
387    }
388
389    /// Clearcoat roughness factor
390    pub fn clearcoat_roughness_factor(&self) -> Option<f32> {
391        self.get_float_property(material_keys::CLEARCOAT_ROUGHNESS_FACTOR)
392    }
393
394    /// Transmission factor
395    pub fn transmission_factor(&self) -> Option<f32> {
396        self.get_float_property(material_keys::TRANSMISSION_FACTOR)
397    }
398
399    /// Volume thickness factor
400    pub fn volume_thickness_factor(&self) -> Option<f32> {
401        self.get_float_property(material_keys::VOLUME_THICKNESS_FACTOR)
402    }
403
404    /// Volume attenuation distance
405    pub fn volume_attenuation_distance(&self) -> Option<f32> {
406        self.get_float_property(material_keys::VOLUME_ATTENUATION_DISTANCE)
407    }
408
409    /// Volume attenuation color
410    pub fn volume_attenuation_color(&self) -> Option<Color3D> {
411        self.get_color_property(material_keys::VOLUME_ATTENUATION_COLOR)
412            .map(|c| Color3D::new(c.x, c.y, c.z))
413    }
414
415    /// Emissive intensity
416    pub fn emissive_intensity(&self) -> Option<f32> {
417        self.get_float_property(material_keys::EMISSIVE_INTENSITY)
418    }
419
420    /// Anisotropy factor
421    pub fn anisotropy_factor(&self) -> Option<f32> {
422        self.get_float_property(material_keys::ANISOTROPY_FACTOR)
423    }
424
425    /// Anisotropy rotation
426    pub fn anisotropy_rotation(&self) -> Option<f32> {
427        self.get_float_property(material_keys::ANISOTROPY_ROTATION)
428    }
429
430    /// Get the opacity factor
431    pub fn opacity(&self) -> Option<f32> {
432        self.get_float_property(material_keys::OPACITY)
433    }
434
435    /// Get the transparency factor
436    pub fn transparency_factor(&self) -> Option<f32> {
437        self.get_float_property(material_keys::TRANSPARENCYFACTOR)
438    }
439
440    /// Get the bump scaling factor
441    pub fn bump_scaling(&self) -> Option<f32> {
442        self.get_float_property(material_keys::BUMPSCALING)
443    }
444
445    /// Get the refraction index
446    pub fn refraction_index(&self) -> Option<f32> {
447        self.get_float_property(material_keys::REFRACTI)
448    }
449
450    /// Get the reflectivity factor
451    pub fn reflectivity(&self) -> Option<f32> {
452        self.get_float_property(material_keys::REFLECTIVITY)
453    }
454
455    /// Get the shading model
456    pub fn shading_model(&self) -> Option<i32> {
457        self.get_integer_property(material_keys::SHADING_MODEL)
458    }
459
460    /// Get the shading model as an enum
461    pub fn shading_model_enum(&self) -> Option<ShadingModel> {
462        self.shading_model()
463            .map(|v| ShadingModel::from_raw(v as u32))
464    }
465
466    /// Get raw information about a material property by key/semantic/index
467    ///
468    /// - `key`: material key string (e.g. "$mat.shininess")
469    /// - `semantic`: texture type semantic if the property is texture-dependent; use `None` for non-texture properties
470    /// - `index`: texture index for texture-dependent properties; 0 otherwise
471    pub fn property_info(
472        &self,
473        key: &CStr,
474        semantic: Option<TextureType>,
475        index: u32,
476    ) -> Option<MaterialPropertyInfo> {
477        let prop_ptr = self.property_ptr(key, semantic, index)?;
478        Some(MaterialPropertyRef::from_ptr(self.scene.clone(), prop_ptr).into_info())
479    }
480
481    /// Get raw information about a material property by key/semantic/index (allocates, convenience).
482    pub fn property_info_str(
483        &self,
484        key: &str,
485        semantic: Option<TextureType>,
486        index: u32,
487    ) -> Option<MaterialPropertyInfo> {
488        let c_key = CString::new(key).ok()?;
489        self.property_info(c_key.as_c_str(), semantic, index)
490    }
491
492    /// Get only the property type information (aiPropertyTypeInfo) for a given key/semantic/index
493    pub fn property_type(
494        &self,
495        key: &CStr,
496        semantic: Option<TextureType>,
497        index: u32,
498    ) -> Option<PropertyTypeInfo> {
499        self.property_info(key, semantic, index)
500            .map(|p| p.type_info)
501    }
502
503    /// Get only the property type information (aiPropertyTypeInfo) for a given key/semantic/index (allocates, convenience).
504    pub fn property_type_str(
505        &self,
506        key: &str,
507        semantic: Option<TextureType>,
508        index: u32,
509    ) -> Option<PropertyTypeInfo> {
510        let c_key = CString::new(key).ok()?;
511        self.property_type(c_key.as_c_str(), semantic, index)
512    }
513
514    fn property_ptr(
515        &self,
516        key: &CStr,
517        semantic: Option<TextureType>,
518        index: u32,
519    ) -> Option<*const sys::aiMaterialProperty> {
520        let mut prop_ptr: *const sys::aiMaterialProperty = std::ptr::null();
521        let ok = unsafe {
522            sys::aiGetMaterialProperty(
523                self.as_raw_sys(),
524                key.as_ptr(),
525                semantic.map(|t| t.to_sys() as u32).unwrap_or(0),
526                index,
527                &mut prop_ptr,
528            ) == sys::aiReturn::aiReturn_SUCCESS
529        };
530        (ok && !prop_ptr.is_null()).then_some(prop_ptr)
531    }
532
533    /// Get the raw bytes of a property (as stored by Assimp)
534    pub fn get_property_raw_ref(
535        &self,
536        key: &CStr,
537        semantic: Option<TextureType>,
538        index: u32,
539    ) -> Option<&[u8]> {
540        let prop_ptr = self.property_ptr(key, semantic, index)?;
541        let prop = ffi::ref_from_ptr(self, prop_ptr)?;
542        Some(ffi::slice_from_ptr_len(
543            self,
544            prop.mData as *const u8,
545            prop.mDataLength as usize,
546        ))
547    }
548
549    /// Get the raw bytes of a property (as stored by Assimp, allocates).
550    pub fn get_property_raw(
551        &self,
552        key: &CStr,
553        semantic: Option<TextureType>,
554        index: u32,
555    ) -> Option<Vec<u8>> {
556        self.get_property_raw_ref(key, semantic, index)
557            .map(|raw| raw.to_vec())
558    }
559
560    /// Get the raw bytes of a property (as stored by Assimp, allocates, convenience).
561    pub fn get_property_raw_str(
562        &self,
563        key: &str,
564        semantic: Option<TextureType>,
565        index: u32,
566    ) -> Option<Vec<u8>> {
567        let c_key = CString::new(key).ok()?;
568        self.get_property_raw(c_key.as_c_str(), semantic, index)
569    }
570
571    /// Get an integer array property (converts from floats if necessary)
572    pub fn get_property_i32_array(
573        &self,
574        key: &CStr,
575        semantic: Option<TextureType>,
576        index: u32,
577    ) -> Option<Vec<i32>> {
578        let info = self.property_info(key, semantic, index)?;
579        // Determine element count using the stored type size
580        let elem_size = match info.type_info {
581            PropertyTypeInfo::Integer => std::mem::size_of::<i32>(),
582            PropertyTypeInfo::Float => std::mem::size_of::<f32>(),
583            PropertyTypeInfo::Double => std::mem::size_of::<f64>(),
584            _ => return None,
585        };
586        if elem_size == 0 {
587            return None;
588        }
589        let count = (info.data_length as usize) / elem_size;
590        let mut out = vec![0i32; count];
591        let mut max = count as u32;
592        let result = unsafe {
593            sys::aiGetMaterialIntegerArray(
594                self.as_raw_sys(),
595                key.as_ptr(),
596                semantic.map(|t| t.to_sys() as u32).unwrap_or(0),
597                index,
598                out.as_mut_ptr(),
599                &mut max,
600            )
601        };
602        if result == sys::aiReturn::aiReturn_SUCCESS {
603            out.truncate(max as usize);
604            Some(out)
605        } else {
606            None
607        }
608    }
609
610    /// Get an integer array property (converts from floats if necessary, allocates, convenience).
611    pub fn get_property_i32_array_str(
612        &self,
613        key: &str,
614        semantic: Option<TextureType>,
615        index: u32,
616    ) -> Option<Vec<i32>> {
617        let c_key = CString::new(key).ok()?;
618        self.get_property_i32_array(c_key.as_c_str(), semantic, index)
619    }
620
621    /// Get a 32-bit float array property. If the property is stored as doubles, it is converted.
622    pub fn get_property_f32_array(
623        &self,
624        key: &CStr,
625        semantic: Option<TextureType>,
626        index: u32,
627    ) -> Option<Vec<f32>> {
628        let info = self.property_info(key, semantic, index)?;
629        match info.type_info {
630            PropertyTypeInfo::Float | PropertyTypeInfo::Double | PropertyTypeInfo::Integer => {
631                // Use Assimp conversion for Float/Integer; for Double, we can also do a manual conversion for more precision.
632                // First, try the API path using aiGetMaterialFloatArray.
633                let elem_size = match info.type_info {
634                    PropertyTypeInfo::Float => std::mem::size_of::<f32>(),
635                    PropertyTypeInfo::Double => std::mem::size_of::<f64>(),
636                    PropertyTypeInfo::Integer => std::mem::size_of::<i32>(),
637                    _ => return None,
638                };
639                let count = (info.data_length as usize) / elem_size;
640                let mut out = vec![0f32; count];
641                let mut max = count as u32;
642                let result = unsafe {
643                    sys::aiGetMaterialFloatArray(
644                        self.as_raw_sys(),
645                        key.as_ptr(),
646                        semantic.map(|t| t.to_sys() as u32).unwrap_or(0),
647                        index,
648                        out.as_mut_ptr(),
649                        &mut max,
650                    )
651                };
652                if result == sys::aiReturn::aiReturn_SUCCESS {
653                    out.truncate(max as usize);
654                    return Some(out);
655                }
656                // Fallback: manual conversion from raw data
657                self.get_property_f64_array(key, semantic, index)
658                    .map(|v| v.into_iter().map(|x| x as f32).collect())
659            }
660            _ => None,
661        }
662    }
663
664    /// Get a 32-bit float array property (allocates, convenience).
665    pub fn get_property_f32_array_str(
666        &self,
667        key: &str,
668        semantic: Option<TextureType>,
669        index: u32,
670    ) -> Option<Vec<f32>> {
671        let c_key = CString::new(key).ok()?;
672        self.get_property_f32_array(c_key.as_c_str(), semantic, index)
673    }
674
675    /// Get a 64-bit float array property by decoding raw bytes.
676    /// If stored as f32, it will be widened; if stored as i32, it will be cast.
677    pub fn get_property_f64_array(
678        &self,
679        key: &CStr,
680        semantic: Option<TextureType>,
681        index: u32,
682    ) -> Option<Vec<f64>> {
683        let info = self.property_info(key, semantic, index)?;
684        let raw = self.get_property_raw_ref(key, semantic, index)?;
685        match info.type_info {
686            PropertyTypeInfo::Double => {
687                let sz = std::mem::size_of::<f64>();
688                if sz == 0 || raw.len() % sz != 0 {
689                    return None;
690                }
691                let mut out = Vec::with_capacity(raw.len() / sz);
692                for chunk in raw.chunks_exact(sz) {
693                    let mut arr = [0u8; 8];
694                    arr.copy_from_slice(chunk);
695                    out.push(f64::from_ne_bytes(arr));
696                }
697                Some(out)
698            }
699            PropertyTypeInfo::Float => {
700                let sz = std::mem::size_of::<f32>();
701                if sz == 0 || raw.len() % sz != 0 {
702                    return None;
703                }
704                let mut out = Vec::with_capacity(raw.len() / sz);
705                for chunk in raw.chunks_exact(sz) {
706                    let mut arr = [0u8; 4];
707                    arr.copy_from_slice(chunk);
708                    out.push(f32::from_ne_bytes(arr) as f64);
709                }
710                Some(out)
711            }
712            PropertyTypeInfo::Integer => {
713                let sz = std::mem::size_of::<i32>();
714                if sz == 0 || raw.len() % sz != 0 {
715                    return None;
716                }
717                let mut out = Vec::with_capacity(raw.len() / sz);
718                for chunk in raw.chunks_exact(sz) {
719                    let mut arr = [0u8; 4];
720                    arr.copy_from_slice(chunk);
721                    out.push(i32::from_ne_bytes(arr) as f64);
722                }
723                Some(out)
724            }
725            _ => None,
726        }
727    }
728
729    /// Get a 64-bit float array property (allocates, convenience).
730    pub fn get_property_f64_array_str(
731        &self,
732        key: &str,
733        semantic: Option<TextureType>,
734        index: u32,
735    ) -> Option<Vec<f64>> {
736        let c_key = CString::new(key).ok()?;
737        self.get_property_f64_array(c_key.as_c_str(), semantic, index)
738    }
739
740    /// Enumerate all properties stored in this material (raw info only)
741    pub fn all_properties(&self) -> Vec<MaterialPropertyInfo> {
742        self.all_properties_iter().collect()
743    }
744
745    /// Iterate properties as owned `MaterialPropertyInfo` (allocates per item, but avoids a `Vec`).
746    pub fn all_properties_iter(&self) -> impl Iterator<Item = MaterialPropertyInfo> + '_ {
747        self.properties().map(MaterialPropertyRef::into_info)
748    }
749
750    /// Iterate all material properties (zero allocation for keys and raw data).
751    pub fn properties(&self) -> MaterialPropertyIterator {
752        let m = self.raw();
753        MaterialPropertyIterator {
754            scene: self.scene.clone(),
755            props: SharedPtr::new(m.mProperties as *const *const sys::aiMaterialProperty),
756            count: m.mNumProperties as usize,
757            index: 0,
758        }
759    }
760
761    /// Check if the material is two-sided
762    pub fn is_two_sided(&self) -> bool {
763        self.get_integer_property(material_keys::TWOSIDED)
764            .map(|v| v != 0)
765            .unwrap_or(false)
766    }
767
768    /// Check if the material is unlit (NoShading/Unlit)
769    pub fn is_unlit(&self) -> bool {
770        matches!(self.shading_model_enum(), Some(ShadingModel::NoShading))
771    }
772
773    /// Get the blend mode for the material
774    pub fn blend_mode(&self) -> Option<BlendMode> {
775        self.get_integer_property(material_keys::BLEND_FUNC)
776            .map(|v| BlendMode::from_raw(v as u32))
777    }
778
779    /// Get the number of textures for a specific type
780    pub fn texture_count(&self, texture_type: TextureType) -> usize {
781        unsafe { sys::aiGetMaterialTextureCount(self.as_raw_sys(), texture_type.to_sys()) as usize }
782    }
783
784    /// Get texture information for a specific type and index (no heap allocation).
785    pub fn texture_ref(&self, texture_type: TextureType, index: usize) -> Option<TextureInfoRef> {
786        if index >= self.texture_count(texture_type) {
787            return None;
788        }
789
790        unsafe {
791            let mut path = sys::aiString::default();
792            let mut mapping = std::mem::MaybeUninit::<sys::aiTextureMapping>::uninit();
793            let mut uv_index = std::mem::MaybeUninit::<u32>::uninit();
794            let mut blend = std::mem::MaybeUninit::<f32>::uninit();
795            let mut op = std::mem::MaybeUninit::<sys::aiTextureOp>::uninit();
796            // Use the exact sys enum type to avoid platform-dependent
797            // signedness mismatches across compilers.
798            let mut map_mode: [sys::aiTextureMapMode; 3] =
799                [sys::aiTextureMapMode::aiTextureMapMode_Wrap; 3];
800            let mut tex_flags: u32 = 0;
801
802            let result = sys::aiGetMaterialTexture(
803                self.as_raw_sys(),
804                texture_type.to_sys(),
805                index as u32,
806                &mut path,
807                mapping.as_mut_ptr(),
808                uv_index.as_mut_ptr(),
809                blend.as_mut_ptr(),
810                op.as_mut_ptr(),
811                map_mode.as_mut_ptr() as *mut _,
812                &mut tex_flags as *mut u32,
813            );
814
815            if result != sys::aiReturn::aiReturn_SUCCESS {
816                return None;
817            }
818
819            let mapping_val = mapping.assume_init();
820            let uv_index_val = uv_index.assume_init();
821            let blend_val = blend.assume_init();
822            let op_val = op.assume_init();
823
824            // Try read UV transform
825            let mut uv_transform = std::mem::MaybeUninit::<sys::aiUVTransform>::uninit();
826            let uv_key: &CStr = c"$tex.uvtrafo";
827            let uv_ok = sys::aiGetMaterialUVTransform(
828                self.as_raw_sys(),
829                uv_key.as_ptr(),
830                texture_type.to_sys() as u32,
831                index as u32,
832                uv_transform.as_mut_ptr(),
833            ) == sys::aiReturn::aiReturn_SUCCESS;
834
835            let uv_transform = if uv_ok {
836                let t = uv_transform.assume_init();
837                Some(UVTransform {
838                    translation: Vector2D::new(t.mTranslation.x, t.mTranslation.y),
839                    scaling: Vector2D::new(t.mScaling.x, t.mScaling.y),
840                    rotation: t.mRotation,
841                })
842            } else {
843                None
844            };
845
846            // Try read TEXMAP_AXIS via property API ("$tex.mapaxis")
847            let axis = {
848                let key: &CStr = c"$tex.mapaxis";
849                let mut prop_ptr: *const sys::aiMaterialProperty = std::ptr::null();
850                let ok = sys::aiGetMaterialProperty(
851                    self.as_raw_sys(),
852                    key.as_ptr(),
853                    texture_type.to_sys() as u32,
854                    index as u32,
855                    &mut prop_ptr,
856                ) == sys::aiReturn::aiReturn_SUCCESS;
857                if ok {
858                    if let Some(prop) = ffi::ref_from_ptr(self, prop_ptr) {
859                        MaterialPropertyData::from_sys(prop)
860                            .and_then(|d| d.read_ne_f32_array::<3>(0))
861                            .map(|[x, y, z]| Vector3D::new(x, y, z))
862                    } else {
863                        None
864                    }
865                } else {
866                    None
867                }
868            };
869
870            Some(TextureInfoRef {
871                path,
872                mapping: TextureMapping::from_raw(mapping_val),
873                uv_index: uv_index_val,
874                blend_factor: blend_val,
875                operation: TextureOperation::from_raw(op_val),
876                map_modes: [
877                    TextureMapMode::from_raw(map_mode[0]),
878                    TextureMapMode::from_raw(map_mode[1]),
879                    TextureMapMode::from_raw(map_mode[2]),
880                ],
881                flags: TextureFlags::from_bits_truncate(tex_flags),
882                uv_transform,
883                axis,
884            })
885        }
886    }
887
888    /// Iterate textures of a given type (no heap allocation).
889    pub fn texture_refs(
890        &self,
891        texture_type: TextureType,
892    ) -> impl Iterator<Item = TextureInfoRef> + '_ {
893        let count = self.texture_count(texture_type);
894        (0..count).filter_map(move |i| self.texture_ref(texture_type, i))
895    }
896
897    /// Get texture information for a specific type and index
898    pub fn texture(&self, texture_type: TextureType, index: usize) -> Option<TextureInfo> {
899        self.texture_ref(texture_type, index)
900            .map(TextureInfoRef::into_owned)
901    }
902}
903
904/// Types of textures that can be applied to materials
905#[derive(Debug, Clone, Copy, PartialEq, Eq)]
906#[repr(u32)]
907pub enum TextureType {
908    /// Diffuse texture (base color)
909    Diffuse = sys::aiTextureType::aiTextureType_DIFFUSE as u32,
910    /// Specular texture (reflectivity)
911    Specular = sys::aiTextureType::aiTextureType_SPECULAR as u32,
912    /// Ambient texture (ambient lighting)
913    Ambient = sys::aiTextureType::aiTextureType_AMBIENT as u32,
914    /// Emissive texture (self-illumination)
915    Emissive = sys::aiTextureType::aiTextureType_EMISSIVE as u32,
916    /// Height texture (displacement mapping)
917    Height = sys::aiTextureType::aiTextureType_HEIGHT as u32,
918    /// Normal texture (surface detail)
919    Normals = sys::aiTextureType::aiTextureType_NORMALS as u32,
920    /// Shininess texture (specular power)
921    Shininess = sys::aiTextureType::aiTextureType_SHININESS as u32,
922    /// Opacity texture (transparency)
923    Opacity = sys::aiTextureType::aiTextureType_OPACITY as u32,
924    /// Displacement texture (geometry displacement)
925    Displacement = sys::aiTextureType::aiTextureType_DISPLACEMENT as u32,
926    /// Lightmap texture (pre-computed lighting)
927    Lightmap = sys::aiTextureType::aiTextureType_LIGHTMAP as u32,
928    /// Reflection texture (environment mapping)
929    Reflection = sys::aiTextureType::aiTextureType_REFLECTION as u32,
930    /// Base color texture (PBR albedo)
931    BaseColor = sys::aiTextureType::aiTextureType_BASE_COLOR as u32,
932    /// Normal camera texture (camera-space normals)
933    NormalCamera = sys::aiTextureType::aiTextureType_NORMAL_CAMERA as u32,
934    /// Emission color texture (PBR emission)
935    EmissionColor = sys::aiTextureType::aiTextureType_EMISSION_COLOR as u32,
936    /// Metalness texture (PBR metallic)
937    Metalness = sys::aiTextureType::aiTextureType_METALNESS as u32,
938    /// Diffuse roughness texture (PBR roughness)
939    DiffuseRoughness = sys::aiTextureType::aiTextureType_DIFFUSE_ROUGHNESS as u32,
940    /// Ambient occlusion texture (shadowing)
941    AmbientOcclusion = sys::aiTextureType::aiTextureType_AMBIENT_OCCLUSION as u32,
942    /// Unknown texture type
943    Unknown = sys::aiTextureType::aiTextureType_UNKNOWN as u32,
944    /// Sheen layer (PBR)
945    Sheen = sys::aiTextureType::aiTextureType_SHEEN as u32,
946    /// Clearcoat layer (PBR)
947    Clearcoat = sys::aiTextureType::aiTextureType_CLEARCOAT as u32,
948    /// Transmission layer (PBR)
949    Transmission = sys::aiTextureType::aiTextureType_TRANSMISSION as u32,
950    /// Maya base color (compat)
951    MayaBase = sys::aiTextureType::aiTextureType_MAYA_BASE as u32,
952    /// Maya specular (compat)
953    MayaSpecular = sys::aiTextureType::aiTextureType_MAYA_SPECULAR as u32,
954    /// Maya specular color (compat)
955    MayaSpecularColor = sys::aiTextureType::aiTextureType_MAYA_SPECULAR_COLOR as u32,
956    /// Maya specular roughness (compat)
957    MayaSpecularRoughness = sys::aiTextureType::aiTextureType_MAYA_SPECULAR_ROUGHNESS as u32,
958    /// Anisotropy (PBR)
959    Anisotropy = sys::aiTextureType::aiTextureType_ANISOTROPY as u32,
960    /// glTF metallic-roughness packed
961    GltfMetallicRoughness = sys::aiTextureType::aiTextureType_GLTF_METALLIC_ROUGHNESS as u32,
962}
963
964/// High-level shading model
965#[derive(Debug, Clone, Copy, PartialEq, Eq)]
966pub enum ShadingModel {
967    /// Flat shading - no interpolation between vertices
968    Flat,
969    /// Gouraud shading - linear interpolation of vertex colors
970    Gouraud,
971    /// Phong shading - per-pixel lighting calculation
972    Phong,
973    /// Blinn-Phong shading - modified Phong with Blinn's halfway vector
974    Blinn,
975    /// Toon/cel shading - cartoon-like rendering
976    Toon,
977    /// Oren-Nayar reflectance model for rough surfaces
978    OrenNayar,
979    /// Minnaert reflectance model
980    Minnaert,
981    /// Cook-Torrance reflectance model for metals
982    CookTorrance,
983    /// No shading - unlit material
984    NoShading,
985    /// Fresnel reflectance model
986    Fresnel,
987    /// PBR specular-glossiness workflow
988    PbrSpecularGlossiness,
989    /// PBR metallic-roughness workflow
990    PbrMetallicRoughness,
991    /// Unknown shading model with raw value
992    Unknown(u32),
993}
994
995impl ShadingModel {
996    fn from_raw(v: u32) -> Self {
997        use sys::aiShadingMode;
998        match v {
999            x if x == aiShadingMode::aiShadingMode_Flat as u32 => ShadingModel::Flat,
1000            x if x == aiShadingMode::aiShadingMode_Gouraud as u32 => ShadingModel::Gouraud,
1001            x if x == aiShadingMode::aiShadingMode_Phong as u32 => ShadingModel::Phong,
1002            x if x == aiShadingMode::aiShadingMode_Blinn as u32 => ShadingModel::Blinn,
1003            x if x == aiShadingMode::aiShadingMode_Toon as u32 => ShadingModel::Toon,
1004            x if x == aiShadingMode::aiShadingMode_OrenNayar as u32 => ShadingModel::OrenNayar,
1005            x if x == aiShadingMode::aiShadingMode_Minnaert as u32 => ShadingModel::Minnaert,
1006            x if x == aiShadingMode::aiShadingMode_CookTorrance as u32 => {
1007                ShadingModel::CookTorrance
1008            }
1009            x if x == aiShadingMode::aiShadingMode_NoShading as u32 => ShadingModel::NoShading,
1010            x if x == aiShadingMode::aiShadingMode_Fresnel as u32 => ShadingModel::Fresnel,
1011            x if x == aiShadingMode::aiShadingMode_PBR_BRDF as u32 => {
1012                ShadingModel::PbrSpecularGlossiness
1013            }
1014            other => ShadingModel::Unknown(other),
1015        }
1016    }
1017}
1018
1019/// High-level classification of material property data types
1020#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1021pub enum PropertyTypeInfo {
1022    /// Single-precision floating point value
1023    Float,
1024    /// Double-precision floating point value
1025    Double,
1026    /// String value
1027    String,
1028    /// Integer value
1029    Integer,
1030    /// Binary buffer/blob data
1031    Buffer,
1032    /// Unknown property type with raw value
1033    Unknown(u32),
1034}
1035
1036impl PropertyTypeInfo {
1037    fn from_sys(t: sys::aiPropertyTypeInfo) -> Self {
1038        match t {
1039            sys::aiPropertyTypeInfo::aiPTI_Float => Self::Float,
1040            sys::aiPropertyTypeInfo::aiPTI_Double => Self::Double,
1041            sys::aiPropertyTypeInfo::aiPTI_String => Self::String,
1042            sys::aiPropertyTypeInfo::aiPTI_Integer => Self::Integer,
1043            sys::aiPropertyTypeInfo::aiPTI_Buffer => Self::Buffer,
1044            other => Self::Unknown(other as u32),
1045        }
1046    }
1047}
1048
1049/// Raw information about a material property
1050#[derive(Debug, Clone)]
1051pub struct MaterialPropertyInfo {
1052    /// Property key
1053    pub key: String,
1054    /// Semantic (texture type) if texture-related
1055    pub semantic: Option<TextureType>,
1056    /// Texture index (0 for non-texture)
1057    pub index: u32,
1058    /// Data length in bytes
1059    pub data_length: u32,
1060    /// Property type info
1061    pub type_info: PropertyTypeInfo,
1062}
1063
1064impl MaterialPropertyInfo {
1065    fn from_ref(p: MaterialPropertyRef) -> Self {
1066        let semantic = p.semantic();
1067        Self {
1068            key: p.key_string(),
1069            semantic,
1070            index: p.index(),
1071            data_length: p.data().len() as u32,
1072            type_info: p.type_info(),
1073        }
1074    }
1075}
1076
1077/// Zero-copy view of an Assimp material property.
1078#[derive(Debug, Clone)]
1079pub struct MaterialPropertyRef {
1080    #[allow(dead_code)]
1081    scene: Scene,
1082    prop_ptr: SharedPtr<sys::aiMaterialProperty>,
1083}
1084
1085/// Decoder for `aiMaterialProperty::mData`.
1086///
1087/// Assimp treats `mData` as a byte blob; callers must not assume alignment.
1088struct MaterialPropertyData<'a> {
1089    bytes: &'a [u8],
1090}
1091
1092impl<'a> MaterialPropertyData<'a> {
1093    /// # Safety
1094    /// The returned slice borrows Assimp-owned memory and must not outlive the scene.
1095    unsafe fn from_sys(prop: &'a sys::aiMaterialProperty) -> Option<Self> {
1096        let bytes =
1097            ffi::slice_from_ptr_len_opt(prop, prop.mData as *const u8, prop.mDataLength as usize)?;
1098        Some(Self { bytes })
1099    }
1100
1101    fn read_ne_u32(&self, offset: usize) -> Option<u32> {
1102        let raw: [u8; 4] = self
1103            .bytes
1104            .get(offset..offset.checked_add(4)?)?
1105            .try_into()
1106            .ok()?;
1107        Some(u32::from_ne_bytes(raw))
1108    }
1109
1110    fn read_ne_i32(&self, offset: usize) -> Option<i32> {
1111        let raw: [u8; 4] = self
1112            .bytes
1113            .get(offset..offset.checked_add(4)?)?
1114            .try_into()
1115            .ok()?;
1116        Some(i32::from_ne_bytes(raw))
1117    }
1118
1119    fn read_ne_f32(&self, offset: usize) -> Option<f32> {
1120        let raw: [u8; 4] = self
1121            .bytes
1122            .get(offset..offset.checked_add(4)?)?
1123            .try_into()
1124            .ok()?;
1125        Some(f32::from_ne_bytes(raw))
1126    }
1127
1128    fn read_ne_f64(&self, offset: usize) -> Option<f64> {
1129        let raw: [u8; 8] = self
1130            .bytes
1131            .get(offset..offset.checked_add(8)?)?
1132            .try_into()
1133            .ok()?;
1134        Some(f64::from_ne_bytes(raw))
1135    }
1136
1137    fn read_ne_f32_array<const N: usize>(&self, offset: usize) -> Option<[f32; N]> {
1138        let need = 4usize.checked_mul(N)?;
1139        let slice = self.bytes.get(offset..offset.checked_add(need)?)?;
1140        let mut out = [0.0f32; N];
1141        for (i, slot) in out.iter_mut().enumerate() {
1142            let start = i * 4;
1143            let raw: [u8; 4] = slice[start..start + 4].try_into().ok()?;
1144            *slot = f32::from_ne_bytes(raw);
1145        }
1146        Some(out)
1147    }
1148
1149    fn read_ne_f64_array<const N: usize>(&self, offset: usize) -> Option<[f64; N]> {
1150        let need = 8usize.checked_mul(N)?;
1151        let slice = self.bytes.get(offset..offset.checked_add(need)?)?;
1152        let mut out = [0.0f64; N];
1153        for (i, slot) in out.iter_mut().enumerate() {
1154            let start = i * 8;
1155            let raw: [u8; 8] = slice[start..start + 8].try_into().ok()?;
1156            *slot = f64::from_ne_bytes(raw);
1157        }
1158        Some(out)
1159    }
1160
1161    fn decode_ai_string(&self) -> Option<sys::aiString> {
1162        let declared_len = self.read_ne_u32(0)? as usize;
1163        let payload = self.bytes.get(4..)?;
1164        let max = (sys::AI_MAXLEN as usize).saturating_sub(1);
1165        let copy_len = declared_len.min(payload.len()).min(max);
1166
1167        let mut value = sys::aiString {
1168            length: copy_len as u32,
1169            data: [0; sys::AI_MAXLEN as usize],
1170        };
1171
1172        for (dst, src) in value.data[..copy_len].iter_mut().zip(&payload[..copy_len]) {
1173            *dst = *src as std::os::raw::c_char;
1174        }
1175        value.data[copy_len] = 0;
1176        Some(value)
1177    }
1178}
1179
1180impl MaterialPropertyRef {
1181    fn from_ptr(scene: Scene, prop_ptr: *const sys::aiMaterialProperty) -> Self {
1182        debug_assert!(!prop_ptr.is_null());
1183        let prop_ptr = unsafe { SharedPtr::new_unchecked(prop_ptr) };
1184        Self { scene, prop_ptr }
1185    }
1186
1187    #[inline]
1188    fn raw(&self) -> &sys::aiMaterialProperty {
1189        self.prop_ptr.as_ref()
1190    }
1191
1192    /// Property key as UTF-8 (lossy), without allocation.
1193    pub fn key_str(&self) -> Cow<'_, str> {
1194        ai_string_to_str(&self.raw().mKey)
1195    }
1196
1197    /// Raw bytes of the key (without assuming NUL-termination).
1198    pub fn key_bytes(&self) -> &[u8] {
1199        let s = &self.raw().mKey;
1200        let len = (s.length as usize).min(s.data.len());
1201        ffi::slice_from_ptr_len(self, s.data.as_ptr() as *const u8, len)
1202    }
1203
1204    /// Property key as owned `String` (allocates).
1205    pub fn key_string(&self) -> String {
1206        ai_string_to_string(&self.raw().mKey)
1207    }
1208
1209    /// Semantic (texture type) if texture-related.
1210    pub fn semantic(&self) -> Option<TextureType> {
1211        TextureType::from_u32(self.raw().mSemantic)
1212    }
1213
1214    /// Texture index (0 for non-texture properties).
1215    pub fn index(&self) -> u32 {
1216        self.raw().mIndex
1217    }
1218
1219    /// Property type info.
1220    pub fn type_info(&self) -> PropertyTypeInfo {
1221        PropertyTypeInfo::from_sys(self.raw().mType)
1222    }
1223
1224    /// Raw property bytes as stored by Assimp (zero-copy).
1225    pub fn data(&self) -> &[u8] {
1226        let p = self.raw();
1227        ffi::slice_from_ptr_len(self, p.mData as *const u8, p.mDataLength as usize)
1228    }
1229
1230    /// Interpret the property payload as an `i32` slice when stored as `Integer` (zero-copy).
1231    pub fn data_i32(&self) -> Option<&[i32]> {
1232        (self.type_info() == PropertyTypeInfo::Integer)
1233            .then(|| self.data_cast_slice_opt())
1234            .flatten()
1235    }
1236
1237    /// Interpret the property payload as a `u32` slice when stored as `Integer` (zero-copy).
1238    ///
1239    /// Note: Assimp stores integer properties as signed `int`. This method simply
1240    /// reinterprets the bytes as `u32` when the payload is aligned and sized for `u32`.
1241    pub fn data_u32(&self) -> Option<&[u32]> {
1242        (self.type_info() == PropertyTypeInfo::Integer)
1243            .then(|| self.data_cast_slice_opt())
1244            .flatten()
1245    }
1246
1247    /// Interpret the property payload as an `f32` slice when stored as `Float` (zero-copy).
1248    pub fn data_f32(&self) -> Option<&[f32]> {
1249        (self.type_info() == PropertyTypeInfo::Float)
1250            .then(|| self.data_cast_slice_opt())
1251            .flatten()
1252    }
1253
1254    /// Interpret the property payload as an `f64` slice when stored as `Double` (zero-copy).
1255    pub fn data_f64(&self) -> Option<&[f64]> {
1256        (self.type_info() == PropertyTypeInfo::Double)
1257            .then(|| self.data_cast_slice_opt())
1258            .flatten()
1259    }
1260
1261    /// Interpret the property payload as an `aiString` when stored as `String` (no heap allocation).
1262    ///
1263    /// Assimp material properties store strings in a compact `aiString`-compatible binary layout:
1264    /// a 32-bit byte length followed by the UTF-8 (usually) bytes (typically including a trailing NUL).
1265    /// We decode this representation into an owned `aiString` buffer for safe, allocation-free use.
1266    pub fn string_ref(&self) -> Option<MaterialStringRef> {
1267        if self.type_info() != PropertyTypeInfo::String {
1268            return None;
1269        }
1270        let p = self.raw();
1271        let d = unsafe { MaterialPropertyData::from_sys(p) }?;
1272        let value = d.decode_ai_string()?;
1273        Some(MaterialStringRef { value })
1274    }
1275
1276    /// Read the first element as `i32` when stored as `Integer`.
1277    pub fn as_i32(&self) -> Option<i32> {
1278        if self.type_info() != PropertyTypeInfo::Integer {
1279            return None;
1280        }
1281        let p = self.raw();
1282        let d = unsafe { MaterialPropertyData::from_sys(p) }?;
1283        d.read_ne_i32(0)
1284    }
1285
1286    /// Read the first element as `u32` when stored as `Integer` and non-negative.
1287    pub fn as_u32(&self) -> Option<u32> {
1288        u32::try_from(self.as_i32()?).ok()
1289    }
1290
1291    /// Read the first element as `bool` when stored as `Integer` (non-zero => true).
1292    pub fn as_bool(&self) -> Option<bool> {
1293        Some(self.as_i32()? != 0)
1294    }
1295
1296    /// Read the first element as `f32` when stored as `Float`.
1297    pub fn as_f32(&self) -> Option<f32> {
1298        if self.type_info() != PropertyTypeInfo::Float {
1299            return None;
1300        }
1301        let p = self.raw();
1302        let d = unsafe { MaterialPropertyData::from_sys(p) }?;
1303        d.read_ne_f32(0)
1304    }
1305
1306    /// Read the first element as `f64` when stored as `Double`.
1307    pub fn as_f64(&self) -> Option<f64> {
1308        if self.type_info() != PropertyTypeInfo::Double {
1309            return None;
1310        }
1311        let p = self.raw();
1312        let d = unsafe { MaterialPropertyData::from_sys(p) }?;
1313        d.read_ne_f64(0)
1314    }
1315
1316    /// Read `N` elements as `f32` when stored as `Float`.
1317    pub fn as_f32_array<const N: usize>(&self) -> Option<[f32; N]> {
1318        if self.type_info() != PropertyTypeInfo::Float {
1319            return None;
1320        }
1321        let p = self.raw();
1322        let d = unsafe { MaterialPropertyData::from_sys(p) }?;
1323        d.read_ne_f32_array::<N>(0)
1324    }
1325
1326    /// Read `N` elements as `f64` when stored as `Double`.
1327    pub fn as_f64_array<const N: usize>(&self) -> Option<[f64; N]> {
1328        if self.type_info() != PropertyTypeInfo::Double {
1329            return None;
1330        }
1331        let p = self.raw();
1332        let d = unsafe { MaterialPropertyData::from_sys(p) }?;
1333        d.read_ne_f64_array::<N>(0)
1334    }
1335
1336    /// Read a `Vec2` when stored as `Float` and the payload has at least 2 floats.
1337    pub fn as_vec2(&self) -> Option<Vector2D> {
1338        let [x, y] = self.as_f32_array::<2>()?;
1339        Some(Vector2D::new(x, y))
1340    }
1341
1342    /// Read a `Vec3` when stored as `Float` and the payload has at least 3 floats.
1343    pub fn as_vec3(&self) -> Option<Vector3D> {
1344        let [x, y, z] = self.as_f32_array::<3>()?;
1345        Some(Vector3D::new(x, y, z))
1346    }
1347
1348    /// Read a `Vec4` when stored as `Float` and the payload has at least 4 floats.
1349    pub fn as_vec4(&self) -> Option<Vector4D> {
1350        let [x, y, z, w] = self.as_f32_array::<4>()?;
1351        Some(Vector4D::new(x, y, z, w))
1352    }
1353
1354    /// Read an RGB color when stored as `Float` and the payload has at least 3 floats.
1355    pub fn as_color3(&self) -> Option<Color3D> {
1356        self.as_vec3()
1357    }
1358
1359    /// Read an RGBA color when stored as `Float` and the payload has at least 4 floats.
1360    pub fn as_color4(&self) -> Option<Color4D> {
1361        self.as_vec4()
1362    }
1363
1364    fn data_cast_slice_opt<T>(&self) -> Option<&[T]> {
1365        let p = self.raw();
1366        let len = p.mDataLength as usize;
1367        let size = std::mem::size_of::<T>();
1368        let align = std::mem::align_of::<T>();
1369
1370        if len == 0 {
1371            return Some(&[]);
1372        }
1373        if p.mData.is_null() {
1374            return None;
1375        }
1376
1377        let ptr = p.mData as *const u8;
1378        if size == 0 {
1379            return Some(&[]);
1380        }
1381        if (ptr as usize) % align != 0 || len % size != 0 {
1382            return None;
1383        }
1384        Some(ffi::slice_from_ptr_len(self, ptr as *const T, len / size))
1385    }
1386
1387    fn into_info(self) -> MaterialPropertyInfo {
1388        MaterialPropertyInfo::from_ref(self)
1389    }
1390}
1391
1392/// Iterator over material properties (skips null entries).
1393pub struct MaterialPropertyIterator {
1394    scene: Scene,
1395    props: Option<SharedPtr<*const sys::aiMaterialProperty>>,
1396    count: usize,
1397    index: usize,
1398}
1399
1400impl Iterator for MaterialPropertyIterator {
1401    type Item = MaterialPropertyRef;
1402
1403    fn next(&mut self) -> Option<Self::Item> {
1404        let props = self.props?;
1405        let slice = crate::ffi::slice_from_ptr_len_opt(&(), props.as_ptr(), self.count)?;
1406        while self.index < slice.len() {
1407            let ptr = slice[self.index];
1408            self.index += 1;
1409            if ptr.is_null() {
1410                continue;
1411            }
1412            return Some(MaterialPropertyRef::from_ptr(self.scene.clone(), ptr));
1413        }
1414        None
1415    }
1416}
1417
1418impl TextureType {
1419    #[inline]
1420    fn to_sys(self) -> sys::aiTextureType {
1421        match self {
1422            Self::Diffuse => sys::aiTextureType::aiTextureType_DIFFUSE,
1423            Self::Specular => sys::aiTextureType::aiTextureType_SPECULAR,
1424            Self::Ambient => sys::aiTextureType::aiTextureType_AMBIENT,
1425            Self::Emissive => sys::aiTextureType::aiTextureType_EMISSIVE,
1426            Self::Height => sys::aiTextureType::aiTextureType_HEIGHT,
1427            Self::Normals => sys::aiTextureType::aiTextureType_NORMALS,
1428            Self::Shininess => sys::aiTextureType::aiTextureType_SHININESS,
1429            Self::Opacity => sys::aiTextureType::aiTextureType_OPACITY,
1430            Self::Displacement => sys::aiTextureType::aiTextureType_DISPLACEMENT,
1431            Self::Lightmap => sys::aiTextureType::aiTextureType_LIGHTMAP,
1432            Self::Reflection => sys::aiTextureType::aiTextureType_REFLECTION,
1433            Self::BaseColor => sys::aiTextureType::aiTextureType_BASE_COLOR,
1434            Self::NormalCamera => sys::aiTextureType::aiTextureType_NORMAL_CAMERA,
1435            Self::EmissionColor => sys::aiTextureType::aiTextureType_EMISSION_COLOR,
1436            Self::Metalness => sys::aiTextureType::aiTextureType_METALNESS,
1437            Self::DiffuseRoughness => sys::aiTextureType::aiTextureType_DIFFUSE_ROUGHNESS,
1438            Self::AmbientOcclusion => sys::aiTextureType::aiTextureType_AMBIENT_OCCLUSION,
1439            Self::Unknown => sys::aiTextureType::aiTextureType_UNKNOWN,
1440            Self::Sheen => sys::aiTextureType::aiTextureType_SHEEN,
1441            Self::Clearcoat => sys::aiTextureType::aiTextureType_CLEARCOAT,
1442            Self::Transmission => sys::aiTextureType::aiTextureType_TRANSMISSION,
1443            Self::MayaBase => sys::aiTextureType::aiTextureType_MAYA_BASE,
1444            Self::MayaSpecular => sys::aiTextureType::aiTextureType_MAYA_SPECULAR,
1445            Self::MayaSpecularColor => sys::aiTextureType::aiTextureType_MAYA_SPECULAR_COLOR,
1446            Self::MayaSpecularRoughness => {
1447                sys::aiTextureType::aiTextureType_MAYA_SPECULAR_ROUGHNESS
1448            }
1449            Self::Anisotropy => sys::aiTextureType::aiTextureType_ANISOTROPY,
1450            Self::GltfMetallicRoughness => {
1451                sys::aiTextureType::aiTextureType_GLTF_METALLIC_ROUGHNESS
1452            }
1453        }
1454    }
1455
1456    /// Try convert a raw u32 (aiTextureType) into TextureType safely
1457    pub fn from_u32(v: u32) -> Option<Self> {
1458        Some(match v {
1459            x if x == sys::aiTextureType::aiTextureType_DIFFUSE as u32 => Self::Diffuse,
1460            x if x == sys::aiTextureType::aiTextureType_SPECULAR as u32 => Self::Specular,
1461            x if x == sys::aiTextureType::aiTextureType_AMBIENT as u32 => Self::Ambient,
1462            x if x == sys::aiTextureType::aiTextureType_EMISSIVE as u32 => Self::Emissive,
1463            x if x == sys::aiTextureType::aiTextureType_HEIGHT as u32 => Self::Height,
1464            x if x == sys::aiTextureType::aiTextureType_NORMALS as u32 => Self::Normals,
1465            x if x == sys::aiTextureType::aiTextureType_SHININESS as u32 => Self::Shininess,
1466            x if x == sys::aiTextureType::aiTextureType_OPACITY as u32 => Self::Opacity,
1467            x if x == sys::aiTextureType::aiTextureType_DISPLACEMENT as u32 => Self::Displacement,
1468            x if x == sys::aiTextureType::aiTextureType_LIGHTMAP as u32 => Self::Lightmap,
1469            x if x == sys::aiTextureType::aiTextureType_REFLECTION as u32 => Self::Reflection,
1470            x if x == sys::aiTextureType::aiTextureType_BASE_COLOR as u32 => Self::BaseColor,
1471            x if x == sys::aiTextureType::aiTextureType_NORMAL_CAMERA as u32 => Self::NormalCamera,
1472            x if x == sys::aiTextureType::aiTextureType_EMISSION_COLOR as u32 => {
1473                Self::EmissionColor
1474            }
1475            x if x == sys::aiTextureType::aiTextureType_METALNESS as u32 => Self::Metalness,
1476            x if x == sys::aiTextureType::aiTextureType_DIFFUSE_ROUGHNESS as u32 => {
1477                Self::DiffuseRoughness
1478            }
1479            x if x == sys::aiTextureType::aiTextureType_AMBIENT_OCCLUSION as u32 => {
1480                Self::AmbientOcclusion
1481            }
1482            x if x == sys::aiTextureType::aiTextureType_UNKNOWN as u32 => Self::Unknown,
1483            x if x == sys::aiTextureType::aiTextureType_SHEEN as u32 => Self::Sheen,
1484            x if x == sys::aiTextureType::aiTextureType_CLEARCOAT as u32 => Self::Clearcoat,
1485            x if x == sys::aiTextureType::aiTextureType_TRANSMISSION as u32 => Self::Transmission,
1486            x if x == sys::aiTextureType::aiTextureType_MAYA_BASE as u32 => Self::MayaBase,
1487            x if x == sys::aiTextureType::aiTextureType_MAYA_SPECULAR as u32 => Self::MayaSpecular,
1488            x if x == sys::aiTextureType::aiTextureType_MAYA_SPECULAR_COLOR as u32 => {
1489                Self::MayaSpecularColor
1490            }
1491            x if x == sys::aiTextureType::aiTextureType_MAYA_SPECULAR_ROUGHNESS as u32 => {
1492                Self::MayaSpecularRoughness
1493            }
1494            x if x == sys::aiTextureType::aiTextureType_ANISOTROPY as u32 => Self::Anisotropy,
1495            x if x == sys::aiTextureType::aiTextureType_GLTF_METALLIC_ROUGHNESS as u32 => {
1496                Self::GltfMetallicRoughness
1497            }
1498            _ => return None,
1499        })
1500    }
1501}
1502
1503#[cfg(test)]
1504mod material_property_data_tests {
1505    use super::*;
1506
1507    fn make_prop_with_data(mut data: Vec<u8>) -> (sys::aiMaterialProperty, Vec<u8>) {
1508        let prop = sys::aiMaterialProperty {
1509            mDataLength: data.len() as u32,
1510            mData: data.as_mut_ptr().cast::<std::os::raw::c_char>(),
1511            ..Default::default()
1512        };
1513        (prop, data)
1514    }
1515
1516    #[test]
1517    fn decode_ai_string_clamps_to_payload_and_adds_terminator() {
1518        let mut data = Vec::new();
1519        data.extend_from_slice(&999u32.to_ne_bytes()); // declared length > payload
1520        data.extend_from_slice(b"abc"); // payload shorter than declared length
1521
1522        let (prop, _data_owner) = make_prop_with_data(data);
1523        let decoded = unsafe { MaterialPropertyData::from_sys(&prop) }
1524            .and_then(|d| d.decode_ai_string())
1525            .unwrap();
1526
1527        assert_eq!(decoded.length, 3);
1528        let bytes: Vec<u8> = decoded.data[..3].iter().map(|c| *c as u8).collect();
1529        assert_eq!(bytes, b"abc");
1530        assert_eq!(decoded.data[3] as u8, 0);
1531    }
1532
1533    #[test]
1534    fn read_ne_primitives_and_arrays_from_bytes() {
1535        let mut data = Vec::new();
1536        data.extend_from_slice(&42i32.to_ne_bytes());
1537        data.extend_from_slice(&1.0f32.to_ne_bytes());
1538        data.extend_from_slice(&2.0f32.to_ne_bytes());
1539        data.extend_from_slice(&3.5f32.to_ne_bytes());
1540        data.extend_from_slice(&9.0f64.to_ne_bytes());
1541
1542        let (prop, _data_owner) = make_prop_with_data(data);
1543        let d = unsafe { MaterialPropertyData::from_sys(&prop) }.unwrap();
1544
1545        assert_eq!(d.read_ne_i32(0), Some(42));
1546        assert_eq!(d.read_ne_f32_array::<3>(4), Some([1.0, 2.0, 3.5]));
1547        assert_eq!(d.read_ne_f64(4 + 12), Some(9.0));
1548        assert_eq!(d.read_ne_u32(9999), None);
1549    }
1550}
1551
1552/// Blend mode for material layers
1553#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1554pub enum BlendMode {
1555    /// Default blending mode (usually alpha blending)
1556    Default,
1557    /// Additive blending mode
1558    Additive,
1559    /// Unknown blend mode with raw value
1560    Unknown(u32),
1561}
1562
1563impl BlendMode {
1564    fn from_raw(v: u32) -> Self {
1565        match v {
1566            x if x == sys::aiBlendMode::aiBlendMode_Default as u32 => BlendMode::Default,
1567            x if x == sys::aiBlendMode::aiBlendMode_Additive as u32 => BlendMode::Additive,
1568            other => BlendMode::Unknown(other),
1569        }
1570    }
1571}
1572
1573/// Which PBR workflow this material uses (heuristic from material.h docs)
1574#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1575pub enum PbrWorkflow {
1576    /// Metallic-roughness PBR workflow (glTF 2.0 standard)
1577    MetallicRoughness,
1578    /// Specular-glossiness PBR workflow (legacy)
1579    SpecularGlossiness,
1580    /// Unknown or undetected PBR workflow
1581    Unknown,
1582}
1583
1584impl Material {
1585    /// Determine PBR workflow based on present factors
1586    pub fn pbr_workflow(&self) -> PbrWorkflow {
1587        if self.metallic_factor().is_some() || self.roughness_factor().is_some() {
1588            PbrWorkflow::MetallicRoughness
1589        } else if self.glossiness_factor().is_some() || self.specular_factor().is_some() {
1590            PbrWorkflow::SpecularGlossiness
1591        } else {
1592            PbrWorkflow::Unknown
1593        }
1594    }
1595
1596    // ---------- Convenience texture getters ----------
1597    /// Get base color texture at the specified index
1598    pub fn base_color_texture(&self, index: usize) -> Option<TextureInfo> {
1599        self.texture(TextureType::BaseColor, index)
1600    }
1601
1602    /// Get the metallic-roughness texture (glTF packed format)
1603    pub fn metallic_roughness_texture(&self) -> Option<TextureInfo> {
1604        // glTF packed metallic-roughness (one texture, index 0)
1605        self.texture(TextureType::GltfMetallicRoughness, 0)
1606    }
1607
1608    /// Get emission texture at the specified index
1609    pub fn emission_texture(&self, index: usize) -> Option<TextureInfo> {
1610        self.texture(TextureType::EmissionColor, index)
1611    }
1612
1613    /// Get normal map texture at the specified index
1614    pub fn normal_texture(&self, index: usize) -> Option<TextureInfo> {
1615        self.texture(TextureType::Normals, index)
1616    }
1617
1618    /// Get sheen color texture
1619    pub fn sheen_color_texture(&self) -> Option<TextureInfo> {
1620        // sheen color texture is TextureType::Sheen, index 0
1621        self.texture(TextureType::Sheen, 0)
1622    }
1623
1624    /// Get sheen roughness texture
1625    pub fn sheen_roughness_texture(&self) -> Option<TextureInfo> {
1626        // sheen roughness texture is TextureType::Sheen, index 1
1627        self.texture(TextureType::Sheen, 1)
1628    }
1629
1630    /// Get clearcoat texture
1631    pub fn clearcoat_texture(&self) -> Option<TextureInfo> {
1632        self.texture(TextureType::Clearcoat, 0)
1633    }
1634
1635    /// Get clearcoat roughness texture
1636    pub fn clearcoat_roughness_texture(&self) -> Option<TextureInfo> {
1637        self.texture(TextureType::Clearcoat, 1)
1638    }
1639
1640    /// Get clearcoat normal map texture
1641    pub fn clearcoat_normal_texture(&self) -> Option<TextureInfo> {
1642        self.texture(TextureType::Clearcoat, 2)
1643    }
1644
1645    /// Get transmission texture
1646    pub fn transmission_texture(&self) -> Option<TextureInfo> {
1647        self.texture(TextureType::Transmission, 0)
1648    }
1649
1650    /// Get volume thickness texture
1651    pub fn volume_thickness_texture(&self) -> Option<TextureInfo> {
1652        // Defined to use aiTextureType_TRANSMISSION, index 1
1653        self.texture(TextureType::Transmission, 1)
1654    }
1655
1656    /// Get anisotropy texture
1657    pub fn anisotropy_texture(&self) -> Option<TextureInfo> {
1658        self.texture(TextureType::Anisotropy, 0)
1659    }
1660
1661    /// Albedo texture (alias of BaseColor)
1662    pub fn albedo_texture(&self, index: usize) -> Option<TextureInfo> {
1663        self.base_color_texture(index)
1664    }
1665
1666    /// Metallic texture (separate channel, not glTF packed)
1667    pub fn metallic_texture(&self, index: usize) -> Option<TextureInfo> {
1668        self.texture(TextureType::Metalness, index)
1669    }
1670
1671    /// Roughness texture (separate channel, not glTF packed)
1672    pub fn roughness_texture(&self, index: usize) -> Option<TextureInfo> {
1673        self.texture(TextureType::DiffuseRoughness, index)
1674    }
1675
1676    /// Ambient occlusion texture
1677    pub fn ambient_occlusion_texture(&self, index: usize) -> Option<TextureInfo> {
1678        self.texture(TextureType::AmbientOcclusion, index)
1679    }
1680
1681    /// Lightmap texture
1682    pub fn lightmap_texture(&self, index: usize) -> Option<TextureInfo> {
1683        self.texture(TextureType::Lightmap, index)
1684    }
1685
1686    /// Displacement texture
1687    pub fn displacement_texture(&self, index: usize) -> Option<TextureInfo> {
1688        self.texture(TextureType::Displacement, index)
1689    }
1690
1691    /// Reflection/environment texture
1692    pub fn reflection_texture(&self, index: usize) -> Option<TextureInfo> {
1693        self.texture(TextureType::Reflection, index)
1694    }
1695
1696    /// Opacity texture
1697    pub fn opacity_texture(&self, index: usize) -> Option<TextureInfo> {
1698        self.texture(TextureType::Opacity, index)
1699    }
1700
1701    /// Height map texture (some formats use this for parallax/bump)
1702    pub fn height_texture(&self, index: usize) -> Option<TextureInfo> {
1703        self.texture(TextureType::Height, index)
1704    }
1705
1706    /// Specular map (spec/gloss workflow)
1707    pub fn specular_texture(&self, index: usize) -> Option<TextureInfo> {
1708        self.texture(TextureType::Specular, index)
1709    }
1710
1711    /// Glossiness map (spec/gloss workflow)
1712    pub fn glossiness_texture(&self, index: usize) -> Option<TextureInfo> {
1713        self.texture(TextureType::Shininess, index)
1714    }
1715
1716    /// Emissive map (PBR emission color)
1717    pub fn emissive_texture(&self, index: usize) -> Option<TextureInfo> {
1718        self.texture(TextureType::EmissionColor, index)
1719    }
1720}
1721
1722/// Texture mapping modes
1723#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1724pub enum TextureMapping {
1725    /// UV coordinate mapping
1726    UV,
1727    /// Spherical mapping
1728    Sphere,
1729    /// Cylindrical mapping
1730    Cylinder,
1731    /// Box mapping
1732    Box,
1733    /// Planar mapping
1734    Plane,
1735    /// Other mapping mode
1736    Other(u32),
1737}
1738
1739impl TextureMapping {
1740    fn from_raw(value: sys::aiTextureMapping) -> Self {
1741        let value_u32 = value as u32;
1742        match value_u32 {
1743            v if v == sys::aiTextureMapping::aiTextureMapping_UV as u32 => Self::UV,
1744            v if v == sys::aiTextureMapping::aiTextureMapping_SPHERE as u32 => Self::Sphere,
1745            v if v == sys::aiTextureMapping::aiTextureMapping_CYLINDER as u32 => Self::Cylinder,
1746            v if v == sys::aiTextureMapping::aiTextureMapping_BOX as u32 => Self::Box,
1747            v if v == sys::aiTextureMapping::aiTextureMapping_PLANE as u32 => Self::Plane,
1748            other => Self::Other(other),
1749        }
1750    }
1751}
1752
1753/// Texture operations
1754#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1755pub enum TextureOperation {
1756    /// Multiply operation
1757    Multiply,
1758    /// Add operation
1759    Add,
1760    /// Subtract operation
1761    Subtract,
1762    /// Divide operation
1763    Divide,
1764    /// Smooth add operation
1765    SmoothAdd,
1766    /// Signed add operation
1767    SignedAdd,
1768    /// Other operation
1769    Other(u32),
1770}
1771
1772impl TextureOperation {
1773    fn from_raw(value: sys::aiTextureOp) -> Self {
1774        let value_u32 = value as u32;
1775        match value_u32 {
1776            v if v == sys::aiTextureOp::aiTextureOp_Multiply as u32 => Self::Multiply,
1777            v if v == sys::aiTextureOp::aiTextureOp_Add as u32 => Self::Add,
1778            v if v == sys::aiTextureOp::aiTextureOp_Subtract as u32 => Self::Subtract,
1779            v if v == sys::aiTextureOp::aiTextureOp_Divide as u32 => Self::Divide,
1780            v if v == sys::aiTextureOp::aiTextureOp_SmoothAdd as u32 => Self::SmoothAdd,
1781            v if v == sys::aiTextureOp::aiTextureOp_SignedAdd as u32 => Self::SignedAdd,
1782            other => Self::Other(other),
1783        }
1784    }
1785}
1786
1787/// Texture mapping modes for UV coordinates
1788#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1789pub enum TextureMapMode {
1790    /// Wrap texture coordinates
1791    Wrap,
1792    /// Clamp texture coordinates to edge
1793    Clamp,
1794    /// Mirror texture coordinates
1795    Mirror,
1796    /// Decal texture mode
1797    Decal,
1798    /// Other texture map mode
1799    Other(u32),
1800}
1801
1802impl TextureMapMode {
1803    fn from_raw(value: sys::aiTextureMapMode) -> Self {
1804        let value_u32 = value as u32;
1805        match value_u32 {
1806            v if v == sys::aiTextureMapMode::aiTextureMapMode_Wrap as u32 => Self::Wrap,
1807            v if v == sys::aiTextureMapMode::aiTextureMapMode_Clamp as u32 => Self::Clamp,
1808            v if v == sys::aiTextureMapMode::aiTextureMapMode_Mirror as u32 => Self::Mirror,
1809            v if v == sys::aiTextureMapMode::aiTextureMapMode_Decal as u32 => Self::Decal,
1810            other => Self::Other(other),
1811        }
1812    }
1813}
1814
1815/// Information about a texture applied to a material
1816#[derive(Debug, Clone)]
1817pub struct TextureInfoRef {
1818    path: sys::aiString,
1819    /// Texture mapping mode
1820    pub mapping: TextureMapping,
1821    /// UV channel index
1822    pub uv_index: u32,
1823    /// Blend factor
1824    pub blend_factor: f32,
1825    /// Texture operation
1826    pub operation: TextureOperation,
1827    /// Texture map modes for U, V, W coordinates
1828    pub map_modes: [TextureMapMode; 3],
1829    /// Texture flags
1830    pub flags: TextureFlags,
1831    /// Optional UV transform
1832    pub uv_transform: Option<UVTransform>,
1833    /// Optional texture mapping axis
1834    pub axis: Option<Vector3D>,
1835}
1836
1837impl TextureInfoRef {
1838    /// Texture path as UTF-8 (lossy), without allocation.
1839    pub fn path_str(&self) -> Cow<'_, str> {
1840        ai_string_to_str(&self.path)
1841    }
1842
1843    /// Raw bytes of the path (without assuming NUL-termination).
1844    pub fn path_bytes(&self) -> &[u8] {
1845        let len = (self.path.length as usize).min(self.path.data.len());
1846        ffi::slice_from_ptr_len(self, self.path.data.as_ptr() as *const u8, len)
1847    }
1848
1849    /// Borrow the underlying Assimp `aiString`.
1850    #[cfg(feature = "raw-sys")]
1851    pub fn path_raw(&self) -> &sys::aiString {
1852        &self.path
1853    }
1854
1855    /// Convert into an owned `TextureInfo` (allocates for the path string).
1856    pub fn into_owned(self) -> TextureInfo {
1857        TextureInfo {
1858            path: ai_string_to_string(&self.path),
1859            mapping: self.mapping,
1860            uv_index: self.uv_index,
1861            blend_factor: self.blend_factor,
1862            operation: self.operation,
1863            map_modes: self.map_modes,
1864            flags: self.flags,
1865            uv_transform: self.uv_transform,
1866            axis: self.axis,
1867        }
1868    }
1869
1870    /// Convert into an owned `TextureInfo` (allocates for the path string).
1871    pub fn to_owned(&self) -> TextureInfo {
1872        self.clone().into_owned()
1873    }
1874}
1875
1876/// Owned information about a texture applied to a material.
1877pub struct TextureInfo {
1878    /// Path to the texture file
1879    pub path: String,
1880    /// Texture mapping mode
1881    pub mapping: TextureMapping,
1882    /// UV channel index
1883    pub uv_index: u32,
1884    /// Blend factor
1885    pub blend_factor: f32,
1886    /// Texture operation
1887    pub operation: TextureOperation,
1888    /// Texture map modes for U, V, W coordinates
1889    pub map_modes: [TextureMapMode; 3],
1890    /// Texture flags
1891    pub flags: TextureFlags,
1892    /// Optional UV transform
1893    pub uv_transform: Option<UVTransform>,
1894    /// Optional texture mapping axis
1895    pub axis: Option<Vector3D>,
1896}
1897
1898/// UV transform information
1899#[derive(Debug, Clone, Copy)]
1900pub struct UVTransform {
1901    /// Translation offset for UV coordinates
1902    pub translation: Vector2D,
1903    /// Scaling factor for UV coordinates
1904    pub scaling: Vector2D,
1905    /// Rotation angle in radians
1906    pub rotation: f32,
1907}
1908
1909bitflags::bitflags! {
1910    /// Texture flags (material.h: aiTextureFlags)
1911    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1912    pub struct TextureFlags: u32 {
1913        /// Invert the texture colors
1914        const INVERT        = sys::aiTextureFlags::aiTextureFlags_Invert as u32;
1915        /// Use the alpha channel of the texture
1916        const USE_ALPHA     = sys::aiTextureFlags::aiTextureFlags_UseAlpha as u32;
1917        /// Ignore the alpha channel of the texture
1918        const IGNORE_ALPHA  = sys::aiTextureFlags::aiTextureFlags_IgnoreAlpha as u32;
1919    }
1920}
1921
1922// Auto-traits (Send/Sync) are derived from the contained pointers and lifetimes.