modelio-rs 0.2.1

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

fn sdk_root() -> PathBuf {
    let out = Command::new("xcrun")
        .args(["--sdk", "macosx", "--show-sdk-path"])
        .output()
        .expect("xcrun");
    assert!(out.status.success());
    PathBuf::from(String::from_utf8(out.stdout).unwrap().trim())
}

fn read(path: &std::path::Path) -> String {
    std::fs::read_to_string(path).unwrap_or_else(|error| panic!("read {}: {error}", path.display()))
}

fn read_header(name: &str) -> String {
    read(&sdk_root().join(format!(
        "System/Library/Frameworks/ModelIO.framework/Headers/{name}.h"
    )))
}

fn read_bridge() -> String {
    let bridge_dir =
        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("swift-bridge/Sources/ModelIOBridge");
    let mut paths = std::fs::read_dir(&bridge_dir)
        .unwrap()
        .filter_map(Result::ok)
        .map(|entry| entry.path())
        .filter(|path| path.extension().is_some_and(|ext| ext == "swift"))
        .collect::<Vec<_>>();
    paths.sort();
    paths
        .iter()
        .map(|path| read(path))
        .collect::<Vec<_>>()
        .join("\n")
}

fn assert_contains_all(haystack: &str, needles: &[&str]) {
    for needle in needles {
        assert!(haystack.contains(needle), "missing `{needle}`");
    }
}

#[test]
fn asset_mesh_material_and_texture_surface_is_present() {
    let asset_header = read_header("MDLAsset");
    assert_contains_all(
        &asset_header,
        &[
            "canImportFileExtension:",
            "canExportFileExtension:",
            "exportAssetToURL:",
            "- (void)addObject:",
            "- (MDLObject *)objectAtIndex:",
        ],
    );

    let mesh_header = read_header("MDLMesh");
    assert_contains_all(
        &mesh_header,
        &[
            "vertexDescriptor",
            "vertexAttributeDataForAttributeNamed:",
            "initBoxWithExtent:",
            "initCylinderWithExtent:",
        ],
    );

    let material_header = read_header("MDLMaterial");
    assert_contains_all(
        &material_header,
        &[
            "initWithName:(NSString*)name scatteringFunction:",
            "materialFace",
            "floatValue",
            "color",
        ],
    );

    let texture_header = read_header("MDLTexture");
    assert_contains_all(
        &texture_header,
        &[
            "initWithURL:(NSURL*)URL name:",
            "MDLCheckerboardTexture",
            "texelDataWithTopLeftOrigin",
            "writeToURL:",
        ],
    );

    let bridge = read_bridge();
    assert_contains_all(
        &bridge,
        &[
            "mdl_asset_new_empty",
            "mdl_asset_export_to_url",
            "mdl_mesh_vertex_descriptor",
            "mdl_material_new",
            "mdl_checkerboard_texture_new",
        ],
    );
}

#[test]
fn object_light_and_camera_surface_is_present() {
    let object_header = read_header("MDLObject");
    assert_contains_all(
        &object_header,
        &[
            "@property (nonatomic, readonly, copy) NSArray<id<MDLComponent>> *components;",
            "- (void)addChild:(MDLObject *)child;",
            "- (MDLObject*)objectAtPath:",
            "@property (nonatomic) BOOL hidden;",
        ],
    );

    let light_header = read_header("MDLLight");
    assert_contains_all(
        &light_header,
        &[
            "irradianceAtPoint:",
            "@property (nonatomic, readwrite) MDLLightType lightType;",
            "@interface MDLPhysicallyPlausibleLight",
            "setColorByTemperature:",
        ],
    );

    let camera_header = read_header("MDLCamera");
    assert_contains_all(
        &camera_header,
        &["projectionMatrix", "frameBoundingBox:", "lookAt:", "rayTo:"],
    );

    let bridge = read_bridge();
    assert_contains_all(
        &bridge,
        &[
            "mdl_object_new",
            "mdl_object_add_child",
            "mdl_light_new",
            "mdl_physically_plausible_light_new",
            "mdl_camera_new",
            "mdl_camera_ray_to",
        ],
    );
}

#[test]
fn voxel_animation_and_animated_value_surface_is_present() {
    let voxel_header = read_header("MDLVoxelArray");
    assert_contains_all(
        &voxel_header,
        &[
            "initWithData:(NSData*)voxelData",
            "voxelExistsAtIndex:",
            "voxelIndices",
            "coarseMesh",
        ],
    );

    let animation_header = read_header("MDLAnimation");
    assert_contains_all(
        &animation_header,
        &[
            "@interface MDLSkeleton",
            "@interface MDLPackedJointAnimation",
            "@interface MDLAnimationBindComponent",
        ],
    );

    let animated_header = read_header("MDLAnimatedValueTypes");
    assert_contains_all(
        &animated_header,
        &[
            "@interface MDLAnimatedValue",
            "@interface MDLAnimatedScalar",
            "@interface MDLAnimatedVector3",
            "@interface MDLAnimatedQuaternion",
            "@interface MDLAnimatedMatrix4x4",
            "@interface MDLAnimatedScalarArray",
        ],
    );

    let bridge = read_bridge();
    assert_contains_all(
        &bridge,
        &[
            "mdl_voxel_array_new_with_indices",
            "mdl_packed_joint_animation_new",
            "mdl_animation_bind_component_new",
            "mdl_animated_scalar_new",
            "mdl_animated_vector3_array_new",
            "mdl_animated_quaternion_array_new",
        ],
    );
}

#[test]
fn submesh_and_vertex_attribute_surface_is_present() {
    let submesh_header = read_header("MDLSubmesh");
    assert_contains_all(
        &submesh_header,
        &[
            "@property (nonatomic, readonly, retain) id<MDLMeshBuffer> indexBuffer;",
            "indexBufferAsIndexType:",
            "@property (nonatomic, retain, nullable) MDLMaterial *material;",
        ],
    );

    let vertex_header = read_header("MDLVertexDescriptor");
    assert_contains_all(
        &vertex_header,
        &[
            "@interface MDLVertexAttribute",
            "initWithName:(NSString *)name",
            "@interface MDLVertexDescriptor",
            "attributeNamed:",
            "setPackedOffsets",
        ],
    );

    let bridge = read_bridge();
    assert_contains_all(
        &bridge,
        &[
            "mdl_submesh_index_buffer_as_type",
            "mdl_submesh_set_material",
            "mdl_vertex_attribute_new",
            "mdl_vertex_descriptor_new_copy",
            "mdl_vertex_descriptor_attribute_named",
        ],
    );
}

#[test]
fn transform_mesh_buffer_resolver_and_light_probe_surface_is_present() {
    let asset_header = read_header("MDLAsset");
    assert_contains_all(
        &asset_header,
        &[
            "@protocol MDLLightProbeIrradianceDataSource <NSObject>",
            "+ (NSArray<MDLLightProbe *> *)placeLightProbesWithDensity:",
        ],
    );

    let resolver_header = read_header("MDLAssetResolver");
    assert_contains_all(
        &resolver_header,
        &[
            "@protocol MDLAssetResolver <NSObject>",
            "@interface MDLRelativeAssetResolver",
            "@interface MDLPathAssetResolver",
            "@interface MDLBundleAssetResolver",
        ],
    );

    let transform_header = read_header("MDLTransform");
    assert_contains_all(
        &transform_header,
        &[
            "@protocol MDLTransformComponent <MDLComponent>",
            "@interface MDLTransform : NSObject <NSCopying, MDLTransformComponent>",
            "- (vector_float3)translationAtTime:",
            "- (void)setScale:(vector_float3)scale forTime:",
        ],
    );

    let transform_stack_header = read_header("MDLTransformStack");
    assert_contains_all(
        &transform_stack_header,
        &[
            "typedef NS_ENUM(NSUInteger, MDLTransformOpRotationOrder)",
            "@protocol MDLTransformOp",
            "@interface MDLTransformRotateXOp",
            "@interface MDLTransformOrientOp",
            "-(MDLTransformTranslateOp*) addTranslateOp:",
            "-(MDLAnimatedValue*) animatedValueWithName:",
        ],
    );

    let mesh_buffer_header = read_header("MDLMeshBuffer");
    assert_contains_all(
        &mesh_buffer_header,
        &[
            "@interface MDLMeshBufferMap : NSObject",
            "@interface MDLMeshBufferData : NSObject <MDLMeshBuffer>",
            "@protocol MDLMeshBufferAllocator <NSObject>",
            "@interface MDLMeshBufferDataAllocator: NSObject <MDLMeshBufferAllocator>",
            "@interface MDLMeshBufferZoneDefault : NSObject <MDLMeshBufferZone>",
        ],
    );

    let texture_header = read_header("MDLTexture");
    assert_contains_all(
        &texture_header,
        &[
            "@interface MDLSkyCubeTexture : MDLTexture",
            "@interface MDLColorSwatchTexture : MDLTexture",
            "@interface MDLNoiseTexture : MDLTexture",
            "@interface MDLNormalMapTexture : MDLTexture",
        ],
    );

    let light_header = read_header("MDLLight");
    assert_contains_all(
        &light_header,
        &[
            "@interface MDLLightProbe : MDLLight",
            "lightProbeWithTextureSize:",
            "@property (nonatomic, retain, nullable, readonly) MDLTexture *reflectiveTexture;",
        ],
    );

    let bridge = read_bridge();
    assert_contains_all(
        &bridge,
        &[
            "mdl_asset_resolver_can_resolve_named",
            "mdl_relative_asset_resolver_new",
            "mdl_mesh_buffer_allocator_new_zone",
            "mdl_mesh_buffer_zone_default_new",
            "mdl_color_swatch_texture_new_temperature_gradient",
            "mdl_normal_map_texture_new",
            "mdl_sky_cube_texture_new_with_azimuth",
            "mdl_transform_component_matrix",
            "mdl_transform_stack_add_orient_op",
            "mdl_transform_rotate_op_animated_value",
            "mdl_light_probe_new",
            "mdl_light_probe_irradiance_data_source_new",
            "mdl_asset_place_light_probes",
        ],
    );
}