bevy_boxddd 0.2.0

Bevy integration for the boxddd Rust bindings to Box3D
Documentation
use bevy::prelude::*;
use bevy::time::Real;
use bevy_boxddd::prelude::*;
use bevy_egui::input::EguiWantsInput;

const MAX_PICK_DISTANCE: f32 = 100.0;
const MIN_DRAG_DEPTH: f32 = 0.35;
const MAX_THROW_SPEED: f32 = 35.0;

#[derive(Resource, Default, Debug)]
pub(crate) struct PhysicsDragState {
    active: Option<DragTarget>,
}

#[derive(Copy, Clone, Debug)]
struct DragTarget {
    entity: Entity,
    body_id: boxddd::BodyId,
    previous_type: boxddd::BodyType,
    rotation: boxddd::Quat,
    depth: f32,
    grab_offset: Vec3,
    last_position: Vec3,
    last_velocity: Vec3,
}

pub(crate) fn update_physics_drag(
    buttons: Res<ButtonInput<MouseButton>>,
    camera: Single<(&Camera, &GlobalTransform)>,
    window: Single<&Window>,
    egui_input: Option<Res<EguiWantsInput>>,
    real_time: Res<Time<Real>>,
    mut context: NonSendMut<BoxdddPhysicsContext>,
    mut drag: ResMut<PhysicsDragState>,
    mut bodies: Query<(&BoxdddBody, &mut Transform)>,
) {
    if buttons.just_released(MouseButton::Left) || !buttons.pressed(MouseButton::Left) {
        if let Some(target) = drag.active.take() {
            finish_drag(target, &mut context);
        }
        return;
    }

    let Some(ray) = cursor_ray(*camera, *window) else {
        return;
    };

    if let Some(target) = drag.active {
        drag.active = update_active_drag(target, ray, &real_time, &mut context, &mut bodies);
        return;
    }

    if !buttons.just_pressed(MouseButton::Left)
        || egui_input
            .as_ref()
            .is_some_and(|input| input.wants_any_pointer_input())
    {
        return;
    }

    let translation = ray.direction * MAX_PICK_DISTANCE;
    let Ok(Some(hit)) = cast_ray_closest(
        &context,
        ray.origin,
        translation,
        boxddd::QueryFilter::default(),
    ) else {
        return;
    };
    let Some(entity) = hit.entity else {
        return;
    };
    let Ok((body, _)) = bodies.get_mut(entity) else {
        return;
    };
    let body_id = body.id();

    let Some(world) = context.world_mut() else {
        return;
    };
    let Ok(previous_type) = world.try_body_type(body_id) else {
        return;
    };
    if previous_type != boxddd::BodyType::Dynamic {
        return;
    }
    let Ok(transform) = world.try_body_transform(body_id) else {
        return;
    };

    let body_position = transform.p.to_bevy_vec3();
    let direction = *ray.direction;
    let depth = (body_position - ray.origin)
        .dot(direction)
        .max(MIN_DRAG_DEPTH);
    let projected_position = point_on_ray(ray.origin, direction, depth);
    let _ = world.try_set_body_type(body_id, boxddd::BodyType::Kinematic);
    let _ = world.try_set_body_linear_velocity(body_id, boxddd::Vec3::ZERO);

    drag.active = Some(DragTarget {
        entity,
        body_id,
        previous_type,
        rotation: transform.q,
        depth,
        grab_offset: body_position - projected_position,
        last_position: body_position,
        last_velocity: Vec3::ZERO,
    });
}

pub(crate) fn draw_physics_pick(
    camera: Single<(&Camera, &GlobalTransform)>,
    window: Single<&Window>,
    context: NonSend<BoxdddPhysicsContext>,
    drag: Res<PhysicsDragState>,
    mut gizmos: Gizmos,
) {
    let Some(ray) = cursor_ray(*camera, *window) else {
        return;
    };

    if let Some(target) = drag.active {
        let cursor_anchor = point_on_ray(ray.origin, *ray.direction, target.depth);
        gizmos.ray(
            ray.origin,
            *ray.direction * target.depth,
            Color::srgb(0.35, 0.70, 1.0),
        );
        gizmos.line(
            cursor_anchor,
            target.last_position,
            Color::srgb(1.0, 0.78, 0.22),
        );
        gizmos.sphere(target.last_position, 0.16, Color::srgb(1.0, 0.78, 0.22));
        return;
    }

    let translation = ray.direction * MAX_PICK_DISTANCE;
    let Ok(Some(hit)) = cast_ray_closest(
        &context,
        ray.origin,
        translation,
        boxddd::QueryFilter::default(),
    ) else {
        return;
    };

    gizmos.sphere(hit.point, 0.08, Color::srgb(1.0, 0.96, 0.25));
    gizmos.ray(hit.point, hit.normal * 0.45, Color::srgb(0.2, 1.0, 0.35));
}

fn update_active_drag(
    target: DragTarget,
    ray: Ray3d,
    real_time: &Time<Real>,
    context: &mut BoxdddPhysicsContext,
    bodies: &mut Query<(&BoxdddBody, &mut Transform)>,
) -> Option<DragTarget> {
    let dt = real_time.delta_secs().max(1.0 / 240.0);
    let target_position =
        point_on_ray(ray.origin, *ray.direction, target.depth) + target.grab_offset;
    let velocity = clamp_throw_velocity((target_position - target.last_position) / dt);
    let world_transform =
        boxddd::WorldTransform::new(target_position.to_boxddd_pos(), target.rotation);

    let Some(world) = context.world_mut() else {
        return None;
    };
    if world
        .try_set_body_target_transform(target.body_id, world_transform, dt, true)
        .or_else(|_| {
            world.try_set_body_transform(
                target.body_id,
                target_position.to_boxddd_pos(),
                target.rotation,
            )
        })
        .is_err()
    {
        return None;
    }
    let _ = world.try_set_body_linear_velocity(target.body_id, velocity.to_boxddd_vec3());

    if let Ok((_, mut transform)) = bodies.get_mut(target.entity) {
        transform.translation = target_position;
    } else {
        finish_drag(target, context);
        return None;
    }

    Some(DragTarget {
        last_position: target_position,
        last_velocity: velocity,
        ..target
    })
}

fn finish_drag(target: DragTarget, context: &mut BoxdddPhysicsContext) {
    let Some(world) = context.world_mut() else {
        return;
    };
    let _ = world.try_set_body_type(target.body_id, target.previous_type);
    if target.previous_type == boxddd::BodyType::Dynamic {
        let _ = world.try_set_body_linear_velocity(
            target.body_id,
            clamp_throw_velocity(target.last_velocity).to_boxddd_vec3(),
        );
    }
}

fn cursor_ray(
    (camera, camera_transform): (&Camera, &GlobalTransform),
    window: &Window,
) -> Option<Ray3d> {
    let cursor_position = window.cursor_position()?;
    camera
        .viewport_to_world(camera_transform, cursor_position)
        .ok()
}

pub(crate) fn point_on_ray(origin: Vec3, direction: Vec3, depth: f32) -> Vec3 {
    origin + direction.normalize_or_zero() * depth.max(MIN_DRAG_DEPTH)
}

pub(crate) fn clamp_throw_velocity(velocity: Vec3) -> Vec3 {
    velocity.clamp_length_max(MAX_THROW_SPEED)
}