bevy_boxddd 0.1.0

Bevy integration for the boxddd Rust bindings to Box3D
Documentation
use bevy_app::App;
use bevy_boxddd::prelude::*;
use bevy_math::Vec3;
use bevy_time::{TimePlugin, TimeUpdateStrategy};
use bevy_transform::components::Transform;

fn physics_app() -> App {
    let mut app = App::new();
    app.add_plugins(TimePlugin)
        .insert_resource(TimeUpdateStrategy::FixedTimesteps(1))
        .add_plugins(BoxdddPhysicsPlugin::new(BoxdddPhysicsSettings::default()));
    app
}

fn run_fixed_frames(app: &mut App, count: usize) {
    for _ in 0..count {
        app.update();
    }
}

fn static_sphere(app: &mut App, position: Vec3, radius: f32) -> bevy_ecs::entity::Entity {
    app.world_mut()
        .spawn((
            RigidBody::Static,
            Collider::sphere(radius),
            Transform::from_translation(position),
        ))
        .id()
}

#[test]
fn closest_ray_hit_maps_to_nearest_bevy_entity() {
    let mut app = physics_app();
    let near = static_sphere(&mut app, Vec3::new(0.0, 0.0, 0.0), 0.35);
    let far = static_sphere(&mut app, Vec3::new(2.0, 0.0, 0.0), 0.35);

    run_fixed_frames(&mut app, 2);

    let context = app.world().get_non_send::<BoxdddPhysicsContext>().unwrap();
    let hit = cast_ray_closest(
        context,
        Vec3::new(-2.0, 0.0, 0.0),
        Vec3::new(5.0, 0.0, 0.0),
        boxddd::QueryFilter::default(),
    )
    .unwrap()
    .unwrap();

    assert_eq!(hit.entity, Some(near));
    assert_ne!(hit.entity, Some(far));
}

#[test]
fn ray_cast_returns_all_mapped_hits_along_ray() {
    let mut app = physics_app();
    let near = static_sphere(&mut app, Vec3::new(0.0, 0.0, 0.0), 0.35);
    let far = static_sphere(&mut app, Vec3::new(2.0, 0.0, 0.0), 0.35);

    run_fixed_frames(&mut app, 2);

    let context = app.world().get_non_send::<BoxdddPhysicsContext>().unwrap();
    let hits = cast_ray(
        context,
        Vec3::new(-2.0, 0.0, 0.0),
        Vec3::new(5.0, 0.0, 0.0),
        boxddd::QueryFilter::default(),
    )
    .unwrap();
    let entities = hits.iter().map(|hit| hit.entity).collect::<Vec<_>>();

    assert!(entities.contains(&Some(near)), "hits: {hits:?}");
    assert!(entities.contains(&Some(far)), "hits: {hits:?}");
}

#[test]
fn ray_miss_returns_none() {
    let mut app = physics_app();
    static_sphere(&mut app, Vec3::new(0.0, 0.0, 0.0), 0.35);

    run_fixed_frames(&mut app, 2);

    let context = app.world().get_non_send::<BoxdddPhysicsContext>().unwrap();
    let hit = cast_ray_closest(
        context,
        Vec3::new(-2.0, 4.0, 0.0),
        Vec3::new(5.0, 0.0, 0.0),
        boxddd::QueryFilter::default(),
    )
    .unwrap();

    assert!(hit.is_none());
}

#[test]
fn query_filter_mask_excludes_shapes() {
    let mut app = physics_app();
    let included = app
        .world_mut()
        .spawn((
            RigidBody::Static,
            Collider::sphere(0.35),
            Transform::from_xyz(0.0, 0.0, 0.0),
            PhysicsMaterial {
                filter: boxddd::Filter {
                    category_bits: 0b10,
                    mask_bits: u64::MAX,
                    group_index: 0,
                },
                ..Default::default()
            },
        ))
        .id();

    run_fixed_frames(&mut app, 2);

    let context = app.world().get_non_send::<BoxdddPhysicsContext>().unwrap();
    let hits = overlap_aabb(
        context,
        Vec3::new(-1.0, -1.0, -1.0),
        Vec3::new(1.0, 1.0, 1.0),
        boxddd::QueryFilter::default().mask_bits(0b10),
    )
    .unwrap();
    assert!(hits.iter().any(|hit| hit.entity == Some(included)));

    let excluded = overlap_aabb(
        context,
        Vec3::new(-1.0, -1.0, -1.0),
        Vec3::new(1.0, 1.0, 1.0),
        boxddd::QueryFilter::default().mask_bits(0b100),
    )
    .unwrap();
    assert!(excluded.is_empty());
}