Skip to main content

Crate boxdd

Crate boxdd 

Source
Expand description

boxdd: Safe, ergonomic Rust bindings for Box2D (v3 C API)

Highlights

  • Thin safe layer on top of the official Box2D v3 C API.
  • Modular API: world, bodies, shapes, joints, queries, collision geometry, events, debug draw.
  • Ergonomics: builder patterns, world-space helpers, and optional math interop (mint/cgmath/nalgebra/glam).
  • Hot-path friendly APIs: keep the convenience Vec-returning methods, reuse caller-owned buffers with *_into, or use visit_* overlap queries to avoid result-container allocation entirely.
  • Character mover helpers: cast movers, collect collision planes, solve planes, and clip velocity without raw FFI.
  • Standalone collision geometry helpers: shape proxies, segment/GJK distance, manifolds, shape cast, TOI, recoverable try_* validation paths, AABB validation/ray cast, and deterministic global math helpers.
  • Core math types (Vec2, Rot, Transform) use explicit from_raw(...) / into_raw() naming for Box2D interop instead of implicit raw conversions.
  • Global Box2D foundation helpers expose allocated-byte inspection, timing ticks/millisecond helpers, thread yielding, and deterministic hashing without dropping to boxdd_sys::ffi.
  • Shape geometry uses crate-owned values (Circle, Segment, ChainSegment, Capsule, Polygon) across helpers, shape editing, and creation, including square/rounded/offset/hull-based polygon builders plus standalone and construction-time try_* geometry helpers without raw FFI.
  • Chain runtime material helpers use visible live-segment indexing on open chains instead of Box2D’s ghost-placeholder storage layout.
  • Safe shape/joint mutators front-load obvious Box2D assert preconditions such as non-negative material scalars and ordered joint limits.
  • Pointer-bearing config wrappers keep their raw re-entry explicit: BodyDef::from_raw(...) and WorldDef::from_raw(...) are unsafe.
  • Live shapes expose safe runtime helpers for AABB, point tests, direct ray casts, computed mass data, and runtime event toggles.
  • Bodies expose safe runtime helpers for rotation, sleep/awake/enabled/bullet/name controls, attached shape/joint enumeration, and body-level contact/hit event toggles.
  • Joints expose safe runtime helpers for joint kind, connected body ids, collide_connected, constraint tuning, local frames, wake controls, and type-specific runtime state across distance/prismatic/revolute/weld/wheel/motor families.
  • ContactId values from contact events or snapshots expose direct safe inherent helpers for validity checks and crate-owned/raw contact-data reads.
  • World runtime helpers expose counters, per-stage Profile timings, explosion control, and try_* access for callback-sensitive tuning toggles.
  • Core value types such as ShapeType, MassData, SurfaceMaterial, and contact manifolds are crate-owned instead of leaking raw Box2D structs.
  • Typed material mixing callbacks for friction and restitution using user_material_id.
  • Three usage styles:
    • Owned handles: OwnedBody/OwnedShape/OwnedJoint/OwnedChain (Drop destroys; easy to store).
    • Scoped handles: Body<'_>/Shape<'_>/Joint<'_>/Chain<'_> (dropping only releases the world borrow).
    • ID-style: raw ids (BodyId/ShapeId/JointId/ChainId) for maximum flexibility.
  • Safe handle methods validate ids and panic on invalid ids (prevents UB if an id becomes stale). For recoverable failures (invalid ids / wrong typed-joint family / calling during Box2D callbacks), use try_* APIs returning ApiResult<T>.
  • Threading: World and owned handles are !Send/!Sync. Run physics on one thread; in async runtimes prefer spawn_local/LocalSet, or create the world inside a dedicated physics thread and communicate via channels.

Quickstart (owned handles)

use boxdd::{World, WorldDef, BodyBuilder, ShapeDef, shapes, Vec2};
let def = WorldDef::builder().gravity(Vec2::new(0.0, -9.8)).build();
let mut world = World::new(def).unwrap();
let mut body = world.create_body_owned(BodyBuilder::new().position([0.0, 2.0]).build());
let sdef = ShapeDef::builder().density(1.0).build();
let poly = shapes::box_polygon(0.5, 0.5);
let _shape = body.create_polygon_shape(&sdef, &poly);
world.step(1.0/60.0, 4);

Quickstart (scoped handles)

use boxdd::{World, WorldDef, BodyBuilder, ShapeDef, shapes, Vec2};
let def = WorldDef::builder().gravity(Vec2::new(0.0, -9.8)).build();
let mut world = World::new(def).unwrap();
{
    // Limit the borrow of `world` by scoping the body handle.
    let mut body = world.create_body(BodyBuilder::new().position([0.0, 2.0]).build());
    let sdef = ShapeDef::builder().density(1.0).build();
    let poly = shapes::box_polygon(0.5, 0.5);
    let _shape = body.create_polygon_shape(&sdef, &poly);
}
world.step(1.0/60.0, 4);

Quickstart (ID-style)

use boxdd::{World, WorldDef, BodyBuilder, ShapeDef, shapes, Vec2};
let def = WorldDef::builder().gravity(Vec2::new(0.0, -9.8)).build();
let mut world = World::new(def).unwrap();
let body_id = world.create_body_id(BodyBuilder::new().position([0.0, 2.0]).build());
let sdef = ShapeDef::builder().density(1.0).build();
let poly = shapes::box_polygon(0.5, 0.5);
let _shape_id = world.create_polygon_shape_for(body_id, &sdef, &poly);
world.step(1.0/60.0, 4);

Math interop (optional features)

  • Vec2 always accepts [f32; 2] and (f32, f32) anywhere Into<Vec2> is used.
  • With mint, cgmath, nalgebra, or glam enabled, Vec2 also accepts those crates’ 2D vector/point types via From/Into.
  • Returned vectors can be converted back using From to the corresponding math types.
  • mint also covers Rot <-> mint::RowMatrix2 / mint::ColumnMatrix2, plus row- and column-major 2D affine matrices for Transform.

Modules

  • world, body, contact, shapes, joints, query, collision, events, debug_draw, prelude. Import boxdd::prelude::* for the most common types.

Queries (AABB + Ray Cast)

use boxdd::{World, WorldDef, BodyBuilder, ShapeDef, shapes, Vec2, Aabb, QueryFilter};
let mut world = World::new(WorldDef::builder().gravity([0.0,-9.8]).build()).unwrap();
let b = world.create_body_id(BodyBuilder::new().position([0.0, 2.0]).build());
let sdef = ShapeDef::builder().density(1.0).build();
world.create_polygon_shape_for(b, &sdef, &shapes::box_polygon(0.5, 0.5));
// AABB overlap
let hits = world.overlap_aabb(Aabb::from_center_half_extents([0.0, 1.0], [1.0, 1.5]), QueryFilter::default());
assert!(!hits.is_empty());
let mut reused = Vec::new();
world.overlap_aabb_into(
    Aabb::from_center_half_extents([0.0, 1.0], [1.0, 1.5]),
    QueryFilter::default(),
    &mut reused,
);
assert_eq!(hits.len(), reused.len());
let mut visited = 0;
let complete = world.visit_overlap_aabb(
    Aabb::from_center_half_extents([0.0, 1.0], [1.0, 1.5]),
    QueryFilter::default(),
    |_| {
        visited += 1;
        true
    },
);
assert!(complete);
assert_eq!(hits.len(), visited);
// Ray (closest)
let r = world.cast_ray_closest(Vec2::new(0.0, 5.0), Vec2::new(0.0, -10.0), QueryFilter::default());
if r.hit { let _ = (r.point, r.normal, r.fraction); }

Character Mover Helpers

use boxdd::{clip_vector, solve_planes, CollisionPlane, QueryFilter, Vec2, World, WorldDef};
let world = World::new(WorldDef::default()).unwrap();
let planes = world.collide_mover([0.0_f32, 0.75], [0.0, 1.75], 0.25, QueryFilter::default());
let mut rigid: Vec<CollisionPlane> = planes
    .into_iter()
    .filter_map(|p| p.into_rigid_collision_plane())
    .collect();
let solved = solve_planes([0.0_f32, -0.1], &mut rigid);
let _clipped_velocity = clip_vector(Vec2::new(0.0, -1.0), &rigid);
let _ = solved.translation;

Collision Geometry

use boxdd::{
    segment_distance, shape_distance, DistanceInput, ShapeProxy, SimplexCache, ToiInput,
    ToiState, Sweep, Transform,
};
let proxy_a = ShapeProxy::new([[-1.0_f32, -1.0], [1.0, -1.0], [1.0, 1.0], [-1.0, 1.0]], 0.0).unwrap();
let proxy_b = ShapeProxy::new([[2.0_f32, -1.0], [2.0, 1.0]], 0.0).unwrap();
let mut cache = SimplexCache::default();
let seg = segment_distance([-1.0_f32, 0.0], [1.0, 0.0], [0.0, -1.0], [0.0, 1.0]);
assert!(seg.distance_squared >= 0.0);
let distance = shape_distance(
    DistanceInput::new(proxy_a, proxy_b, Transform::IDENTITY, Transform::IDENTITY),
    &mut cache,
);
assert!(distance.distance >= 0.0);
let toi = boxdd::time_of_impact(ToiInput::new(
    proxy_a,
    proxy_b,
    Sweep::new([0.0_f32, 0.0], [0.0, 0.0], [0.0, 0.0], boxdd::Rot::IDENTITY, boxdd::Rot::IDENTITY),
    Sweep::new([0.0_f32, 0.0], [0.0, 0.0], [-2.0, 0.0], boxdd::Rot::IDENTITY, boxdd::Rot::IDENTITY),
));
let _ = matches!(toi.state, ToiState::Hit | ToiState::Separated | ToiState::Overlapped | ToiState::Failed | ToiState::Unknown);

Material Mixing Callbacks

use boxdd::{MaterialMixInput, World, WorldDef};
let mut world = World::new(WorldDef::default()).unwrap();
world.set_friction_callback(|a: MaterialMixInput, b: MaterialMixInput| {
    if a.user_material_id == 1 || b.user_material_id == 1 {
        0.0
    } else {
        (a.coefficient * b.coefficient).sqrt()
    }
});

Feature Flags

  • serialize: scene snapshot helpers (save/apply world config; build/restore minimal full-scene snapshot).
  • pkg-config: allow linking against a system box2d via pkg-config.
  • mint: lightweight math interop types (mint::Vector2, mint::Point2, mint::RowMatrix2 / mint::ColumnMatrix2 for Rot, and row/column-major 2D affine matrices for Transform).
  • cgmath / nalgebra / glam: conversions with their 2D math types.
  • bytemuck: Pod/Zeroable for core math types (Vec2, Rot, Transform, Aabb) for zero-copy interop.

Threading and async

  • WorldDef::builder().worker_count(n) preserves Box2D’s worker-count setting, but actual multithreaded stepping still requires explicit raw task callbacks through unsafe WorldBuilder::task_system_raw(...) / WorldDef::set_task_system_raw(...). It does not make World, WorldHandle, or owned handles Send/Sync.
  • Keep the world on one thread/task. In async runtimes prefer spawn_local / LocalSet; in multi-threaded engines prefer a dedicated physics thread plus channels.
  • set_custom_filter*, set_pre_solve*, set_friction_callback, and set_restitution_callback may run on Box2D worker threads and therefore require Send + Sync closures.
  • See examples/physics_thread.rs for the dedicated-thread pattern.

Error handling

  • The default safe surface panics on misuse such as stale ids or calling Box2D while the world is locked in a callback. This keeps the common path terse and avoids Rust-level UB.
  • At runtime boundaries, prefer try_* APIs and handle ApiError explicitly.
  • try_* setters also turn obvious Box2D assert preconditions into recoverable ApiError values instead of relying on assert-enabled native builds.
  • WorldDef, BodyDef, ShapeDef, SurfaceMaterial, JointBase, and concrete *JointDef values expose validate() for preflight checks before crossing the FFI boundary.
  • Crate-owned geometry values (Circle, Segment, Capsule, ChainSegment, Polygon) also expose is_valid() / validate() for preflight geometry checks, and the world-free helper methods (mass_data, aabb, contains_point, ray_cast, transformed) follow the same panic-by-default plus recoverable try_* split as the rest of the crate.

Events

  • Four access styles:
    • By value: World and WorldHandle expose contact_events()/sensor_events()/body_events()/joint_events() for owned data that can be stored or used cross-frame.
    • Reusable buffers: World and WorldHandle also expose *_events_into(...) to reuse caller-owned event storage across frames.
    • Zero‑copy views: with_*_events_view(...) iterate without allocations (borrows internal buffers).
    • Raw slices: unsafe { with_*_events_raw(...) } expose FFI slices (borrows internal buffers).
  • Callback-sensitive event entrypoints also expose matching try_* variants so callback-lock failures can return ApiError::InCallback instead of forcing panic-only control flow.
  • Borrowed view/raw event APIs intentionally stay on World, not WorldHandle, because they are tied to the completed step’s world-local event buffers and deferred-destroy flushing behavior.

Example (reusable buffers + zero‑copy views)

use boxdd::{ContactEvents, World, WorldDef};
let mut world = World::new(WorldDef::default()).unwrap();
let mut contact_events = ContactEvents::default();
world.contact_events_into(&mut contact_events);
world.with_contact_events_view(|begin, end, hit| {
    let _ = (begin.count(), end.count(), hit.count());
});
world.with_sensor_events_view(|beg, end| { let _ = (beg.count(), end.count()); });
world.with_body_events_view(|moves| { for m in moves { let _ = (m.body_id(), m.fell_asleep()); } });
world.with_joint_events_view(|j| { let _ = j.count(); });

Re-exports§

pub use body::OwnedBody;
pub use body::Body;
pub use body::BodyBuilder;
pub use body::BodyDef;
pub use body::BodyType;
pub use collision::CastOutput;
pub use collision::DistanceInput;
pub use collision::DistanceOutput;
pub use collision::MAX_SHAPE_PROXY_POINTS;
pub use collision::SegmentDistanceResult;
pub use collision::ShapeCastPairInput;
pub use collision::ShapeProxy;
pub use collision::SimplexCache;
pub use collision::Sweep;
pub use collision::ToiInput;
pub use collision::ToiOutput;
pub use collision::ToiState;
pub use collision::collide_capsule_and_circle;
pub use collision::collide_capsules;
pub use collision::collide_chain_segment_and_capsule;
pub use collision::collide_chain_segment_and_circle;
pub use collision::collide_chain_segment_and_polygon;
pub use collision::collide_circles;
pub use collision::collide_polygon_and_capsule;
pub use collision::collide_polygon_and_circle;
pub use collision::collide_polygons;
pub use collision::collide_segment_and_capsule;
pub use collision::collide_segment_and_circle;
pub use collision::collide_segment_and_polygon;
pub use collision::segment_distance;
pub use collision::shape_cast;
pub use collision::shape_distance;
pub use collision::time_of_impact;
pub use collision::try_collide_capsule_and_circle;
pub use collision::try_collide_capsules;
pub use collision::try_collide_chain_segment_and_capsule;
pub use collision::try_collide_chain_segment_and_circle;
pub use collision::try_collide_chain_segment_and_polygon;
pub use collision::try_collide_circles;
pub use collision::try_collide_polygon_and_capsule;
pub use collision::try_collide_polygon_and_circle;
pub use collision::try_collide_polygons;
pub use collision::try_collide_segment_and_capsule;
pub use collision::try_collide_segment_and_circle;
pub use collision::try_collide_segment_and_polygon;
pub use collision::try_segment_distance;
pub use collision::try_shape_cast;
pub use collision::try_shape_distance;
pub use collision::try_time_of_impact;
pub use core::math::HASH_INIT;
pub use core::math::Rot;
pub use core::math::Transform;
pub use core::math::Version;
pub use core::math::allocated_byte_count;
pub use core::math::atan2;
pub use core::math::compute_cos_sin;
pub use core::math::hash_bytes;
pub use core::math::is_valid_float;
pub use core::math::length_units_per_meter;
pub use core::math::milliseconds_and_reset;
pub use core::math::milliseconds_since;
pub use core::math::rotation_between_unit_vectors;
pub use core::math::set_length_units_per_meter;
pub use core::math::ticks;
pub use core::math::version;
pub use core::math::yield_now;
pub use debug_draw::DebugDraw;
pub use debug_draw::DebugDrawCmd;
pub use debug_draw::DebugDrawOptions;
pub use debug_draw::HexColor;
pub use error::ApiError;
pub use error::ApiResult;
pub use events::BodyMoveEvent;
pub use events::ContactBeginTouchEvent;
pub use events::ContactEndTouchEvent;
pub use events::ContactEvents;
pub use events::ContactHitEvent;
pub use events::JointEvent;
pub use events::SensorBeginTouchEvent;
pub use events::SensorEndTouchEvent;
pub use events::SensorEvents;
pub use filter::Filter;
pub use joints::ConstraintTuning;
pub use joints::DistanceJointBuilder;
pub use joints::DistanceJointDef;
pub use joints::FilterJointBuilder;
pub use joints::FilterJointDef;
pub use joints::Joint;
pub use joints::JointBase;
pub use joints::JointBaseBuilder;
pub use joints::JointType;
pub use joints::MotorJointBuilder;
pub use joints::MotorJointDef;
pub use joints::PrismaticJointBuilder;
pub use joints::PrismaticJointDef;
pub use joints::RevoluteJointBuilder;
pub use joints::RevoluteJointDef;
pub use joints::WeldJointBuilder;
pub use joints::WeldJointDef;
pub use joints::WheelJointBuilder;
pub use joints::WheelJointDef;
pub use query::Aabb;
pub use query::CollisionPlane;
pub use query::MoverPlaneResult;
pub use query::Plane;
pub use query::PlaneSolverResult;
pub use query::QueryFilter;
pub use query::RayResult;
pub use query::clip_vector;
pub use query::solve_planes;
pub use query::try_clip_vector;
pub use query::try_solve_planes;
pub use shapes::chain::Chain;
pub use shapes::chain::ChainDef;
pub use shapes::chain::ChainDefBuilder;
pub use shapes::chain::ChainDefMaterialLayout;
pub use shapes::chain::OwnedChain;
pub use shapes::Capsule;
pub use shapes::ChainSegment;
pub use shapes::Circle;
pub use shapes::MAX_POLYGON_VERTICES;
pub use shapes::OwnedShape;
pub use shapes::Polygon;
pub use shapes::Segment;
pub use shapes::Shape;
pub use shapes::ShapeDef;
pub use shapes::ShapeDefBuilder;
pub use shapes::ShapeType;
pub use shapes::SurfaceMaterial;
pub use types::BodyId;
pub use types::ChainId;
pub use types::ContactData;
pub use types::ContactId;
pub use types::JointId;
pub use types::Manifold;
pub use types::ManifoldPoint;
pub use types::MassData;
pub use types::MotionLocks;
pub use types::ShapeId;
pub use types::Vec2;
pub use world::CallbackWorld;
pub use world::MaterialMixInput;
pub use world::OutstandingOwnedHandles;
pub use world::OwnedHandleCounts;
pub use world::Profile;
pub use world::World;
pub use world::WorldBuilder;
pub use world::WorldDef;
pub use world::WorldHandle;
pub use world_extras::ExplosionDef;

Modules§

body
collision
Standalone low-level collision geometry helpers.
contact
core
debug_draw
Debug Draw bridge to Box2D v3 callbacks.
error
Fallible error types for try_* APIs.
events
Event snapshots and zero-copy visitors.
filter
joints
Joint builders and creation helpers (modularized).
prelude
query
Broad-phase queries, casts, and character-mover helpers.
shapes
Shapes API
tuning
Tuning Notes and Upstream Constants
types
world
world_extras
Additional world runtime helpers and value types that sit beside the core world API.