Skip to main content

asset_importer/
lib.rs

1//! # Asset Importer
2//!
3//! Comprehensive Rust bindings for the Assimp 3D asset import library.
4//!
5//! This crate provides safe, idiomatic Rust bindings for Assimp, offering
6//! more complete functionality than existing alternatives like `russimp`.
7//!
8//! ## Features
9//!
10//! - **Complete API coverage**: Import, export, and post-processing
11//! - **Memory safety**: Safe Rust abstractions over raw C API
12//! - **Zero-cost abstractions**: Minimal overhead over direct C API usage
13//! - **Custom I/O**: Support for custom file systems and progress callbacks
14//! - **Modern Rust**: Uses latest Rust features and idioms
15//!
16//! ## Quick Start
17//!
18//! ```rust,no_run
19//! use asset_importer::{Importer, postprocess::PostProcessSteps};
20//!
21//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
22//! let scene = Importer::new().import_file_with("model.fbx", |b| {
23//!     b.with_post_process(PostProcessSteps::TRIANGULATE | PostProcessSteps::FLIP_UVS)
24//! })?;
25//!
26//! println!("Loaded {} meshes", scene.meshes().count());
27//! # Ok(())
28//! # }
29//! ```
30//!
31//! ## Architecture
32//!
33//! This crate is built on top of `asset-importer-sys`, which provides the raw
34//! FFI bindings. The high-level API is designed to be:
35//!
36//! - **Safe**: All unsafe operations are encapsulated
37//! - **Ergonomic**: Builder patterns and method chaining
38//! - **Efficient**: Zero-copy where possible
39//! - **Extensible**: Support for custom I/O and callbacks
40//!
41//! ## Build features
42//! This crate supports three mutually exclusive build modes:
43//! - `prebuilt` (default): download/use prebuilt Assimp binaries via `asset-importer-sys`
44//! - `build-assimp`: build Assimp from source via CMake
45//! - `system`: link against a system-installed Assimp (requires libclang/bindgen)
46//!
47//! For `system`, use `--no-default-features --features system`.
48
49#![deny(unsafe_op_in_unsafe_fn)]
50#![warn(missing_docs)]
51
52#[cfg(any(
53    all(feature = "prebuilt", feature = "build-assimp"),
54    all(feature = "prebuilt", feature = "system"),
55    all(feature = "build-assimp", feature = "system"),
56))]
57compile_error!(
58    "Build mode features are mutually exclusive. Use exactly one of: `prebuilt` (default), `build-assimp`, or `system`.\n\
59     Hint: for system Assimp use `--no-default-features --features system`."
60);
61
62#[cfg(feature = "raw-sys")]
63pub use asset_importer_sys as sys;
64
65#[cfg(not(feature = "raw-sys"))]
66pub(crate) use asset_importer_sys as sys;
67
68// Re-export common types for convenience
69pub use crate::{
70    error::{Error, Result},
71    importer::{ImportBuilder, Importer, PropertyStore, PropertyValue, import_properties},
72    scene::{MemoryInfo, Scene},
73    types::*,
74};
75
76/// Zero-copy raw view types for Assimp-owned data.
77pub mod raw;
78
79#[cfg(feature = "export")]
80pub use crate::exporter::{ExportBlob, ExportBuilder, ExportFormatDesc, export_properties};
81
82// Re-export logging functionality
83#[allow(deprecated)]
84pub use crate::logging::{LogLevel, LogStream, Logger};
85
86// Re-export metadata functionality
87pub use crate::metadata::{Metadata, MetadataEntry, MetadataType};
88
89// Re-export material functionality
90pub use crate::material::{
91    Material, MaterialPropertyInfo, MaterialPropertyIterator, MaterialPropertyRef,
92    MaterialStringRef, PropertyTypeInfo, TextureInfo, TextureInfoRef, TextureType, material_keys,
93};
94
95// Re-export texture functionality
96pub use crate::texture::{Texel, Texture, TextureData, TextureIterator};
97
98// Re-export AABB functionality
99pub use crate::aabb::AABB;
100
101// Re-export bone functionality
102pub use crate::bone::{Bone, BoneIterator, VertexWeight};
103
104// Re-export animation type for convenience (used by examples)
105pub use crate::animation::Animation;
106
107// Re-export importer description functionality
108pub use crate::importer_desc::{
109    ImporterDesc, ImporterDescIterator, ImporterFlags, get_all_importer_descs,
110    get_all_importer_descs_iter, get_importer_desc, get_importer_desc_cstr,
111};
112
113// Core modules
114mod bridge_properties;
115pub mod error;
116pub(crate) mod ffi;
117pub mod importer;
118pub mod importer_desc;
119pub mod scene;
120pub mod types;
121
122// Component modules
123pub mod animation;
124pub mod camera;
125pub mod light;
126pub mod material;
127pub mod mesh;
128pub mod node;
129
130// Data structure modules
131pub mod aabb;
132pub mod bone;
133pub mod texture;
134
135// Advanced features
136#[cfg(feature = "export")]
137pub mod exporter;
138pub mod io;
139pub mod logging;
140pub mod metadata;
141pub mod progress;
142
143// Utility modules
144pub mod math;
145pub mod postprocess;
146pub mod utils;
147
148mod ptr;
149
150/// Version information
151pub mod version {
152    /// Version of this crate
153    pub const CRATE_VERSION: &str = env!("CARGO_PKG_VERSION");
154
155    /// Version of the underlying Assimp library
156    pub fn assimp_version() -> String {
157        format!(
158            "{}.{}.{}",
159            assimp_version_major(),
160            assimp_version_minor(),
161            assimp_version_patch()
162        )
163    }
164
165    /// Major version of Assimp
166    pub fn assimp_version_major() -> u32 {
167        unsafe { crate::sys::aiGetVersionMajor() }
168    }
169
170    /// Minor version of Assimp
171    pub fn assimp_version_minor() -> u32 {
172        unsafe { crate::sys::aiGetVersionMinor() }
173    }
174
175    /// Patch version of Assimp
176    pub fn assimp_version_patch() -> u32 {
177        unsafe { crate::sys::aiGetVersionPatch() }
178    }
179
180    /// Revision of Assimp
181    pub fn assimp_version_revision() -> u32 {
182        unsafe { crate::sys::aiGetVersionRevision() }
183    }
184
185    /// Version string reported by Assimp
186    pub fn assimp_version_string() -> String {
187        assimp_version()
188    }
189
190    /// Compile flags used to build Assimp
191    pub fn assimp_compile_flags() -> u32 {
192        unsafe { crate::sys::aiGetCompileFlags() }
193    }
194
195    /// Branch name of the Assimp runtime
196    pub fn assimp_branch_name() -> String {
197        unsafe { crate::error::c_str_to_string_or_empty(crate::sys::aiGetBranchName()) }
198    }
199
200    /// Legal/license string for the Assimp runtime
201    pub fn assimp_legal_string() -> String {
202        unsafe { crate::error::c_str_to_string_or_empty(crate::sys::aiGetLegalString()) }
203    }
204}
205
206/// Check if a file extension is supported for import.
207pub fn is_extension_supported(extension: &str) -> crate::Result<bool> {
208    let c_extension = std::ffi::CString::new(extension).map_err(|_| {
209        crate::Error::invalid_parameter("file extension contains NUL byte".to_string())
210    })?;
211    Ok(unsafe { crate::sys::aiIsExtensionSupported(c_extension.as_ptr()) != 0 })
212}
213
214const FALLBACK_IMPORT_EXTENSIONS: [&str; 15] = [
215    ".obj", ".fbx", ".dae", ".gltf", ".glb", ".3ds", ".blend", ".x", ".ply", ".stl", ".md2",
216    ".md3", ".md5", ".ase", ".ifc",
217];
218
219/// An allocation-minimized import extension list.
220///
221/// This keeps the raw Assimp extension list string and provides an iterator over `&str` views
222/// (e.g. `".obj"`), avoiding per-extension allocations.
223#[derive(Debug, Clone)]
224pub struct ImportExtensions {
225    raw: Option<String>,
226}
227
228#[derive(Debug)]
229enum ImportExtensionsIterInner<'a> {
230    Assimp(std::str::Split<'a, char>),
231    Fallback(std::slice::Iter<'a, &'static str>),
232}
233
234/// Iterator over supported import extensions.
235#[derive(Debug)]
236pub struct ImportExtensionsIter<'a> {
237    inner: ImportExtensionsIterInner<'a>,
238}
239
240impl<'a> Iterator for ImportExtensionsIter<'a> {
241    type Item = &'a str;
242
243    fn next(&mut self) -> Option<Self::Item> {
244        loop {
245            match &mut self.inner {
246                ImportExtensionsIterInner::Assimp(split) => {
247                    let ext = split.next()?;
248                    let trimmed = ext.trim();
249                    if trimmed.starts_with("*.") && trimmed.len() > 1 {
250                        return Some(&trimmed[1..]);
251                    }
252                }
253                ImportExtensionsIterInner::Fallback(iter) => {
254                    let s: &'static str = iter.next()?;
255                    return Some(s);
256                }
257            }
258        }
259    }
260}
261
262impl ImportExtensions {
263    /// Raw Assimp extension list, if available (e.g. `"*.3ds;*.obj;*.dae"`).
264    pub fn raw_assimp_list(&self) -> Option<&str> {
265        self.raw.as_deref()
266    }
267
268    /// Iterate extensions as `".ext"` strings (without allocation).
269    pub fn iter(&self) -> ImportExtensionsIter<'_> {
270        if let Some(s) = self.raw.as_deref() {
271            ImportExtensionsIter {
272                inner: ImportExtensionsIterInner::Assimp(s.split(';')),
273            }
274        } else {
275            ImportExtensionsIter {
276                inner: ImportExtensionsIterInner::Fallback(FALLBACK_IMPORT_EXTENSIONS.iter()),
277            }
278        }
279    }
280
281    /// Collect into owned `String`s.
282    pub fn to_vec(&self) -> Vec<String> {
283        self.iter().map(str::to_string).collect()
284    }
285}
286
287/// Get all supported import file extensions (allocation-minimized).
288pub fn get_import_extensions_list() -> ImportExtensions {
289    let mut ai_string = crate::sys::aiString {
290        length: 0,
291        data: [0; 1024],
292    };
293
294    unsafe {
295        crate::sys::aiGetExtensionList(&mut ai_string);
296    }
297
298    if ai_string.length > 0 {
299        ImportExtensions {
300            raw: Some(crate::types::ai_string_to_string(&ai_string)),
301        }
302    } else {
303        ImportExtensions { raw: None }
304    }
305}
306
307/// Get a list of all supported import file extensions (allocates).
308pub fn get_import_extensions() -> Vec<String> {
309    get_import_extensions_list().to_vec()
310}
311
312/// Get a list of all supported export formats
313#[cfg(feature = "export")]
314pub fn get_export_formats() -> Vec<crate::exporter::ExportFormatDesc> {
315    get_export_formats_iter().collect()
316}
317
318/// Iterate supported export formats without allocating a `Vec`.
319///
320/// Each yielded item is still an owned `ExportFormatDesc` (it contains copied strings).
321#[cfg(feature = "export")]
322pub fn get_export_formats_iter() -> ExportFormatDescIterator {
323    ExportFormatDescIterator {
324        index: 0,
325        count: unsafe { sys::aiGetExportFormatCount() },
326    }
327}
328
329/// Iterator over Assimp export format descriptions.
330#[cfg(feature = "export")]
331pub struct ExportFormatDescIterator {
332    index: usize,
333    count: usize,
334}
335
336#[cfg(feature = "export")]
337impl Iterator for ExportFormatDescIterator {
338    type Item = crate::exporter::ExportFormatDesc;
339
340    fn next(&mut self) -> Option<Self::Item> {
341        while self.index < self.count {
342            let i = self.index;
343            self.index += 1;
344            unsafe {
345                let desc_ptr = sys::aiGetExportFormatDescription(i);
346                if desc_ptr.is_null() {
347                    continue;
348                }
349                let Some(desc_ref) = crate::ffi::ref_from_ptr(self, desc_ptr) else {
350                    continue;
351                };
352                let desc = crate::exporter::ExportFormatDesc::from_raw(desc_ref);
353                sys::aiReleaseExportFormatDescription(desc_ptr);
354                return Some(desc);
355            }
356        }
357        None
358    }
359
360    fn size_hint(&self) -> (usize, Option<usize>) {
361        let remaining = self.count.saturating_sub(self.index);
362        (0, Some(remaining))
363    }
364}
365
366/// Enable verbose logging for debugging
367pub fn enable_verbose_logging(enable: bool) {
368    unsafe {
369        crate::sys::aiEnableVerboseLogging(if enable { 1 } else { 0 });
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    #[test]
378    fn test_version_info() {
379        let version = version::assimp_version();
380        assert!(!version.is_empty());
381
382        let major = version::assimp_version_major();
383        let minor = version::assimp_version_minor();
384        let patch = version::assimp_version_patch();
385        let revision = version::assimp_version_revision();
386
387        // Assimp should be at least version 5.0
388        assert!(major >= 5);
389        println!(
390            "Assimp version: {}.{}.{} (revision {})",
391            major, minor, patch, revision
392        );
393    }
394
395    #[test]
396    fn test_extension_support() {
397        // These formats should definitely be supported
398        assert!(is_extension_supported("obj").unwrap());
399        assert!(is_extension_supported("fbx").unwrap());
400        assert!(is_extension_supported("dae").unwrap());
401        assert!(is_extension_supported("gltf").unwrap());
402
403        // This should not be supported
404        assert!(!is_extension_supported("xyz").unwrap());
405    }
406
407    #[test]
408    fn test_get_extensions() {
409        let extensions = get_import_extensions();
410        assert!(!extensions.is_empty());
411        assert!(extensions.contains(&".obj".to_string()));
412        println!("Supported extensions: {:?}", extensions);
413    }
414
415    #[test]
416    fn test_send_sync_traits() {
417        // This test verifies that our core types implement Send + Sync
418        // If this compiles, our unsafe implementations are working
419
420        fn assert_send_sync<T: Send + Sync>() {}
421
422        // Test core types - if these compile, Send + Sync are implemented
423        assert_send_sync::<Scene>();
424
425        // The test passes if it compiles
426        println!("✅ Core types implement Send + Sync!");
427    }
428}