boxdd 0.6.0

Safe, ergonomic Rust bindings for Box2D v3
Documentation
use boxdd_sys::ffi;

use crate::error::Result;
use crate::types::{BodyId, Position, Vec2};

use super::super::{
    scoped::Body,
    validation::{
        check_body_world_point_in_local_range, check_valid_body_float, check_valid_body_position,
        check_valid_body_vec2, check_valid_native_body_position,
    },
};
use super::{mass::body_world_center_of_mass_impl, raw_body_id};

#[inline]
fn body_apply_force_impl<F: Into<Vec2>, P: Into<Position>>(
    id: BodyId,
    force: F,
    point: P,
    wake: bool,
) {
    let force: ffi::b2Vec2 = force.into().into_raw();
    let point: ffi::b2Pos = point.into().into_raw();
    unsafe { ffi::b2Body_ApplyForce(raw_body_id(id), force, point, wake) };
}

#[inline]
fn body_apply_force_to_center_impl<V: Into<Vec2>>(id: BodyId, force: V, wake: bool) {
    let force: ffi::b2Vec2 = force.into().into_raw();
    unsafe { ffi::b2Body_ApplyForceToCenter(raw_body_id(id), force, wake) };
}

#[inline]
fn body_apply_torque_impl(id: BodyId, torque: f32, wake: bool) {
    unsafe { ffi::b2Body_ApplyTorque(raw_body_id(id), torque, wake) }
}

#[inline]
fn body_clear_forces_impl(id: BodyId) {
    unsafe { ffi::b2Body_ClearForces(raw_body_id(id)) };
}

#[inline]
fn body_apply_linear_impulse_impl<F: Into<Vec2>, P: Into<Position>>(
    id: BodyId,
    impulse: F,
    point: P,
    wake: bool,
) {
    let impulse: ffi::b2Vec2 = impulse.into().into_raw();
    let point: ffi::b2Pos = point.into().into_raw();
    unsafe { ffi::b2Body_ApplyLinearImpulse(raw_body_id(id), impulse, point, wake) };
}

#[inline]
fn body_apply_linear_impulse_to_center_impl<V: Into<Vec2>>(id: BodyId, impulse: V, wake: bool) {
    let impulse: ffi::b2Vec2 = impulse.into().into_raw();
    unsafe { ffi::b2Body_ApplyLinearImpulseToCenter(raw_body_id(id), impulse, wake) };
}

#[inline]
fn body_apply_angular_impulse_impl(id: BodyId, impulse: f32, wake: bool) {
    unsafe { ffi::b2Body_ApplyAngularImpulse(raw_body_id(id), impulse, wake) }
}

impl Body<'_> {
    pub fn apply_force<F: Into<Vec2>, P: Into<Position>>(
        &mut self,
        force: F,
        point: P,
        wake: bool,
    ) -> Result<()> {
        let id = self.body_id();
        let force = crate::core::callback_state::PendingUserValue::new(force);
        let point = crate::core::callback_state::PendingUserValue::new(point);
        self.body_access().call(move |_| {
            let force =
                check_valid_body_vec2("Body::apply_force", "force", force.into_inner().into())?;
            let point =
                check_valid_body_position("Body::apply_force", "point", point.into_inner().into())?;
            let center = check_valid_native_body_position(
                "Body::apply_force",
                "world_center_of_mass",
                body_world_center_of_mass_impl(id),
            )?;
            let point =
                check_body_world_point_in_local_range("Body::apply_force", "point", point, center)?;
            body_apply_force_impl(id, force, point, wake);
            Ok(())
        })
    }

    pub fn apply_force_to_center<V: Into<Vec2>>(&mut self, force: V, wake: bool) -> Result<()> {
        self.body_access().call(|_| {
            let force =
                check_valid_body_vec2("Body::apply_force_to_center", "force", force.into())?;
            body_apply_force_to_center_impl(self.body_id(), force, wake);
            Ok(())
        })
    }

    pub fn apply_torque(&mut self, torque: f32, wake: bool) -> Result<()> {
        self.body_access().call(|_| {
            let torque = check_valid_body_float("Body::apply_torque", "torque", torque)?;
            body_apply_torque_impl(self.body_id(), torque, wake);
            Ok(())
        })
    }

    pub fn clear_forces(&mut self) -> Result<()> {
        self.body_access().call(|_| {
            body_clear_forces_impl(self.body_id());
            Ok(())
        })
    }

    pub fn apply_linear_impulse<F: Into<Vec2>, P: Into<Position>>(
        &mut self,
        impulse: F,
        point: P,
        wake: bool,
    ) -> Result<()> {
        let id = self.body_id();
        let impulse = crate::core::callback_state::PendingUserValue::new(impulse);
        let point = crate::core::callback_state::PendingUserValue::new(point);
        self.body_access().call(move |_| {
            let impulse = check_valid_body_vec2(
                "Body::apply_linear_impulse",
                "impulse",
                impulse.into_inner().into(),
            )?;
            let point = check_valid_body_position(
                "Body::apply_linear_impulse",
                "point",
                point.into_inner().into(),
            )?;
            let center = check_valid_native_body_position(
                "Body::apply_linear_impulse",
                "world_center_of_mass",
                body_world_center_of_mass_impl(id),
            )?;
            let point = check_body_world_point_in_local_range(
                "Body::apply_linear_impulse",
                "point",
                point,
                center,
            )?;
            body_apply_linear_impulse_impl(id, impulse, point, wake);
            Ok(())
        })
    }

    pub fn apply_linear_impulse_to_center<V: Into<Vec2>>(
        &mut self,
        impulse: V,
        wake: bool,
    ) -> Result<()> {
        self.body_access().call(|_| {
            let impulse = check_valid_body_vec2(
                "Body::apply_linear_impulse_to_center",
                "impulse",
                impulse.into(),
            )?;
            body_apply_linear_impulse_to_center_impl(self.body_id(), impulse, wake);
            Ok(())
        })
    }

    pub fn apply_angular_impulse(&mut self, impulse: f32, wake: bool) -> Result<()> {
        self.body_access().call(|_| {
            let impulse =
                check_valid_body_float("Body::apply_angular_impulse", "impulse", impulse)?;
            body_apply_angular_impulse_impl(self.body_id(), impulse, wake);
            Ok(())
        })
    }
}

#[cfg(test)]
mod tests {
    use std::{cell::Cell, rc::Rc};

    use super::*;

    struct PositionConversionProbe {
        converted: Rc<Cell<bool>>,
        drops: Rc<Cell<usize>>,
    }

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

    impl Drop for PositionConversionProbe {
        fn drop(&mut self) {
            self.drops.set(self.drops.get() + 1);
            panic!("secondary unmaterialized body input cleanup panic");
        }
    }

    struct InvokeOnDrop<F: FnOnce()>(Option<F>);

    impl<F: FnOnce()> Drop for InvokeOnDrop<F> {
        fn drop(&mut self) {
            if let Some(invoke) = self.0.take() {
                invoke();
            }
        }
    }

    #[cfg(not(target_arch = "wasm32"))]
    #[test]
    fn rejected_later_apply_force_input_cleanup_during_outer_unwind_does_not_abort() {
        const CHILD: &str = "BOXDD_OUTER_UNWIND_APPLY_FORCE_INPUT_ORDER";
        const TEST_NAME: &str = "body::runtime::dynamics::tests::rejected_later_apply_force_input_cleanup_during_outer_unwind_does_not_abort";
        const PRIMARY_PANIC: &str = "outer apply-force input unwind remains primary";

        if std::env::var_os(CHILD).is_some() {
            let foundation = crate::Foundation::initialize_default().unwrap();
            let mut world = foundation.create_world(foundation.world_def()).unwrap();
            let body_id = world.create_body(world.body_def()).unwrap();
            let mut body = world.body(body_id).unwrap();
            let converted = Rc::new(Cell::new(false));
            let drops = Rc::new(Cell::new(0));
            let rejected = Rc::new(Cell::new(false));
            let converted_from_drop = Rc::clone(&converted);
            let drops_from_drop = Rc::clone(&drops);
            let rejected_from_drop = Rc::clone(&rejected);

            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
                let _invoke = InvokeOnDrop(Some(|| {
                    rejected_from_drop.set(
                        body.apply_force(
                            Vec2::new(f32::NAN, 0.0),
                            PositionConversionProbe {
                                converted: converted_from_drop,
                                drops: drops_from_drop,
                            },
                            true,
                        )
                        .is_err(),
                    );
                }));
                std::panic::panic_any(PRIMARY_PANIC);
            }));

            let payload = result.expect_err("the outer panic must keep unwinding");
            assert_eq!(payload.downcast_ref::<&'static str>(), Some(&PRIMARY_PANIC));
            assert!(rejected.get());
            assert!(!converted.get());
            assert_eq!(drops.get(), 1);
            eprintln!("boxdd-outer-unwind-apply-force-input-order: completed");
            return;
        }

        let output = std::process::Command::new(
            std::env::current_exe().expect("test executable path must be available"),
        )
        .args(["--exact", TEST_NAME, "--nocapture"])
        .env(CHILD, "1")
        .output()
        .expect("outer-unwind apply-force input-order child process must start");
        assert!(
            output.status.success(),
            "outer-unwind apply-force input-order child aborted\nstdout:\n{}\nstderr:\n{}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr),
        );
        assert!(
            String::from_utf8_lossy(&output.stderr)
                .contains("boxdd-outer-unwind-apply-force-input-order: completed"),
            "outer-unwind apply-force input-order child did not complete\nstdout:\n{}\nstderr:\n{}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr),
        );
    }
}