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 usevisit_*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 explicitfrom_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-timetry_*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(...)andWorldDef::from_raw(...)areunsafe. - 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. ContactIdvalues 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
Profiletimings, explosion control, andtry_*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.
- Owned handles:
- 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 returningApiResult<T>. - Threading:
Worldand owned handles are!Send/!Sync. Run physics on one thread; in async runtimes preferspawn_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)
Vec2always accepts[f32; 2]and(f32, f32)anywhereInto<Vec2>is used.- With
mint,cgmath,nalgebra, orglamenabled,Vec2also accepts those crates’ 2D vector/point types viaFrom/Into. - Returned vectors can be converted back using
Fromto the corresponding math types. mintalso coversRot <-> mint::RowMatrix2/mint::ColumnMatrix2, plus row- and column-major 2D affine matrices forTransform.
Modules
world,body,contact,shapes,joints,query,collision,events,debug_draw,prelude. Importboxdd::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 systembox2dvia pkg-config.mint: lightweight math interop types (mint::Vector2,mint::Point2,mint::RowMatrix2/mint::ColumnMatrix2forRot, and row/column-major 2D affine matrices forTransform).cgmath/nalgebra/glam: conversions with their 2D math types.bytemuck:Pod/Zeroablefor 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 throughunsafe WorldBuilder::task_system_raw(...)/WorldDef::set_task_system_raw(...). It does not makeWorld,WorldHandle, or owned handlesSend/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, andset_restitution_callbackmay run on Box2D worker threads and therefore requireSend + Syncclosures.- See
examples/physics_thread.rsfor 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 handleApiErrorexplicitly. try_*setters also turn obvious Box2D assert preconditions into recoverableApiErrorvalues instead of relying on assert-enabled native builds.WorldDef,BodyDef,ShapeDef,SurfaceMaterial,JointBase, and concrete*JointDefvalues exposevalidate()for preflight checks before crossing the FFI boundary.- Crate-owned geometry values (
Circle,Segment,Capsule,ChainSegment,Polygon) also exposeis_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 recoverabletry_*split as the rest of the crate.
Events
- Four access styles:
- By value:
WorldandWorldHandleexposecontact_events()/sensor_events()/body_events()/joint_events()for owned data that can be stored or used cross-frame. - Reusable buffers:
WorldandWorldHandlealso 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).
- By value:
- Callback-sensitive event entrypoints also expose matching
try_*variants so callback-lock failures can returnApiError::InCallbackinstead of forcing panic-only control flow. - Borrowed view/raw event APIs intentionally stay on
World, notWorldHandle, 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.