Skip to main content

boxdd/query/
types.rs

1use crate::error::ApiResult;
2use crate::types::{ShapeId, Vec2};
3use boxdd_sys::ffi;
4
5pub(super) fn minimum_mover_radius() -> f32 {
6    0.01 * crate::length_units_per_meter()
7}
8
9pub(super) fn assert_query_vec2_valid(name: &str, value: Vec2) {
10    assert!(
11        value.is_valid(),
12        "{name} must be a valid Box2D vector, got {:?}",
13        value
14    );
15}
16pub(super) fn check_query_vec2_valid(value: Vec2) -> ApiResult<()> {
17    if value.is_valid() {
18        Ok(())
19    } else {
20        Err(crate::error::ApiError::InvalidArgument)
21    }
22}
23pub(super) fn assert_query_aabb_valid(aabb: Aabb) {
24    assert!(aabb.is_valid(), "aabb must be valid, got {:?}", aabb);
25}
26pub(super) fn check_query_aabb_valid(aabb: Aabb) -> ApiResult<()> {
27    if aabb.is_valid() {
28        Ok(())
29    } else {
30        Err(crate::error::ApiError::InvalidArgument)
31    }
32}
33
34#[inline]
35pub(super) fn assert_query_non_negative_finite_scalar(name: &str, value: f32) {
36    assert!(
37        crate::is_valid_float(value) && value >= 0.0,
38        "{name} must be finite and >= 0.0, got {value}"
39    );
40}
41
42#[inline]
43pub(super) fn check_query_non_negative_finite_scalar(value: f32) -> ApiResult<()> {
44    if crate::is_valid_float(value) && value >= 0.0 {
45        Ok(())
46    } else {
47        Err(crate::error::ApiError::InvalidArgument)
48    }
49}
50
51#[inline]
52pub(super) fn assert_query_angle_valid(angle_radians: f32) {
53    assert!(
54        crate::is_valid_float(angle_radians),
55        "angle_radians must be finite, got {angle_radians}"
56    );
57}
58
59#[inline]
60pub(super) fn check_query_angle_valid(angle_radians: f32) -> ApiResult<()> {
61    if crate::is_valid_float(angle_radians) {
62        Ok(())
63    } else {
64        Err(crate::error::ApiError::InvalidArgument)
65    }
66}
67
68#[inline]
69pub(super) fn assert_query_mover_radius_valid(radius: f32) {
70    let minimum = minimum_mover_radius();
71    assert!(
72        crate::is_valid_float(radius) && radius > minimum,
73        "mover radius must be finite and > {minimum}, got {radius}"
74    );
75}
76
77#[inline]
78pub(super) fn check_query_mover_radius_valid(radius: f32) -> ApiResult<()> {
79    if crate::is_valid_float(radius) && radius > minimum_mover_radius() {
80        Ok(())
81    } else {
82        Err(crate::error::ApiError::InvalidArgument)
83    }
84}
85
86/// Axis-aligned bounding box
87#[doc(alias = "aabb")]
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89#[repr(C)]
90#[derive(Copy, Clone, Debug, PartialEq)]
91pub struct Aabb {
92    pub lower: Vec2,
93    pub upper: Vec2,
94}
95
96#[cfg(feature = "bytemuck")]
97unsafe impl bytemuck::Zeroable for Aabb {}
98#[cfg(feature = "bytemuck")]
99unsafe impl bytemuck::Pod for Aabb {}
100
101#[cfg(feature = "bytemuck")]
102const _: () = {
103    assert!(core::mem::size_of::<Aabb>() == 16);
104    assert!(core::mem::align_of::<Aabb>() == 4);
105};
106
107impl Aabb {
108    #[inline]
109    pub fn from_raw(raw: ffi::b2AABB) -> Self {
110        Self {
111            lower: Vec2::from_raw(raw.lowerBound),
112            upper: Vec2::from_raw(raw.upperBound),
113        }
114    }
115
116    #[inline]
117    pub fn into_raw(self) -> ffi::b2AABB {
118        ffi::b2AABB {
119            lowerBound: self.lower.into_raw(),
120            upperBound: self.upper.into_raw(),
121        }
122    }
123
124    /// Create an AABB from lower and upper points.
125    #[inline]
126    pub fn new<L: Into<Vec2>, U: Into<Vec2>>(lower: L, upper: U) -> Self {
127        Self {
128            lower: lower.into(),
129            upper: upper.into(),
130        }
131    }
132    /// Create an AABB from center and half-extents (both in world units).
133    #[inline]
134    pub fn from_center_half_extents<C: Into<Vec2>, H: Into<Vec2>>(center: C, half: H) -> Self {
135        let c = center.into();
136        let h = half.into();
137        Self {
138            lower: Vec2::new(c.x - h.x, c.y - h.y),
139            upper: Vec2::new(c.x + h.x, c.y + h.y),
140        }
141    }
142}
143
144#[cfg(feature = "mint")]
145impl From<Aabb> for (mint::Point2<f32>, mint::Point2<f32>) {
146    #[inline]
147    fn from(a: Aabb) -> Self {
148        (a.lower.into(), a.upper.into())
149    }
150}
151
152#[cfg(feature = "mint")]
153impl From<(mint::Point2<f32>, mint::Point2<f32>)> for Aabb {
154    #[inline]
155    fn from((lower, upper): (mint::Point2<f32>, mint::Point2<f32>)) -> Self {
156        Self::new(lower, upper)
157    }
158}
159
160#[cfg(feature = "mint")]
161impl From<Aabb> for (mint::Vector2<f32>, mint::Vector2<f32>) {
162    #[inline]
163    fn from(a: Aabb) -> Self {
164        (a.lower.into(), a.upper.into())
165    }
166}
167
168#[cfg(feature = "mint")]
169impl From<(mint::Vector2<f32>, mint::Vector2<f32>)> for Aabb {
170    #[inline]
171    fn from((lower, upper): (mint::Vector2<f32>, mint::Vector2<f32>)) -> Self {
172        Self::new(lower, upper)
173    }
174}
175
176#[cfg(feature = "glam")]
177impl From<Aabb> for (glam::Vec2, glam::Vec2) {
178    #[inline]
179    fn from(a: Aabb) -> Self {
180        (a.lower.into(), a.upper.into())
181    }
182}
183
184#[cfg(feature = "glam")]
185impl From<(glam::Vec2, glam::Vec2)> for Aabb {
186    #[inline]
187    fn from((lower, upper): (glam::Vec2, glam::Vec2)) -> Self {
188        Self {
189            lower: lower.into(),
190            upper: upper.into(),
191        }
192    }
193}
194
195#[cfg(feature = "cgmath")]
196impl From<Aabb> for (cgmath::Point2<f32>, cgmath::Point2<f32>) {
197    #[inline]
198    fn from(a: Aabb) -> Self {
199        (a.lower.into(), a.upper.into())
200    }
201}
202
203#[cfg(feature = "cgmath")]
204impl From<(cgmath::Point2<f32>, cgmath::Point2<f32>)> for Aabb {
205    #[inline]
206    fn from((lower, upper): (cgmath::Point2<f32>, cgmath::Point2<f32>)) -> Self {
207        Self::new(lower, upper)
208    }
209}
210
211#[cfg(feature = "cgmath")]
212impl From<Aabb> for (cgmath::Vector2<f32>, cgmath::Vector2<f32>) {
213    #[inline]
214    fn from(a: Aabb) -> Self {
215        (a.lower.into(), a.upper.into())
216    }
217}
218
219#[cfg(feature = "cgmath")]
220impl From<(cgmath::Vector2<f32>, cgmath::Vector2<f32>)> for Aabb {
221    #[inline]
222    fn from((lower, upper): (cgmath::Vector2<f32>, cgmath::Vector2<f32>)) -> Self {
223        Self::new(lower, upper)
224    }
225}
226
227#[cfg(feature = "nalgebra")]
228impl From<Aabb> for (nalgebra::Point2<f32>, nalgebra::Point2<f32>) {
229    #[inline]
230    fn from(a: Aabb) -> Self {
231        (a.lower.into(), a.upper.into())
232    }
233}
234
235#[cfg(feature = "nalgebra")]
236impl From<(nalgebra::Point2<f32>, nalgebra::Point2<f32>)> for Aabb {
237    #[inline]
238    fn from((lower, upper): (nalgebra::Point2<f32>, nalgebra::Point2<f32>)) -> Self {
239        Self::new(lower, upper)
240    }
241}
242
243#[cfg(feature = "nalgebra")]
244impl From<Aabb> for (nalgebra::Vector2<f32>, nalgebra::Vector2<f32>) {
245    #[inline]
246    fn from(a: Aabb) -> Self {
247        (a.lower.into(), a.upper.into())
248    }
249}
250
251#[cfg(feature = "nalgebra")]
252impl From<(nalgebra::Vector2<f32>, nalgebra::Vector2<f32>)> for Aabb {
253    #[inline]
254    fn from((lower, upper): (nalgebra::Vector2<f32>, nalgebra::Vector2<f32>)) -> Self {
255        Self::new(lower, upper)
256    }
257}
258
259/// Filter for queries
260#[doc(alias = "query_filter")]
261#[derive(Copy, Clone, Debug)]
262pub struct QueryFilter(pub(crate) ffi::b2QueryFilter);
263
264impl Default for QueryFilter {
265    fn default() -> Self {
266        Self(unsafe { ffi::b2DefaultQueryFilter() })
267    }
268}
269
270#[cfg(feature = "serde")]
271impl serde::Serialize for QueryFilter {
272    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
273    where
274        S: serde::Serializer,
275    {
276        #[derive(serde::Serialize)]
277        struct Repr {
278            category_bits: u64,
279            mask_bits: u64,
280        }
281        Repr {
282            category_bits: self.0.categoryBits,
283            mask_bits: self.0.maskBits,
284        }
285        .serialize(serializer)
286    }
287}
288
289#[cfg(feature = "serde")]
290impl<'de> serde::Deserialize<'de> for QueryFilter {
291    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
292    where
293        D: serde::Deserializer<'de>,
294    {
295        #[derive(serde::Deserialize)]
296        struct Repr {
297            category_bits: u64,
298            mask_bits: u64,
299        }
300        let r = Repr::deserialize(deserializer)?;
301        Ok(Self(ffi::b2QueryFilter {
302            categoryBits: r.category_bits,
303            maskBits: r.mask_bits,
304        }))
305    }
306}
307
308impl QueryFilter {
309    pub fn category_bits(&self) -> u64 {
310        self.0.categoryBits
311    }
312
313    pub fn mask_bits(&self) -> u64 {
314        self.0.maskBits
315    }
316
317    pub fn mask(mut self, bits: u64) -> Self {
318        self.0.maskBits = bits;
319        self
320    }
321    pub fn category(mut self, bits: u64) -> Self {
322        self.0.categoryBits = bits;
323        self
324    }
325}
326
327/// Result of a closest ray cast
328#[doc(alias = "ray_result")]
329#[derive(Copy, Clone, Debug)]
330pub struct RayResult {
331    pub shape_id: ShapeId,
332    pub point: Vec2,
333    pub normal: Vec2,
334    pub fraction: f32,
335    pub hit: bool,
336}
337
338impl RayResult {
339    #[inline]
340    pub fn from_raw(raw: ffi::b2RayResult) -> Self {
341        Self {
342            shape_id: ShapeId::from_raw(raw.shapeId),
343            point: Vec2::from_raw(raw.point),
344            normal: Vec2::from_raw(raw.normal),
345            fraction: raw.fraction,
346            hit: raw.hit,
347        }
348    }
349}
350
351/// A collision plane used by Box2D's character mover helpers.
352#[doc(alias = "plane")]
353#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
354#[repr(C)]
355#[derive(Copy, Clone, Debug, PartialEq)]
356pub struct Plane {
357    pub normal: Vec2,
358    pub offset: f32,
359}
360
361impl Plane {
362    #[inline]
363    pub fn new<N: Into<Vec2>>(normal: N, offset: f32) -> Self {
364        Self {
365            normal: normal.into(),
366            offset,
367        }
368    }
369
370    #[inline]
371    pub fn is_valid(self) -> bool {
372        unsafe { ffi::b2IsValidPlane(self.into_raw()) }
373    }
374
375    #[inline]
376    pub fn from_raw(raw: ffi::b2Plane) -> Self {
377        Self {
378            normal: Vec2::from_raw(raw.normal),
379            offset: raw.offset,
380        }
381    }
382
383    #[inline]
384    pub fn into_raw(self) -> ffi::b2Plane {
385        ffi::b2Plane {
386            normal: self.normal.into_raw(),
387            offset: self.offset,
388        }
389    }
390}
391
392#[cfg(feature = "bytemuck")]
393unsafe impl bytemuck::Zeroable for Plane {}
394#[cfg(feature = "bytemuck")]
395unsafe impl bytemuck::Pod for Plane {}
396
397const _: () = {
398    assert!(core::mem::size_of::<Plane>() == core::mem::size_of::<ffi::b2Plane>());
399    assert!(core::mem::align_of::<Plane>() == core::mem::align_of::<ffi::b2Plane>());
400};
401
402/// Result item returned by `collide_mover`.
403#[doc(alias = "plane_result")]
404#[derive(Copy, Clone, Debug)]
405pub struct MoverPlaneResult {
406    pub shape_id: ShapeId,
407    pub plane: Plane,
408    pub point: Vec2,
409    pub hit: bool,
410}
411
412impl MoverPlaneResult {
413    /// Convert a valid mover-plane result into a collision plane for `solve_planes`.
414    ///
415    /// Returns `None` when `hit` is `false`, matching Box2D's guidance to ignore that result.
416    #[inline]
417    pub fn into_collision_plane(
418        self,
419        push_limit: f32,
420        clip_velocity: bool,
421    ) -> Option<CollisionPlane> {
422        self.hit
423            .then(|| CollisionPlane::new(self.plane, push_limit, clip_velocity))
424    }
425
426    /// Convert a valid mover-plane result into a rigid collision plane.
427    ///
428    /// This uses `f32::MAX` as the push limit and enables velocity clipping.
429    #[inline]
430    pub fn into_rigid_collision_plane(self) -> Option<CollisionPlane> {
431        self.into_collision_plane(CollisionPlane::RIGID_PUSH_LIMIT, true)
432    }
433}
434
435/// Collision plane input for `solve_planes` and `clip_vector`.
436#[doc(alias = "collision_plane")]
437#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
438#[repr(C)]
439#[derive(Copy, Clone, Debug, PartialEq)]
440pub struct CollisionPlane {
441    pub plane: Plane,
442    pub push_limit: f32,
443    pub push: f32,
444    pub clip_velocity: bool,
445}
446
447impl CollisionPlane {
448    pub const RIGID_PUSH_LIMIT: f32 = f32::MAX;
449
450    #[inline]
451    pub fn new(plane: Plane, push_limit: f32, clip_velocity: bool) -> Self {
452        Self {
453            plane,
454            push_limit,
455            push: 0.0,
456            clip_velocity,
457        }
458    }
459
460    #[inline]
461    pub fn rigid(plane: Plane) -> Self {
462        Self::new(plane, Self::RIGID_PUSH_LIMIT, true)
463    }
464
465    /// Validate this collision plane for Box2D mover solver helpers.
466    pub fn validate(&self) -> ApiResult<()> {
467        check_query_collision_plane_valid(self)
468    }
469
470    #[inline]
471    pub fn from_raw(raw: ffi::b2CollisionPlane) -> Self {
472        Self {
473            plane: Plane::from_raw(raw.plane),
474            push_limit: raw.pushLimit,
475            push: raw.push,
476            clip_velocity: raw.clipVelocity,
477        }
478    }
479
480    #[inline]
481    pub fn into_raw(self) -> ffi::b2CollisionPlane {
482        ffi::b2CollisionPlane {
483            plane: self.plane.into_raw(),
484            pushLimit: self.push_limit,
485            push: self.push,
486            clipVelocity: self.clip_velocity,
487        }
488    }
489}
490
491#[inline]
492pub(super) fn assert_query_solver_collision_plane_valid(plane: &CollisionPlane) {
493    assert!(
494        check_query_solver_collision_plane_valid(plane).is_ok(),
495        "collision plane must be solver-valid, got {:?}",
496        plane
497    );
498}
499
500#[inline]
501pub(super) fn check_query_solver_collision_plane_valid(plane: &CollisionPlane) -> ApiResult<()> {
502    if !plane.plane.is_valid() {
503        return Err(crate::error::ApiError::InvalidArgument);
504    }
505    check_query_non_negative_finite_scalar(plane.push_limit)
506}
507
508#[inline]
509pub(super) fn assert_query_collision_plane_valid(plane: &CollisionPlane) {
510    assert!(
511        check_query_collision_plane_valid(plane).is_ok(),
512        "collision plane must be valid, got {:?}",
513        plane
514    );
515}
516
517#[inline]
518pub(super) fn check_query_collision_plane_valid(plane: &CollisionPlane) -> ApiResult<()> {
519    check_query_solver_collision_plane_valid(plane)?;
520    check_query_non_negative_finite_scalar(plane.push)
521}
522
523const _: () = {
524    assert!(
525        core::mem::size_of::<CollisionPlane>() == core::mem::size_of::<ffi::b2CollisionPlane>()
526    );
527    assert!(
528        core::mem::align_of::<CollisionPlane>() == core::mem::align_of::<ffi::b2CollisionPlane>()
529    );
530};
531
532/// Result returned by `solve_planes`.
533#[doc(alias = "plane_solver_result")]
534#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
535#[derive(Copy, Clone, Debug, PartialEq)]
536pub struct PlaneSolverResult {
537    pub translation: Vec2,
538    pub iteration_count: i32,
539}
540
541impl PlaneSolverResult {
542    #[inline]
543    pub fn from_raw(raw: ffi::b2PlaneSolverResult) -> Self {
544        Self {
545            translation: Vec2::from_raw(raw.translation),
546            iteration_count: raw.iterationCount,
547        }
548    }
549}
550
551#[inline]
552pub(super) fn raw_collision_planes_mut(
553    planes: &mut [CollisionPlane],
554) -> *mut ffi::b2CollisionPlane {
555    if planes.is_empty() {
556        core::ptr::null_mut()
557    } else {
558        planes.as_mut_ptr().cast()
559    }
560}
561
562#[inline]
563pub(super) fn raw_collision_planes(planes: &[CollisionPlane]) -> *const ffi::b2CollisionPlane {
564    if planes.is_empty() {
565        core::ptr::null()
566    } else {
567        planes.as_ptr().cast()
568    }
569}
570
571/// Solve the translation that best satisfies the supplied mover collision planes.
572///
573/// The `push` field on each collision plane is updated in place by Box2D.
574#[inline]
575pub fn solve_planes<V: Into<Vec2>>(
576    target_delta: V,
577    planes: &mut [CollisionPlane],
578) -> PlaneSolverResult {
579    let target_delta = target_delta.into();
580    assert_query_vec2_valid("target_delta", target_delta);
581    for plane in planes.iter() {
582        assert_query_solver_collision_plane_valid(plane);
583    }
584    let raw = unsafe {
585        ffi::b2SolvePlanes(
586            target_delta.into_raw(),
587            raw_collision_planes_mut(planes),
588            planes.len() as i32,
589        )
590    };
591    PlaneSolverResult::from_raw(raw)
592}
593
594/// Solve the translation that best satisfies the supplied mover collision planes.
595///
596/// Returns `ApiError::InvalidArgument` when `target_delta` or any collision plane is invalid.
597#[inline]
598pub fn try_solve_planes<V: Into<Vec2>>(
599    target_delta: V,
600    planes: &mut [CollisionPlane],
601) -> ApiResult<PlaneSolverResult> {
602    let target_delta = target_delta.into();
603    check_query_vec2_valid(target_delta)?;
604    for plane in planes.iter() {
605        check_query_solver_collision_plane_valid(plane)?;
606    }
607    let raw = unsafe {
608        ffi::b2SolvePlanes(
609            target_delta.into_raw(),
610            raw_collision_planes_mut(planes),
611            planes.len() as i32,
612        )
613    };
614    Ok(PlaneSolverResult::from_raw(raw))
615}
616
617/// Clip a velocity or movement vector against solved collision planes.
618#[inline]
619pub fn clip_vector<V: Into<Vec2>>(vector: V, planes: &[CollisionPlane]) -> Vec2 {
620    let vector = vector.into();
621    assert_query_vec2_valid("vector", vector);
622    for plane in planes.iter() {
623        assert_query_collision_plane_valid(plane);
624    }
625    Vec2::from_raw(unsafe {
626        ffi::b2ClipVector(
627            vector.into_raw(),
628            raw_collision_planes(planes),
629            planes.len() as i32,
630        )
631    })
632}
633
634/// Clip a velocity or movement vector against solved collision planes.
635///
636/// Returns `ApiError::InvalidArgument` when `vector` or any collision plane state is invalid.
637#[inline]
638pub fn try_clip_vector<V: Into<Vec2>>(vector: V, planes: &[CollisionPlane]) -> ApiResult<Vec2> {
639    let vector = vector.into();
640    check_query_vec2_valid(vector)?;
641    for plane in planes.iter() {
642        check_query_collision_plane_valid(plane)?;
643    }
644    Ok(Vec2::from_raw(unsafe {
645        ffi::b2ClipVector(
646            vector.into_raw(),
647            raw_collision_planes(planes),
648            planes.len() as i32,
649        )
650    }))
651}