modelio-rs 0.2.4

Safe Rust bindings for Apple's ModelIO framework — assets, meshes, materials, lights, cameras, voxels, textures, and animation on macOS
Documentation
use std::ptr;

use crate::asset::Asset;
use crate::error::Result;
use crate::ffi;
use crate::handle::ObjectHandle;
use crate::light::Light;
use crate::object::Object;
use crate::texture::Texture;
use crate::types::{BoundingBox, ProbePlacement};
use crate::util::required_handle;

type IrradianceCallbackFn = dyn Fn([f32; 3]) -> Vec<f32> + Send + Sync + 'static;

struct IrradianceCallback {
    callback: Box<IrradianceCallbackFn>,
}

#[no_mangle]
pub extern "C" fn mdlx_light_probe_irradiance_data_source_coefficients(
    context: *mut core::ffi::c_void,
    x: f32,
    y: f32,
    z: f32,
    out_values: *mut f32,
    capacity: u64,
) -> u64 {
    let Some(context) = (!context.is_null()).then_some(context.cast::<IrradianceCallback>()) else {
        return 0;
    };
    // SAFETY: The unsafe operation is valid in this context.
    let values = (unsafe { &*context }.callback)([x, y, z]);
    let total = values.len();
    if out_values.is_null() || capacity == 0 {
        return total as u64;
    }
    let write_count = total.min(capacity as usize);
    // SAFETY: The unsafe operation is valid in this context.
    unsafe { out_values.copy_from_nonoverlapping(values.as_ptr(), write_count) };
    total as u64
}

#[no_mangle]
pub extern "C" fn mdlx_light_probe_irradiance_data_source_release(context: *mut core::ffi::c_void) {
    if context.is_null() {
        return;
    }
    // SAFETY: The unsafe operation is valid in this context.
    unsafe { drop(Box::from_raw(context.cast::<IrradianceCallback>())) };
}

fn release_callback_context(context: *mut core::ffi::c_void) {
    mdlx_light_probe_irradiance_data_source_release(context);
}

fn array_objects<T, F>(
    array_ptr: *mut core::ffi::c_void,
    context: &'static str,
    mut map: F,
) -> Result<Vec<T>>
where
    F: FnMut(ObjectHandle) -> T,
{
    let array = required_handle(array_ptr, context)?;
    // SAFETY: ObjectHandle wraps a valid opaque pointer from Swift; FFI function accepts it safely.
    let count = unsafe { ffi::mdl_array_count(array.as_ptr()) as usize };
    let mut values = Vec::with_capacity(count);
    for index in 0..count {
        // SAFETY: ObjectHandle wraps a valid opaque pointer from Swift; FFI function accepts it safely.
        let ptr = unsafe { ffi::mdl_array_object_at(array.as_ptr(), index as u64) };
        // SAFETY: The unsafe operation is valid in this context.
        if let Some(handle) = unsafe { ObjectHandle::from_retained_ptr(ptr) } {
            values.push(map(handle));
        }
    }
    Ok(values)
}

#[derive(Debug, Clone)]
/// Wraps the corresponding Model I/O light probe counterpart.
pub struct LightProbe {
    handle: ObjectHandle,
}

impl LightProbe {
    /// Builds this wrapper from the retained handle of the wrapped Model I/O light probe counterpart.
    pub(crate) fn from_handle(handle: ObjectHandle) -> Self {
        Self { handle }
    }

    /// Wraps the corresponding Model I/O initializer for the wrapped Model I/O light probe counterpart.
    pub fn new(
        reflective_texture: Option<&Texture>,
        irradiance_texture: Option<&Texture>,
    ) -> Result<Self> {
        let mut out_probe = ptr::null_mut();
        let mut out_error = ptr::null_mut();
        // SAFETY: The unsafe operation is valid in this context.
        let status = unsafe {
            ffi::mdl_light_probe_new(
                reflective_texture.map_or(ptr::null_mut(), Texture::as_ptr),
                irradiance_texture.map_or(ptr::null_mut(), Texture::as_ptr),
                &mut out_probe,
                &mut out_error,
            )
        };
        crate::util::status_result(status, out_error)?;
        Ok(Self::from_handle(required_handle(
            out_probe,
            "MDLLightProbe",
        )?))
    }

    /// Calls the corresponding Model I/O method on the wrapped Model I/O light probe counterpart.
    pub fn generate_spherical_harmonics_from_irradiance(&self, level: usize) {
        // SAFETY: The unsafe operation is valid in this context.
        unsafe {
            ffi::mdl_light_probe_generate_spherical_harmonics_from_irradiance(
                self.handle.as_ptr(),
                level as u64,
            );
        }
    }

    #[must_use]
    /// Calls the corresponding Model I/O method on the wrapped Model I/O light probe counterpart.
    pub fn reflective_texture(&self) -> Option<Texture> {
        // SAFETY: ObjectHandle wraps a valid opaque pointer from Swift; FFI function accepts it safely.
        let ptr = unsafe { ffi::mdl_light_probe_reflective_texture(self.handle.as_ptr()) };
        // SAFETY: The unsafe operation is valid in this context.
        unsafe { ObjectHandle::from_retained_ptr(ptr) }.map(Texture::from_handle)
    }

    #[must_use]
    /// Calls the corresponding Model I/O method on the wrapped Model I/O light probe counterpart.
    pub fn irradiance_texture(&self) -> Option<Texture> {
        // SAFETY: ObjectHandle wraps a valid opaque pointer from Swift; FFI function accepts it safely.
        let ptr = unsafe { ffi::mdl_light_probe_irradiance_texture(self.handle.as_ptr()) };
        // SAFETY: The unsafe operation is valid in this context.
        unsafe { ObjectHandle::from_retained_ptr(ptr) }.map(Texture::from_handle)
    }

    #[must_use]
    /// Calls the corresponding Model I/O method on the wrapped Model I/O light probe counterpart.
    pub fn spherical_harmonics_level(&self) -> usize {
        // SAFETY: ObjectHandle wraps a valid opaque pointer from Swift; FFI function accepts it safely.
        unsafe { ffi::mdl_light_probe_spherical_harmonics_level(self.handle.as_ptr()) as usize }
    }

    #[must_use]
    /// Calls the corresponding Model I/O method on the wrapped Model I/O light probe counterpart.
    pub fn spherical_harmonics_coefficients(&self) -> Vec<f32> {
        // SAFETY: The unsafe operation is valid in this context.
        let count = unsafe {
            ffi::mdl_light_probe_spherical_harmonics_coefficient_count(self.handle.as_ptr())
                as usize
        };
        let mut values = vec![0.0_f32; count];
        if values.is_empty() {
            return values;
        }
        // SAFETY: The unsafe operation is valid in this context.
        let written = unsafe {
            ffi::mdl_light_probe_copy_spherical_harmonics_coefficients(
                self.handle.as_ptr(),
                values.as_mut_ptr(),
                values.len() as u64,
            )
        } as usize;
        values.truncate(written);
        values
    }

    #[must_use]
    /// Calls the corresponding Model I/O method on the wrapped Model I/O light probe counterpart.
    pub fn as_light(&self) -> Light {
        Light::from_handle(self.handle.clone())
    }

    #[must_use]
    /// Calls the corresponding Model I/O method on the wrapped Model I/O light probe counterpart.
    pub fn as_object(&self) -> Object {
        Object::from_handle(self.handle.clone())
    }
}

#[derive(Debug, Clone)]
/// Wraps the corresponding Model I/O light probe irradiance data source counterpart.
pub struct LightProbeIrradianceDataSource {
    handle: ObjectHandle,
}

impl LightProbeIrradianceDataSource {
    /// Builds this wrapper from the retained handle of the wrapped Model I/O light probe irradiance data source counterpart.
    pub(crate) fn from_handle(handle: ObjectHandle) -> Self {
        Self { handle }
    }

    /// Returns the opaque pointer used to call the wrapped Model I/O light probe irradiance data source counterpart.
    pub(crate) fn as_ptr(&self) -> *mut core::ffi::c_void {
        self.handle.as_ptr()
    }

    /// Calls the corresponding Model I/O method on the wrapped Model I/O light probe irradiance data source counterpart.
    pub fn new<F>(
        bounding_box: BoundingBox,
        spherical_harmonics_level: usize,
        coefficients_at_position: F,
    ) -> Result<Self>
    where
        F: Fn([f32; 3]) -> Vec<f32> + Send + Sync + 'static,
    {
        let callback = Box::new(IrradianceCallback {
            callback: Box::new(coefficients_at_position),
        });
        let callback_ptr = Box::into_raw(callback).cast::<core::ffi::c_void>();
        let mut out_data_source = ptr::null_mut();
        let mut out_error = ptr::null_mut();
        // SAFETY: The unsafe operation is valid in this context.
        let status = unsafe {
            ffi::mdl_light_probe_irradiance_data_source_new(
                bounding_box.min[0],
                bounding_box.min[1],
                bounding_box.min[2],
                bounding_box.max[0],
                bounding_box.max[1],
                bounding_box.max[2],
                spherical_harmonics_level as u64,
                callback_ptr,
                &mut out_data_source,
                &mut out_error,
            )
        };
        if let Err(error) = crate::util::status_result(status, out_error) {
            release_callback_context(callback_ptr);
            return Err(error);
        }
        match required_handle(out_data_source, "MDLLightProbeIrradianceDataSource") {
            Ok(handle) => Ok(Self::from_handle(handle)),
            Err(error) => {
                release_callback_context(callback_ptr);
                Err(error)
            }
        }
    }

    #[must_use]
    /// Calls the corresponding Model I/O method on the wrapped Model I/O light probe irradiance data source counterpart.
    pub fn bounding_box(&self) -> BoundingBox {
        let mut min = [0.0_f32; 3];
        let mut max = [0.0_f32; 3];
        // SAFETY: The unsafe operation is valid in this context.
        unsafe {
            ffi::mdl_light_probe_irradiance_data_source_bounding_box(
                self.handle.as_ptr(),
                &mut min[0],
                &mut min[1],
                &mut min[2],
                &mut max[0],
                &mut max[1],
                &mut max[2],
            );
        }
        BoundingBox { min, max }
    }

    /// Calls the corresponding Model I/O method on the wrapped Model I/O light probe irradiance data source counterpart.
    pub fn set_bounding_box(&self, bounding_box: BoundingBox) {
        // SAFETY: The unsafe operation is valid in this context.
        unsafe {
            ffi::mdl_light_probe_irradiance_data_source_set_bounding_box(
                self.handle.as_ptr(),
                bounding_box.min[0],
                bounding_box.min[1],
                bounding_box.min[2],
                bounding_box.max[0],
                bounding_box.max[1],
                bounding_box.max[2],
            );
        }
    }

    #[must_use]
    /// Calls the corresponding Model I/O method on the wrapped Model I/O light probe irradiance data source counterpart.
    pub fn spherical_harmonics_level(&self) -> usize {
        // SAFETY: The unsafe operation is valid in this context.
        unsafe {
            ffi::mdl_light_probe_irradiance_data_source_spherical_harmonics_level(
                self.handle.as_ptr(),
            ) as usize
        }
    }

    /// Calls the corresponding Model I/O method on the wrapped Model I/O light probe irradiance data source counterpart.
    pub fn set_spherical_harmonics_level(&self, spherical_harmonics_level: usize) {
        // SAFETY: The unsafe operation is valid in this context.
        unsafe {
            ffi::mdl_light_probe_irradiance_data_source_set_spherical_harmonics_level(
                self.handle.as_ptr(),
                spherical_harmonics_level as u64,
            );
        }
    }
}

impl Asset {
    /// Calls the corresponding Model I/O method on the wrapped Model I/O asset counterpart.
    pub fn place_light_probes(
        density: f32,
        heuristic: ProbePlacement,
        data_source: &LightProbeIrradianceDataSource,
    ) -> Result<Vec<LightProbe>> {
        // SAFETY: The unsafe operation is valid in this context.
        let ptr = unsafe {
            ffi::mdl_asset_place_light_probes(density, heuristic.as_raw(), data_source.as_ptr())
        };
        if ptr.is_null() {
            return Ok(Vec::new());
        }
        array_objects(ptr, "MDLAsset light probes", LightProbe::from_handle)
    }
}