boxdd 0.6.0

Safe, ergonomic Rust bindings for Box2D v3
Documentation
#![allow(
    clippy::too_many_arguments,
    reason = "raw helpers preserve Box2D query arguments and the explicit world origin"
)]

use boxdd_sys::ffi;

#[cfg(not(target_arch = "wasm32"))]
use crate::error::{Error, Result};
use crate::types::{Position, Vec2};

#[cfg(not(target_arch = "wasm32"))]
use super::buffers::{
    MoverQueryBuffer, RawMoverPlane, RawRayHit, RayQueryBuffer, ShapeQueryBuffer,
};
use super::types::*;
use crate::world::QueryCall;

#[cfg(not(target_arch = "wasm32"))]
struct CollectCtx<'a, B> {
    buffer: &'a mut B,
    error: Option<Error>,
    panic: crate::core::callback_state::PanicSlot,
}

#[cfg(not(target_arch = "wasm32"))]
impl<'a, B> CollectCtx<'a, B> {
    fn new(buffer: &'a mut B) -> Self {
        Self {
            buffer,
            error: None,
            panic: crate::core::callback_state::PanicSlot::default(),
        }
    }

    fn push(&mut self, push: impl FnOnce(&mut B) -> Result<()>) -> bool {
        if self.error.is_some() || self.panic.has_panicked() {
            return false;
        }
        let result =
            crate::core::callback_state::invoke_owner_callback(&mut self.panic, None, || {
                Some(push(self.buffer))
            });
        match result {
            Some(Ok(())) => true,
            Some(Err(error)) => {
                self.error = Some(error);
                false
            }
            None => false,
        }
    }

    fn finish(self) -> Result<()> {
        let Self { error, panic, .. } = self;
        panic.resume_or_forget();
        error.map_or(Ok(()), Err)
    }
}

#[cfg(not(target_arch = "wasm32"))]
unsafe extern "C" fn collect_shape_id_cb(
    shape_id: ffi::b2ShapeId,
    context: *mut core::ffi::c_void,
) -> bool {
    let context = context.cast::<CollectCtx<'_, ShapeQueryBuffer>>();
    if context.is_null() || !context.is_aligned() {
        return false;
    }
    let context = unsafe { &mut *context };
    context.push(|buffer| buffer.push_raw(shape_id))
}

#[allow(clippy::unnecessary_cast)]
#[cfg(not(target_arch = "wasm32"))]
unsafe extern "C" fn collect_ray_result_cb(
    shape_id: ffi::b2ShapeId,
    point: ffi::b2Pos,
    normal: ffi::b2Vec2,
    fraction: f32,
    context: *mut core::ffi::c_void,
) -> f32 {
    let context = context.cast::<CollectCtx<'_, RayQueryBuffer>>();
    if context.is_null() || !context.is_aligned() {
        return 0.0;
    }
    let context = unsafe { &mut *context };
    if context.push(|buffer| {
        buffer.push_raw(RawRayHit {
            shape_id,
            point,
            normal,
            fraction,
        })
    }) {
        1.0f32
    } else {
        0.0f32
    }
}

#[cfg(not(target_arch = "wasm32"))]
unsafe extern "C" fn collect_mover_plane_result_cb(
    shape_id: ffi::b2ShapeId,
    plane: *const ffi::b2PlaneResult,
    context: *mut core::ffi::c_void,
) -> bool {
    let context = context.cast::<CollectCtx<'_, MoverQueryBuffer>>();
    if context.is_null() || !context.is_aligned() {
        return false;
    }
    let context = unsafe { &mut *context };
    if plane.is_null() || !plane.is_aligned() {
        context.error = ::core::option::Option::Some(Error::InvalidNativeOutput {
            operation: "Query::collide_mover",
            output: "plane",
            constraint: "a non-null aligned mover-plane pointer",
        });
        return false;
    }
    let plane = unsafe { *plane };
    context.push(|buffer| buffer.push_raw(RawMoverPlane { shape_id, plane }))
}

#[cfg(not(target_arch = "wasm32"))]
pub(super) fn overlap_aabb(
    call: &QueryCall<'_>,
    origin: Position,
    aabb: Aabb,
    filter: QueryFilter,
    buffer: &mut ShapeQueryBuffer,
) -> Result<()> {
    let mut context = CollectCtx::new(buffer);
    unsafe {
        let _ = ffi::b2World_OverlapAABB(
            call.raw_world(),
            origin.into_raw(),
            aabb.into_raw(),
            filter.0,
            Some(collect_shape_id_cb),
            &mut context as *mut _ as *mut _,
        );
    }
    context.finish()
}

#[cfg(not(target_arch = "wasm32"))]
pub(super) fn overlap_shape(
    call: &QueryCall<'_>,
    origin: Position,
    proxy: &ffi::b2ShapeProxy,
    filter: QueryFilter,
    buffer: &mut ShapeQueryBuffer,
) -> Result<()> {
    let mut context = CollectCtx::new(buffer);
    unsafe {
        let _ = ffi::b2World_OverlapShape(
            call.raw_world(),
            origin.into_raw(),
            proxy,
            filter.0,
            Some(collect_shape_id_cb),
            &mut context as *mut _ as *mut _,
        );
    }
    context.finish()
}

pub(super) fn cast_ray_closest(
    call: &QueryCall<'_>,
    origin: Position,
    translation: Vec2,
    filter: QueryFilter,
) -> ffi::b2RayResult {
    unsafe {
        ffi::b2World_CastRayClosest(
            call.raw_world(),
            origin.into_raw(),
            translation.into_raw(),
            filter.0,
        )
    }
}

#[cfg(not(target_arch = "wasm32"))]
pub(super) fn cast_ray_all(
    call: &QueryCall<'_>,
    origin: Position,
    translation: Vec2,
    filter: QueryFilter,
    buffer: &mut RayQueryBuffer,
) -> Result<()> {
    let mut context = CollectCtx::new(buffer);
    unsafe {
        let _ = ffi::b2World_CastRay(
            call.raw_world(),
            origin.into_raw(),
            translation.into_raw(),
            filter.0,
            Some(collect_ray_result_cb),
            &mut context as *mut _ as *mut _,
        );
    }
    context.finish()
}

#[cfg(not(target_arch = "wasm32"))]
pub(super) fn cast_shape(
    call: &QueryCall<'_>,
    origin: Position,
    proxy: &ffi::b2ShapeProxy,
    translation: Vec2,
    filter: QueryFilter,
    buffer: &mut RayQueryBuffer,
) -> Result<()> {
    let mut context = CollectCtx::new(buffer);
    unsafe {
        let _ = ffi::b2World_CastShape(
            call.raw_world(),
            origin.into_raw(),
            proxy,
            translation.into_raw(),
            filter.0,
            Some(collect_ray_result_cb),
            &mut context as *mut _ as *mut _,
        );
    }
    context.finish()
}

pub(super) fn make_capsule(c1: Vec2, c2: Vec2, radius: f32) -> ffi::b2Capsule {
    ffi::b2Capsule {
        center1: c1.into_raw(),
        center2: c2.into_raw(),
        radius,
    }
}

pub(super) fn cast_mover(
    call: &QueryCall<'_>,
    origin: Position,
    c1: Vec2,
    c2: Vec2,
    radius: f32,
    translation: Vec2,
    filter: QueryFilter,
) -> f32 {
    let capsule = make_capsule(c1, c2, radius);
    unsafe {
        ffi::b2World_CastMover(
            call.raw_world(),
            origin.into_raw(),
            &capsule,
            translation.into_raw(),
            filter.0,
        )
    }
}

#[cfg(not(target_arch = "wasm32"))]
pub(super) fn collide_mover(
    call: &QueryCall<'_>,
    origin: Position,
    c1: Vec2,
    c2: Vec2,
    radius: f32,
    filter: QueryFilter,
    buffer: &mut MoverQueryBuffer,
) -> Result<()> {
    let capsule = make_capsule(c1, c2, radius);
    let mut context = CollectCtx::new(buffer);
    unsafe {
        ffi::b2World_CollideMover(
            call.raw_world(),
            origin.into_raw(),
            &capsule,
            filter.0,
            ::core::option::Option::Some(collect_mover_plane_result_cb),
            &mut context as *mut _ as *mut _,
        );
    }
    context.finish()
}

#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
    use super::*;

    #[test]
    #[cfg(not(target_arch = "wasm32"))]
    fn callbacks_match_box2d_typedefs() {
        let _: ffi::b2CastResultFcn = Some(collect_ray_result_cb);
        let _: ffi::b2PlaneResultFcn = Some(collect_mover_plane_result_cb);
        let _: ffi::b2OverlapResultFcn = Some(collect_shape_id_cb);
    }

    #[test]
    #[cfg(not(target_arch = "wasm32"))]
    fn query_callback_trampolines_fail_closed_on_invalid_native_pointers() {
        let shape_id: ffi::b2ShapeId = unsafe { core::mem::zeroed() };
        assert!(!unsafe { collect_shape_id_cb(shape_id, core::ptr::null_mut()) });
        assert_eq!(
            unsafe {
                collect_ray_result_cb(
                    shape_id,
                    Position::ZERO.into_raw(),
                    Vec2::ZERO.into_raw(),
                    0.0,
                    core::ptr::null_mut(),
                )
            },
            0.0
        );
        assert!(!unsafe {
            collect_mover_plane_result_cb(shape_id, core::ptr::null(), core::ptr::null_mut())
        });

        let mut buffer = MoverQueryBuffer::new();
        let mut context = CollectCtx::new(&mut buffer);
        let context_pointer = core::ptr::from_mut(&mut context).cast::<core::ffi::c_void>();
        assert!(!unsafe {
            collect_mover_plane_result_cb(shape_id, core::ptr::null(), context_pointer)
        });
        assert_eq!(
            context.error,
            Some(Error::InvalidNativeOutput {
                operation: "Query::collide_mover",
                output: "plane",
                constraint: "a non-null aligned mover-plane pointer",
            })
        );
    }

    #[test]
    #[cfg(not(target_arch = "wasm32"))]
    fn world_queries_match_origin_aware_box2d_signatures() {
        type OverlapAabb = unsafe extern "C" fn(
            ffi::b2WorldId,
            ffi::b2Pos,
            ffi::b2AABB,
            ffi::b2QueryFilter,
            ffi::b2OverlapResultFcn,
            *mut core::ffi::c_void,
        ) -> ffi::b2TreeStats;
        type CastShape = unsafe extern "C" fn(
            ffi::b2WorldId,
            ffi::b2Pos,
            *const ffi::b2ShapeProxy,
            ffi::b2Vec2,
            ffi::b2QueryFilter,
            ffi::b2CastResultFcn,
            *mut core::ffi::c_void,
        ) -> ffi::b2TreeStats;

        let _: OverlapAabb = ffi::b2World_OverlapAABB;
        let _: CastShape = ffi::b2World_CastShape;
    }
}