boxdd 0.6.0

Safe, ergonomic Rust bindings for Box2D v3
Documentation
use crate::Vec2;
use crate::error::Error;
#[cfg(not(target_arch = "wasm32"))]
use crate::{DebugDraw, DebugDrawCmd, DebugDrawOptions, ExplosionDef, HexColor, Position};
use std::cell::Cell;
use std::rc::Rc;

struct GravityConversionProbe {
    converted: Rc<Cell<bool>>,
}

impl From<GravityConversionProbe> for Vec2 {
    fn from(probe: GravityConversionProbe) -> Self {
        probe.converted.set(true);
        Vec2::ZERO
    }
}

#[cfg(not(target_arch = "wasm32"))]
#[derive(Default)]
struct CountingDebugDraw {
    calls: usize,
}

#[cfg(not(target_arch = "wasm32"))]
impl DebugDraw for CountingDebugDraw {
    fn draw_point(&mut self, _p: Position, _size: f32, _color: HexColor) {
        self.calls += 1;
    }
}

#[cfg(not(target_arch = "wasm32"))]
fn debug_draw_sentinel() -> DebugDrawCmd {
    DebugDrawCmd::Point {
        p: Position::ZERO,
        size: 17.0,
        color: HexColor::GREEN,
    }
}

#[test]
fn world_entries_reject_busy_worlds_before_access_or_mutation() {
    let mut world = crate::Foundation::initialize_default()
        .unwrap()
        .create_world(
            crate::Foundation::get()
                .expect("Foundation must be initialized before constructing a WorldDef")
                .world_def(),
        )
        .unwrap();
    world.set_user_data(String::from("original")).unwrap();

    let recording = world.core().begin_recording_activity().unwrap();

    assert_eq!(world.gravity().unwrap_err(), Error::WorldBusy);
    assert_eq!(
        world
            .set_user_data(String::from("replacement"))
            .unwrap_err(),
        Error::WorldBusy
    );
    assert_eq!(world.clear_user_data().unwrap_err(), Error::WorldBusy);
    assert_eq!(
        world.take_user_data::<String>().unwrap_err(),
        Error::WorldBusy
    );

    drop(recording);
    assert_eq!(
        world
            .with_user_data::<String, _>(Clone::clone)
            .unwrap()
            .as_deref(),
        Some("original")
    );

    let restoring = world.core().begin_restore_activity().unwrap();
    assert_eq!(world.gravity().unwrap_err(), Error::WorldBusy);
    drop(restoring);
}

#[test]
fn object_capability_acquisition_gates_activity_before_identity_checks() {
    let mut world = crate::Foundation::initialize_default()
        .unwrap()
        .create_world(
            crate::Foundation::get()
                .expect("Foundation must be initialized before constructing a WorldDef")
                .world_def(),
        )
        .unwrap();
    let live = world
        .create_body(
            crate::Foundation::get()
                .expect("Foundation must be initialized before constructing a BodyDef")
                .body_def(),
        )
        .unwrap();
    let stale = world
        .create_body(
            crate::Foundation::get()
                .expect("Foundation must be initialized before constructing a BodyDef")
                .body_def(),
        )
        .unwrap();
    world.body(stale).unwrap().destroy().unwrap();

    let mut foreign_world = crate::Foundation::initialize_default()
        .unwrap()
        .create_world(
            crate::Foundation::get()
                .expect("Foundation must be initialized before constructing a WorldDef")
                .world_def(),
        )
        .unwrap();
    let foreign = foreign_world
        .create_body(
            crate::Foundation::get()
                .expect("Foundation must be initialized before constructing a BodyDef")
                .body_def(),
        )
        .unwrap();

    let recording = world.core().begin_recording_activity().unwrap();
    assert_eq!(world.body(stale).err().unwrap(), Error::WorldBusy);
    assert_eq!(world.body(foreign).err().unwrap(), Error::WorldBusy);
    drop(recording);

    assert!(
        world
            .body(live)
            .unwrap()
            .is_contact_recycling_enabled()
            .unwrap()
    );

    world.core().poison();
    assert_eq!(world.body(stale).err().unwrap(), Error::WorldPoisoned);
    assert_eq!(world.body(foreign).err().unwrap(), Error::WorldPoisoned);
}

#[test]
fn runtime_control_entries_gate_before_validation_and_conversion() {
    let mut world = crate::Foundation::initialize_default()
        .unwrap()
        .create_world(
            crate::Foundation::get()
                .expect("Foundation must be initialized before constructing a WorldDef")
                .world_def(),
        )
        .unwrap();
    let original_gravity = world.gravity().unwrap();
    let converted = Rc::new(Cell::new(false));

    let recording = world.core().begin_recording_activity().unwrap();

    assert_eq!(world.step(f32::NAN, 0).unwrap_err(), Error::WorldBusy);
    assert_eq!(
        world
            .set_gravity(GravityConversionProbe {
                converted: Rc::clone(&converted),
            })
            .unwrap_err(),
        Error::WorldBusy
    );
    assert!(!converted.get());
    assert_eq!(world.gravity().unwrap_err(), Error::WorldBusy);
    assert_eq!(world.counters().unwrap_err(), Error::WorldBusy);
    assert_eq!(world.enable_sleeping(false).unwrap_err(), Error::WorldBusy);
    assert_eq!(
        world
            .set_contact_tuning(f32::NAN, f32::NAN, f32::NAN)
            .unwrap_err(),
        Error::WorldBusy
    );
    assert_eq!(world.is_continuous_enabled().unwrap_err(), Error::WorldBusy);

    drop(recording);
    assert_eq!(world.gravity().unwrap(), original_gravity);
}

#[test]
fn callback_entries_leave_host_and_material_registries_unchanged_when_busy() {
    let mut world = crate::Foundation::initialize_default()
        .unwrap()
        .create_world(
            crate::Foundation::get()
                .expect("Foundation must be initialized before constructing a WorldDef")
                .world_def(),
        )
        .unwrap();
    let recording = world.core().begin_recording_activity().unwrap();

    assert_eq!(
        world.set_custom_filter(|_, _| true).unwrap_err(),
        Error::WorldBusy
    );
    assert!(world.core().custom_filter.lock().unwrap().is_none());
    assert_eq!(
        world
            .set_friction_callback(crate::MixerId::from_bytes([0x51; 32]), |a, b| {
                a.coefficient.max(b.coefficient)
            })
            .unwrap_err(),
        Error::WorldBusy
    );
    assert!(
        world
            .core()
            .material_mix
            .lock()
            .unwrap()
            .slot_for_test()
            .is_none()
    );

    drop(recording);
    world
        .set_friction_callback(crate::MixerId::from_bytes([0x51; 32]), |a, b| {
            a.coefficient.max(b.coefficient)
        })
        .unwrap();
    let slot = world
        .core()
        .material_mix
        .lock()
        .unwrap()
        .slot_for_test()
        .unwrap();
    assert_eq!(
        world.core().material_mix.lock().unwrap().presence(),
        (true, false)
    );

    let restoring = world.core().begin_restore_activity().unwrap();
    assert_eq!(
        world.clear_friction_callback().unwrap_err(),
        Error::WorldBusy
    );
    assert_eq!(
        world.core().material_mix.lock().unwrap().slot_for_test(),
        Some(slot)
    );
    assert_eq!(
        world.core().material_mix.lock().unwrap().presence(),
        (true, false)
    );

    drop(restoring);
    world.clear_friction_callback().unwrap();
    assert!(
        world
            .core()
            .material_mix
            .lock()
            .unwrap()
            .slot_for_test()
            .is_none()
    );
}

#[test]
#[cfg(not(target_arch = "wasm32"))]
fn debug_draw_and_explosion_leave_outputs_untouched_when_busy() {
    let mut world = crate::Foundation::initialize_default()
        .unwrap()
        .create_world(
            crate::Foundation::get()
                .expect("Foundation must be initialized before constructing a WorldDef")
                .world_def(),
        )
        .unwrap();
    let mut commands = vec![debug_draw_sentinel()];
    let mut drawer = CountingDebugDraw::default();
    let explosion = ExplosionDef::new();

    let recording = world.core().begin_recording_activity().unwrap();

    assert_eq!(
        world
            .debug_draw_collect_into(&mut commands, DebugDrawOptions::default())
            .unwrap_err(),
        Error::WorldBusy
    );
    assert!(matches!(
        commands.as_slice(),
        [DebugDrawCmd::Point { size: 17.0, .. }]
    ));
    assert_eq!(
        world
            .debug_draw(&mut drawer, DebugDrawOptions::default())
            .unwrap_err(),
        Error::WorldBusy
    );
    assert_eq!(drawer.calls, 0);
    assert_eq!(world.explode(&explosion).unwrap_err(), Error::WorldBusy);

    drop(recording);
}

#[test]
#[cfg(not(target_arch = "wasm32"))]
fn world_entries_reject_poisoned_worlds() {
    let mut world = crate::Foundation::initialize_default()
        .unwrap()
        .create_world(
            crate::Foundation::get()
                .expect("Foundation must be initialized before constructing a WorldDef")
                .world_def(),
        )
        .unwrap();
    let mut drawer = CountingDebugDraw::default();
    let explosion = ExplosionDef::new();
    world.core().poison();

    assert_eq!(world.has_user_data().unwrap_err(), Error::WorldPoisoned);
    assert_eq!(world.gravity().unwrap_err(), Error::WorldPoisoned);
    assert_eq!(world.step(1.0 / 60.0, 1).unwrap_err(), Error::WorldPoisoned);
    assert_eq!(
        world.set_custom_filter(|_, _| true).unwrap_err(),
        Error::WorldPoisoned
    );
    assert_eq!(
        world
            .debug_draw(&mut drawer, DebugDrawOptions::default())
            .unwrap_err(),
        Error::WorldPoisoned
    );
    assert_eq!(world.explode(&explosion).unwrap_err(), Error::WorldPoisoned);
}

#[test]
#[cfg(not(target_arch = "wasm32"))]
fn callback_error_precedes_world_activity_and_argument_errors() {
    let mut world = crate::Foundation::initialize_default()
        .unwrap()
        .create_world(
            crate::Foundation::get()
                .expect("Foundation must be initialized before constructing a WorldDef")
                .world_def(),
        )
        .unwrap();
    let converted = Rc::new(Cell::new(false));
    let mut commands = vec![debug_draw_sentinel()];
    let explosion = ExplosionDef::new();
    let recording = world.core().begin_recording_activity().unwrap();

    {
        let _guard = crate::core::callback_state::CallbackGuard::enter();
        assert_eq!(world.gravity().unwrap_err(), Error::InCallback);
        assert_eq!(world.step(f32::NAN, 0).unwrap_err(), Error::InCallback);
        assert_eq!(
            world
                .set_gravity(GravityConversionProbe {
                    converted: Rc::clone(&converted),
                })
                .unwrap_err(),
            Error::InCallback
        );
        assert!(!converted.get());
        assert_eq!(
            world.set_user_data(String::from("blocked")).unwrap_err(),
            Error::InCallback
        );
        assert_eq!(
            world.set_custom_filter(|_, _| true).unwrap_err(),
            Error::InCallback
        );
        assert_eq!(
            world
                .debug_draw_collect_into(&mut commands, DebugDrawOptions::default())
                .unwrap_err(),
            Error::InCallback
        );
        assert_eq!(commands.len(), 1);
        assert_eq!(world.explode(&explosion).unwrap_err(), Error::InCallback);
    }

    drop(recording);
}

#[test]
fn creating_a_world_from_a_callback_returns_before_native_creation() {
    let _guard = crate::core::callback_state::CallbackGuard::enter();
    assert!(matches!(
        crate::Foundation::initialize_default()
            .unwrap()
            .create_world(
                crate::Foundation::get()
                    .expect("Foundation must be initialized before constructing a WorldDef")
                    .world_def()
            ),
        Err(Error::InCallback)
    ));
}