boxddd 0.2.0

Safe, ergonomic Rust bindings for Box3D
Documentation
use boxddd::{BodyDef, BodyType, BoxHull, Error, ShapeDef, Sphere, Vec3, World, WorldDef};
use std::sync::{
    Arc,
    atomic::{AtomicBool, AtomicUsize, Ordering},
};

fn contact_world() -> (World, boxddd::ShapeId, boxddd::ShapeId) {
    let mut world = World::new(
        WorldDef::builder()
            .gravity(Vec3::new(0.0, -10.0, 0.0))
            .build(),
    )
    .unwrap();
    let ground = world.create_body(BodyDef::builder().position([0.0, -0.5, 0.0]).build());
    let ground_shape = world.create_hull_shape(
        ground,
        &ShapeDef::builder().enable_custom_filtering(true).build(),
        &BoxHull::new(10.0, 0.5, 10.0),
    );
    let sphere = world.create_body(
        BodyDef::builder()
            .body_type(BodyType::Dynamic)
            .position([0.0, 2.0, 0.0])
            .build(),
    );
    let sphere_shape = world.create_sphere_shape(
        sphere,
        &ShapeDef::builder()
            .density(1.0)
            .enable_contact_events(true)
            .enable_pre_solve_events(true)
            .enable_custom_filtering(true)
            .build(),
        &Sphere::new([0.0, 0.0, 0.0], 0.5),
    );
    (world, ground_shape, sphere_shape)
}

#[test]
fn custom_filter_can_disable_contacts() {
    let (mut world, ground_shape, sphere_shape) = contact_world();
    let calls = Arc::new(AtomicUsize::new(0));
    let saw_expected_pair = Arc::new(AtomicBool::new(false));

    world.set_custom_filter({
        let calls = Arc::clone(&calls);
        let saw_expected_pair = Arc::clone(&saw_expected_pair);
        move |a, b| {
            calls.fetch_add(1, Ordering::Relaxed);
            if [a, b].contains(&ground_shape) && [a, b].contains(&sphere_shape) {
                saw_expected_pair.store(true, Ordering::Relaxed);
            }
            false
        }
    });

    for _ in 0..90 {
        world.step(1.0 / 60.0, 4);
    }

    assert!(calls.load(Ordering::Relaxed) > 0);
    assert!(saw_expected_pair.load(Ordering::Relaxed));
    assert!(world.contact_events().begin.is_empty());

    world.clear_custom_filter();
}

#[test]
fn pre_solve_callback_is_invoked_and_can_disable_contacts_for_step() {
    let (mut world, ground_shape, sphere_shape) = contact_world();
    let calls = Arc::new(AtomicUsize::new(0));
    let saw_expected_pair = Arc::new(AtomicBool::new(false));

    world.set_pre_solve({
        let calls = Arc::clone(&calls);
        let saw_expected_pair = Arc::clone(&saw_expected_pair);
        move |a, b, point, normal| {
            calls.fetch_add(1, Ordering::Relaxed);
            if [a, b].contains(&ground_shape) && [a, b].contains(&sphere_shape) {
                saw_expected_pair.store(true, Ordering::Relaxed);
            }
            assert!(point.validate().is_ok());
            assert!(normal.is_valid());
            false
        }
    });

    for _ in 0..90 {
        world.step(1.0 / 60.0, 4);
    }

    assert!(calls.load(Ordering::Relaxed) > 0);
    assert!(saw_expected_pair.load(Ordering::Relaxed));
    world.clear_pre_solve();
}

#[test]
fn callback_panic_is_caught_and_reported_after_step() {
    let (mut world, _, _) = contact_world();
    world.set_custom_filter(|_, _| panic!("custom filter panic"));

    let mut result = Ok(());
    for _ in 0..90 {
        result = world.try_step(1.0 / 60.0, 4);
        if result == Err(Error::CallbackPanicked) {
            break;
        }
    }

    assert_eq!(result, Err(Error::CallbackPanicked));
}

#[test]
fn material_mix_callbacks_receive_user_material_ids() {
    let mut world = World::new(
        WorldDef::builder()
            .gravity(Vec3::new(0.0, -10.0, 0.0))
            .build(),
    )
    .unwrap();
    let friction_calls = Arc::new(AtomicUsize::new(0));
    let restitution_calls = Arc::new(AtomicUsize::new(0));
    let saw_materials = Arc::new(AtomicBool::new(false));

    world.set_friction_callback({
        let friction_calls = Arc::clone(&friction_calls);
        let saw_materials = Arc::clone(&saw_materials);
        move |a, b| {
            friction_calls.fetch_add(1, Ordering::Relaxed);
            if [a.user_material_id, b.user_material_id].contains(&7)
                && [a.user_material_id, b.user_material_id].contains(&11)
            {
                saw_materials.store(true, Ordering::Relaxed);
            }
            0.25
        }
    });
    world.set_restitution_callback({
        let restitution_calls = Arc::clone(&restitution_calls);
        move |a, b| {
            restitution_calls.fetch_add(1, Ordering::Relaxed);
            a.coefficient.max(b.coefficient)
        }
    });

    let ground = world.create_body(BodyDef::builder().position([0.0, -0.5, 0.0]).build());
    world.create_hull_shape(
        ground,
        &ShapeDef::builder()
            .friction(0.4)
            .user_material_id(11)
            .build(),
        &BoxHull::new(10.0, 0.5, 10.0),
    );
    let sphere = world.create_body(
        BodyDef::builder()
            .body_type(BodyType::Dynamic)
            .position([0.0, 2.0, 0.0])
            .build(),
    );
    world.create_sphere_shape(
        sphere,
        &ShapeDef::builder()
            .density(1.0)
            .friction(0.9)
            .restitution(0.1)
            .user_material_id(7)
            .build(),
        &Sphere::new([0.0, 0.0, 0.0], 0.5),
    );

    for _ in 0..90 {
        world.step(1.0 / 60.0, 4);
    }

    assert!(friction_calls.load(Ordering::Relaxed) > 0);
    assert!(restitution_calls.load(Ordering::Relaxed) > 0);
    assert!(saw_materials.load(Ordering::Relaxed));

    world.clear_friction_callback();
    world.clear_restitution_callback();
}

#[test]
fn material_mix_callback_panic_is_reported_after_step() {
    let (mut world, _, _) = contact_world();
    world.set_friction_callback(|_, _| panic!("friction mix panic"));

    let mut result = Ok(());
    for _ in 0..90 {
        result = world.try_step(1.0 / 60.0, 4);
        if result == Err(Error::CallbackPanicked) {
            break;
        }
    }

    assert_eq!(result, Err(Error::CallbackPanicked));
    world.clear_friction_callback();
}

#[test]
fn non_finite_material_mix_returns_fallback_without_panic() {
    let (mut world, _, _) = contact_world();
    let friction_calls = Arc::new(AtomicUsize::new(0));
    let restitution_calls = Arc::new(AtomicUsize::new(0));

    world.set_friction_callback({
        let friction_calls = Arc::clone(&friction_calls);
        move |_, _| {
            friction_calls.fetch_add(1, Ordering::Relaxed);
            f32::NAN
        }
    });
    world.set_restitution_callback({
        let restitution_calls = Arc::clone(&restitution_calls);
        move |_, _| {
            restitution_calls.fetch_add(1, Ordering::Relaxed);
            f32::INFINITY
        }
    });

    for _ in 0..90 {
        world.try_step(1.0 / 60.0, 4).unwrap();
    }

    assert!(friction_calls.load(Ordering::Relaxed) > 0);
    assert!(restitution_calls.load(Ordering::Relaxed) > 0);
    world.clear_friction_callback();
    world.clear_restitution_callback();
}

#[test]
fn callback_registration_respects_callback_guard() {
    let mut world = World::new(WorldDef::default()).unwrap();
    let _guard = boxddd::__private::enter_callback_guard_for_test();

    assert_eq!(
        world.try_set_custom_filter(|_, _| true).unwrap_err(),
        Error::InCallback
    );
    assert_eq!(
        world.try_set_pre_solve(|_, _, _, _| true).unwrap_err(),
        Error::InCallback
    );
    assert_eq!(
        world
            .try_set_friction_callback(|a, _| a.coefficient)
            .unwrap_err(),
        Error::InCallback
    );
    assert_eq!(
        world
            .try_set_restitution_callback(|a, _| a.coefficient)
            .unwrap_err(),
        Error::InCallback
    );
}