Skip to main content

boxdd/query/
types.rs

1use crate::core::foundation::transient_native_lease;
2use crate::error::Result;
3use crate::types::{Position, ShapeId, Vec2};
4use boxdd_sys::ffi;
5
6pub(super) fn minimum_mover_radius() -> Result<f32> {
7    Ok(0.01 * crate::core::foundation::current_length_units_per_meter()?)
8}
9
10pub(super) fn check_query_vec2_valid(
11    operation: &'static str,
12    argument: &'static str,
13    value: Vec2,
14) -> Result<()> {
15    if value.is_valid() {
16        Ok(())
17    } else {
18        Err(crate::error::Error::invalid_argument(
19            operation,
20            argument,
21            "a finite vector",
22        ))
23    }
24}
25
26pub(super) fn check_query_position_valid(
27    operation: &'static str,
28    argument: &'static str,
29    value: Position,
30) -> Result<()> {
31    if value.is_valid() {
32        Ok(())
33    } else {
34        Err(crate::error::Error::invalid_argument(
35            operation,
36            argument,
37            "a finite world position",
38        ))
39    }
40}
41pub(super) fn check_query_aabb_valid(operation: &'static str, aabb: Aabb) -> Result<()> {
42    if aabb.is_valid() {
43        Ok(())
44    } else {
45        Err(crate::error::Error::invalid_argument(
46            operation,
47            "aabb",
48            "finite ordered lower and upper bounds",
49        ))
50    }
51}
52
53#[inline]
54pub(super) fn check_query_non_negative_finite_scalar(
55    operation: &'static str,
56    argument: &'static str,
57    value: f32,
58) -> Result<()> {
59    if crate::is_valid_float(value) && value >= 0.0 {
60        Ok(())
61    } else {
62        Err(crate::error::Error::invalid_argument(
63            operation,
64            argument,
65            "a finite value greater than or equal to zero",
66        ))
67    }
68}
69
70#[inline]
71pub(super) fn check_query_mover_radius_valid(operation: &'static str, radius: f32) -> Result<()> {
72    if crate::is_valid_float(radius) && radius > minimum_mover_radius()? {
73        Ok(())
74    } else {
75        Err(crate::error::Error::invalid_argument(
76            operation,
77            "radius",
78            "a finite value greater than the configured minimum mover radius",
79        ))
80    }
81}
82
83/// Axis-aligned bounding box with `f32` coordinates in both precision modes.
84///
85/// When Box2D computes one from a [`crate::WorldTransform`] in double-precision mode, it narrows
86/// the absolute bounds outward so this conservative box still contains the represented geometry.
87#[doc(alias = "aabb")]
88#[cfg_attr(feature = "serde", derive(serde::Serialize))]
89#[repr(C)]
90#[derive(Copy, Clone, Debug, PartialEq)]
91pub struct Aabb {
92    pub(crate) lower: Vec2,
93    pub(crate) upper: Vec2,
94}
95
96#[cfg(feature = "bytemuck")]
97unsafe impl bytemuck::Zeroable for Aabb {}
98#[cfg(feature = "bytemuck")]
99const _: () = {
100    assert!(core::mem::size_of::<Aabb>() == 16);
101    assert!(core::mem::align_of::<Aabb>() == 4);
102};
103
104impl Aabb {
105    #[inline]
106    /// Construct from a raw Box2D AABB after validating its invariants.
107    pub fn from_raw(raw: ffi::b2AABB) -> Result<Self> {
108        let aabb = Self {
109            lower: Vec2::from_raw(raw.lowerBound),
110            upper: Vec2::from_raw(raw.upperBound),
111        };
112        check_query_aabb_valid("Aabb::from_raw", aabb)?;
113        Ok(aabb)
114    }
115
116    #[inline]
117    pub(crate) fn from_raw_unvalidated(raw: ffi::b2AABB) -> Self {
118        Self {
119            lower: Vec2::from_raw(raw.lowerBound),
120            upper: Vec2::from_raw(raw.upperBound),
121        }
122    }
123
124    #[inline]
125    pub const fn lower(self) -> Vec2 {
126        self.lower
127    }
128
129    #[inline]
130    pub const fn upper(self) -> Vec2 {
131        self.upper
132    }
133
134    #[inline]
135    pub fn into_raw(self) -> ffi::b2AABB {
136        ffi::b2AABB {
137            lowerBound: self.lower.into_raw(),
138            upperBound: self.upper.into_raw(),
139        }
140    }
141
142    /// Create an AABB from lower and upper points.
143    #[inline]
144    pub fn new<L: Into<Vec2>, U: Into<Vec2>>(lower: L, upper: U) -> Result<Self> {
145        let aabb = Self {
146            lower: lower.into(),
147            upper: upper.into(),
148        };
149        check_query_aabb_valid("Aabb::new", aabb)?;
150        Ok(aabb)
151    }
152    /// Create an AABB from center and half-extents (both in world units).
153    #[inline]
154    pub fn from_center_half_extents<C: Into<Vec2>, H: Into<Vec2>>(
155        center: C,
156        half: H,
157    ) -> Result<Self> {
158        let c = center.into();
159        let h = half.into();
160        let aabb = Self {
161            lower: Vec2::new(c.x - h.x, c.y - h.y),
162            upper: Vec2::new(c.x + h.x, c.y + h.y),
163        };
164        check_query_aabb_valid("Aabb::from_center_half_extents", aabb)?;
165        Ok(aabb)
166    }
167}
168
169#[cfg(feature = "serde")]
170impl<'de> serde::Deserialize<'de> for Aabb {
171    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
172    where
173        D: serde::Deserializer<'de>,
174    {
175        #[derive(serde::Deserialize)]
176        struct Repr {
177            lower: Vec2,
178            upper: Vec2,
179        }
180
181        let repr = <Repr as serde::Deserialize>::deserialize(deserializer)?;
182        Self::new(repr.lower, repr.upper).map_err(serde::de::Error::custom)
183    }
184}
185
186#[cfg(feature = "mint")]
187impl From<Aabb> for (mint::Point2<f32>, mint::Point2<f32>) {
188    #[inline]
189    fn from(a: Aabb) -> Self {
190        (a.lower.into(), a.upper.into())
191    }
192}
193
194#[cfg(feature = "mint")]
195impl TryFrom<(mint::Point2<f32>, mint::Point2<f32>)> for Aabb {
196    type Error = crate::Error;
197
198    #[inline]
199    fn try_from((lower, upper): (mint::Point2<f32>, mint::Point2<f32>)) -> Result<Self> {
200        Self::new(lower, upper)
201    }
202}
203
204#[cfg(feature = "mint")]
205impl From<Aabb> for (mint::Vector2<f32>, mint::Vector2<f32>) {
206    #[inline]
207    fn from(a: Aabb) -> Self {
208        (a.lower.into(), a.upper.into())
209    }
210}
211
212#[cfg(feature = "mint")]
213impl TryFrom<(mint::Vector2<f32>, mint::Vector2<f32>)> for Aabb {
214    type Error = crate::Error;
215
216    #[inline]
217    fn try_from((lower, upper): (mint::Vector2<f32>, mint::Vector2<f32>)) -> Result<Self> {
218        Self::new(lower, upper)
219    }
220}
221
222#[cfg(feature = "glam")]
223impl From<Aabb> for (glam::Vec2, glam::Vec2) {
224    #[inline]
225    fn from(a: Aabb) -> Self {
226        (a.lower.into(), a.upper.into())
227    }
228}
229
230#[cfg(feature = "glam")]
231impl TryFrom<(glam::Vec2, glam::Vec2)> for Aabb {
232    type Error = crate::Error;
233
234    #[inline]
235    fn try_from((lower, upper): (glam::Vec2, glam::Vec2)) -> Result<Self> {
236        Self::new(lower, upper)
237    }
238}
239
240#[cfg(feature = "nalgebra")]
241impl From<Aabb> for (nalgebra::Point2<f32>, nalgebra::Point2<f32>) {
242    #[inline]
243    fn from(a: Aabb) -> Self {
244        (a.lower.into(), a.upper.into())
245    }
246}
247
248#[cfg(feature = "nalgebra")]
249impl TryFrom<(nalgebra::Point2<f32>, nalgebra::Point2<f32>)> for Aabb {
250    type Error = crate::Error;
251
252    #[inline]
253    fn try_from((lower, upper): (nalgebra::Point2<f32>, nalgebra::Point2<f32>)) -> Result<Self> {
254        Self::new(lower, upper)
255    }
256}
257
258#[cfg(feature = "nalgebra")]
259impl From<Aabb> for (nalgebra::Vector2<f32>, nalgebra::Vector2<f32>) {
260    #[inline]
261    fn from(a: Aabb) -> Self {
262        (a.lower.into(), a.upper.into())
263    }
264}
265
266#[cfg(feature = "nalgebra")]
267impl TryFrom<(nalgebra::Vector2<f32>, nalgebra::Vector2<f32>)> for Aabb {
268    type Error = crate::Error;
269
270    #[inline]
271    fn try_from((lower, upper): (nalgebra::Vector2<f32>, nalgebra::Vector2<f32>)) -> Result<Self> {
272        Self::new(lower, upper)
273    }
274}
275
276/// Filter for queries
277#[doc(alias = "query_filter")]
278#[derive(Copy, Clone, Debug)]
279pub struct QueryFilter(pub(crate) ffi::b2QueryFilter);
280
281impl Default for QueryFilter {
282    fn default() -> Self {
283        Self(crate::core::native_defaults::query_filter())
284    }
285}
286
287#[cfg(feature = "serde")]
288impl serde::Serialize for QueryFilter {
289    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
290    where
291        S: serde::Serializer,
292    {
293        #[derive(serde::Serialize)]
294        struct Repr {
295            category_bits: u64,
296            mask_bits: u64,
297        }
298        Repr {
299            category_bits: self.0.categoryBits,
300            mask_bits: self.0.maskBits,
301        }
302        .serialize(serializer)
303    }
304}
305
306#[cfg(feature = "serde")]
307impl<'de> serde::Deserialize<'de> for QueryFilter {
308    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
309    where
310        D: serde::Deserializer<'de>,
311    {
312        #[derive(serde::Deserialize)]
313        struct Repr {
314            category_bits: u64,
315            mask_bits: u64,
316        }
317        let r = <Repr as serde::Deserialize>::deserialize(deserializer)?;
318        Ok(Self(ffi::b2QueryFilter {
319            categoryBits: r.category_bits,
320            maskBits: r.mask_bits,
321        }))
322    }
323}
324
325impl QueryFilter {
326    /// Create a query filter using Box2D's defaults.
327    #[inline]
328    pub fn new() -> Self {
329        Self::default()
330    }
331
332    pub fn category_bits(&self) -> u64 {
333        self.0.categoryBits
334    }
335
336    pub fn mask_bits(&self) -> u64 {
337        self.0.maskBits
338    }
339
340    pub fn mask(mut self, bits: u64) -> Self {
341        self.0.maskBits = bits;
342        self
343    }
344    pub fn category(mut self, bits: u64) -> Self {
345        self.0.categoryBits = bits;
346        self
347    }
348}
349
350/// Result of a closest ray cast
351#[doc(alias = "ray_result")]
352#[derive(Copy, Clone, Debug)]
353pub struct RayResult {
354    pub shape_id: ShapeId,
355    /// Absolute world-space hit position.
356    pub point: Position,
357    /// Surface normal in world orientation.
358    ///
359    /// This is a unit vector for a regular hit. Box2D returns [`Vec2::ZERO`] together with a zero
360    /// [`Self::fraction`] when a shape cast starts in overlap.
361    pub normal: Vec2,
362    /// Fraction of the query translation at which the hit occurred.
363    pub fraction: f32,
364    pub hit: bool,
365}
366
367/// Complete result of a closest ray cast, including broad-phase traversal statistics.
368///
369/// The statistics are retained even when [`Self::hit`] is `None`.
370#[doc(alias = "closest_ray_cast_result")]
371#[derive(Copy, Clone, Debug)]
372pub struct ClosestRayCastResult {
373    /// Closest shape hit, or `None` when the ray did not hit a shape.
374    pub hit: Option<RayResult>,
375    /// Number of broad-phase tree nodes visited by Box2D.
376    pub node_visits: i32,
377    /// Number of broad-phase leaves visited by Box2D.
378    pub leaf_visits: i32,
379}
380
381/// A collision plane used by Box2D's character mover helpers.
382#[doc(alias = "plane")]
383#[cfg_attr(feature = "serde", derive(serde::Serialize))]
384#[repr(C)]
385#[derive(Copy, Clone, Debug, PartialEq)]
386pub struct Plane {
387    pub(crate) normal: Vec2,
388    pub(crate) offset: f32,
389}
390
391impl Plane {
392    #[inline]
393    pub fn new<N: Into<Vec2>>(normal: N, offset: f32) -> Result<Self> {
394        let plane = Self {
395            normal: normal.into(),
396            offset,
397        };
398        if plane.is_valid() {
399            Ok(plane)
400        } else {
401            Err(crate::Error::invalid_argument(
402                "Plane::new",
403                "normal/offset",
404                "a finite plane with a unit normal",
405            ))
406        }
407    }
408
409    /// Return whether this plane is valid for Box2D mover algorithms.
410    #[inline]
411    pub fn is_valid(self) -> bool {
412        self.normal.is_valid()
413            && (1.0 - (self.normal.x * self.normal.x + self.normal.y * self.normal.y)).abs()
414                < 100.0 * f32::EPSILON
415            && self.offset.is_finite()
416    }
417
418    #[inline]
419    /// Construct from a raw Box2D plane after validating its invariants.
420    pub fn from_raw(raw: ffi::b2Plane) -> Result<Self> {
421        let plane = Self {
422            normal: Vec2::from_raw(raw.normal),
423            offset: raw.offset,
424        };
425        if plane.is_valid() {
426            Ok(plane)
427        } else {
428            Err(crate::Error::invalid_argument(
429                "Plane::from_raw",
430                "raw",
431                "a finite plane with a unit normal",
432            ))
433        }
434    }
435
436    #[inline]
437    pub(crate) fn from_raw_unvalidated(raw: ffi::b2Plane) -> Self {
438        Self {
439            normal: Vec2::from_raw(raw.normal),
440            offset: raw.offset,
441        }
442    }
443
444    #[inline]
445    pub const fn normal(self) -> Vec2 {
446        self.normal
447    }
448
449    #[inline]
450    pub const fn offset(self) -> f32 {
451        self.offset
452    }
453
454    #[inline]
455    pub fn into_raw(self) -> ffi::b2Plane {
456        ffi::b2Plane {
457            normal: self.normal.into_raw(),
458            offset: self.offset,
459        }
460    }
461}
462
463#[cfg(feature = "serde")]
464impl<'de> serde::Deserialize<'de> for Plane {
465    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
466    where
467        D: serde::Deserializer<'de>,
468    {
469        #[derive(serde::Deserialize)]
470        struct Repr {
471            normal: Vec2,
472            offset: f32,
473        }
474
475        let repr = <Repr as serde::Deserialize>::deserialize(deserializer)?;
476        Self::new(repr.normal, repr.offset).map_err(serde::de::Error::custom)
477    }
478}
479
480const _: () = {
481    assert!(core::mem::size_of::<Plane>() == core::mem::size_of::<ffi::b2Plane>());
482    assert!(core::mem::align_of::<Plane>() == core::mem::align_of::<ffi::b2Plane>());
483};
484
485/// Result item returned by `collide_mover`.
486#[doc(alias = "plane_result")]
487#[derive(Copy, Clone, Debug)]
488pub struct MoverPlaneResult {
489    pub shape_id: ShapeId,
490    pub plane: Plane,
491    /// Contact point relative to the `origin` supplied to `collide_mover`.
492    pub point: Vec2,
493    pub hit: bool,
494}
495
496impl MoverPlaneResult {
497    /// Convert a valid mover-plane result into a collision plane for `solve_planes`.
498    ///
499    /// Returns `None` when `hit` is `false`, matching Box2D's guidance to ignore that result.
500    #[inline]
501    pub fn into_collision_plane(
502        self,
503        push_limit: f32,
504        clip_velocity: bool,
505    ) -> Result<Option<CollisionPlane>> {
506        self.hit
507            .then(|| CollisionPlane::new(self.plane, push_limit, clip_velocity))
508            .transpose()
509    }
510
511    /// Convert a valid mover-plane result into a rigid collision plane.
512    ///
513    /// This uses `f32::MAX` as the push limit and enables velocity clipping.
514    #[inline]
515    pub fn into_rigid_collision_plane(self) -> Result<Option<CollisionPlane>> {
516        self.into_collision_plane(CollisionPlane::RIGID_PUSH_LIMIT, true)
517    }
518}
519
520/// Collision plane input for `solve_planes` and `clip_vector`.
521#[doc(alias = "collision_plane")]
522#[cfg_attr(feature = "serde", derive(serde::Serialize))]
523#[repr(C)]
524#[derive(Copy, Clone, Debug, PartialEq)]
525pub struct CollisionPlane {
526    pub(crate) plane: Plane,
527    pub(crate) push_limit: f32,
528    pub(crate) push: f32,
529    pub(crate) clip_velocity: bool,
530}
531
532impl CollisionPlane {
533    pub const RIGID_PUSH_LIMIT: f32 = f32::MAX;
534
535    #[inline]
536    pub fn new(plane: Plane, push_limit: f32, clip_velocity: bool) -> Result<Self> {
537        let collision_plane = Self {
538            plane,
539            push_limit,
540            push: 0.0,
541            clip_velocity,
542        };
543        check_query_collision_plane_valid("CollisionPlane::new", &collision_plane)?;
544        Ok(collision_plane)
545    }
546
547    #[inline]
548    pub fn rigid(plane: Plane) -> Result<Self> {
549        Self::new(plane, Self::RIGID_PUSH_LIMIT, true)
550    }
551
552    /// Validate this collision plane for Box2D mover solver helpers.
553    pub fn validate(&self) -> Result<()> {
554        check_query_collision_plane_valid("CollisionPlane::validate", self)
555    }
556
557    #[inline]
558    /// Construct from a raw Box2D collision plane after validating its invariants.
559    pub fn from_raw(raw: ffi::b2CollisionPlane) -> Result<Self> {
560        let plane = Self::from_raw_unvalidated(raw);
561        check_query_collision_plane_valid("CollisionPlane::from_raw", &plane)?;
562        Ok(plane)
563    }
564
565    #[inline]
566    pub(crate) fn from_raw_unvalidated(raw: ffi::b2CollisionPlane) -> Self {
567        Self {
568            plane: Plane::from_raw_unvalidated(raw.plane),
569            push_limit: raw.pushLimit,
570            push: raw.push,
571            clip_velocity: raw.clipVelocity,
572        }
573    }
574
575    #[inline]
576    pub const fn plane(self) -> Plane {
577        self.plane
578    }
579
580    #[inline]
581    pub const fn push_limit(self) -> f32 {
582        self.push_limit
583    }
584
585    #[inline]
586    pub const fn push(self) -> f32 {
587        self.push
588    }
589
590    #[inline]
591    pub const fn clip_velocity(self) -> bool {
592        self.clip_velocity
593    }
594
595    #[inline]
596    pub fn into_raw(self) -> ffi::b2CollisionPlane {
597        ffi::b2CollisionPlane {
598            plane: self.plane.into_raw(),
599            pushLimit: self.push_limit,
600            push: self.push,
601            clipVelocity: self.clip_velocity,
602        }
603    }
604}
605
606#[cfg(feature = "serde")]
607impl<'de> serde::Deserialize<'de> for CollisionPlane {
608    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
609    where
610        D: serde::Deserializer<'de>,
611    {
612        #[derive(serde::Deserialize)]
613        struct Repr {
614            plane: Plane,
615            push_limit: f32,
616            push: f32,
617            clip_velocity: bool,
618        }
619
620        let repr = <Repr as serde::Deserialize>::deserialize(deserializer)?;
621        let plane = Self {
622            plane: repr.plane,
623            push_limit: repr.push_limit,
624            push: repr.push,
625            clip_velocity: repr.clip_velocity,
626        };
627        check_query_collision_plane_valid("CollisionPlane::deserialize", &plane)
628            .map_err(serde::de::Error::custom)?;
629        Ok(plane)
630    }
631}
632
633#[inline]
634pub(super) fn check_query_solver_collision_plane_valid(
635    operation: &'static str,
636    plane: &CollisionPlane,
637) -> Result<()> {
638    if !plane.plane.is_valid() {
639        return Err(crate::error::Error::invalid_argument(
640            operation,
641            "planes[].plane",
642            "a finite plane with a unit normal",
643        ));
644    }
645    check_query_non_negative_finite_scalar(operation, "planes[].push_limit", plane.push_limit)
646}
647
648#[inline]
649pub(super) fn check_query_collision_plane_valid(
650    operation: &'static str,
651    plane: &CollisionPlane,
652) -> Result<()> {
653    check_query_solver_collision_plane_valid(operation, plane)?;
654    check_query_non_negative_finite_scalar(operation, "planes[].push", plane.push)
655}
656
657const _: () = {
658    assert!(
659        core::mem::size_of::<CollisionPlane>() == core::mem::size_of::<ffi::b2CollisionPlane>()
660    );
661    assert!(
662        core::mem::align_of::<CollisionPlane>() == core::mem::align_of::<ffi::b2CollisionPlane>()
663    );
664};
665
666/// Result returned by `solve_planes`.
667#[doc(alias = "plane_solver_result")]
668#[cfg_attr(feature = "serde", derive(serde::Serialize))]
669#[derive(Copy, Clone, Debug, PartialEq)]
670pub struct PlaneSolverResult {
671    translation: Vec2,
672    iteration_count: i32,
673}
674
675impl PlaneSolverResult {
676    /// Construct a checked plane-solver result.
677    #[inline]
678    pub fn new<T: Into<Vec2>>(translation: T, iteration_count: i32) -> Result<Self> {
679        let result = Self {
680            translation: translation.into(),
681            iteration_count,
682        };
683        result.validate_for("PlaneSolverResult::new")?;
684        Ok(result)
685    }
686
687    /// Construct from a raw Box2D result after validating its invariants.
688    #[inline]
689    pub fn from_raw(raw: ffi::b2PlaneSolverResult) -> Result<Self> {
690        let result = Self::from_raw_unvalidated(raw);
691        result.validate_for("PlaneSolverResult::from_raw")?;
692        Ok(result)
693    }
694
695    #[inline]
696    fn from_native(operation: &'static str, raw: ffi::b2PlaneSolverResult) -> Result<Self> {
697        let result = Self::from_raw_unvalidated(raw);
698        if !result.translation.is_valid() {
699            return Err(crate::error::Error::InvalidNativeOutput {
700                operation,
701                output: "translation",
702                constraint: "a finite vector",
703            });
704        }
705        if result.iteration_count < 0 {
706            return Err(crate::error::Error::InvalidNativeOutput {
707                operation,
708                output: "iteration_count",
709                constraint: "a non-negative native int",
710            });
711        }
712        Ok(result)
713    }
714
715    #[inline]
716    fn from_raw_unvalidated(raw: ffi::b2PlaneSolverResult) -> Self {
717        Self {
718            translation: Vec2::from_raw(raw.translation),
719            iteration_count: raw.iterationCount,
720        }
721    }
722
723    /// Validate the result's public invariants.
724    #[inline]
725    pub fn validate(&self) -> Result<()> {
726        self.validate_for("PlaneSolverResult::validate")
727    }
728
729    #[inline]
730    fn validate_for(&self, operation: &'static str) -> Result<()> {
731        check_query_vec2_valid(operation, "translation", self.translation)?;
732        if self.iteration_count < 0 {
733            return Err(crate::error::Error::invalid_argument(
734                operation,
735                "iteration_count",
736                "a non-negative native int",
737            ));
738        }
739        Ok(())
740    }
741
742    /// Return the translation selected by the solver.
743    #[inline]
744    pub const fn translation(self) -> Vec2 {
745        self.translation
746    }
747
748    /// Return the number of solver iterations.
749    #[inline]
750    pub const fn iteration_count(self) -> i32 {
751        self.iteration_count
752    }
753
754    /// Convert this result into its raw Box2D representation.
755    #[inline]
756    pub fn into_raw(self) -> ffi::b2PlaneSolverResult {
757        ffi::b2PlaneSolverResult {
758            translation: self.translation.into_raw(),
759            iterationCount: self.iteration_count,
760        }
761    }
762}
763
764#[cfg(feature = "serde")]
765impl<'de> serde::Deserialize<'de> for PlaneSolverResult {
766    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
767    where
768        D: serde::Deserializer<'de>,
769    {
770        #[derive(serde::Deserialize)]
771        struct Repr {
772            translation: Vec2,
773            iteration_count: i32,
774        }
775
776        let repr = <Repr as serde::Deserialize>::deserialize(deserializer)?;
777        Self::new(repr.translation, repr.iteration_count).map_err(serde::de::Error::custom)
778    }
779}
780
781#[inline]
782pub(super) fn raw_collision_planes(planes: &[CollisionPlane]) -> *const ffi::b2CollisionPlane {
783    if planes.is_empty() {
784        core::ptr::null()
785    } else {
786        planes.as_ptr().cast()
787    }
788}
789
790fn commit_native_solved_planes(
791    planes: &mut [CollisionPlane],
792    raw_planes: Vec<ffi::b2CollisionPlane>,
793) -> Result<()> {
794    debug_assert_eq!(planes.len(), raw_planes.len());
795    if raw_planes
796        .iter()
797        .copied()
798        .any(|plane| CollisionPlane::from_raw(plane).is_err())
799    {
800        return Err(crate::error::Error::InvalidNativeOutput {
801            operation: "solve_planes",
802            output: "planes",
803            constraint: "finite valid collision planes with non-negative push values",
804        });
805    }
806    for (plane, raw) in planes.iter_mut().zip(raw_planes) {
807        *plane = CollisionPlane::from_raw_unvalidated(raw);
808    }
809    Ok(())
810}
811
812#[inline]
813fn check_collision_plane_count(operation: &'static str, count: usize) -> Result<i32> {
814    i32::try_from(count).map_err(|_| {
815        crate::error::Error::invalid_argument(
816            operation,
817            "planes",
818            "a slice length representable by a native int",
819        )
820    })
821}
822
823/// Solve the translation that best satisfies the supplied mover collision planes.
824///
825/// The `push` field on each collision plane is updated in place by Box2D.
826/// Invalid values, callback re-entry, and unavailable native foundation state are reported.
827#[inline]
828pub fn solve_planes<V: Into<Vec2>>(
829    target_delta: V,
830    planes: &mut [CollisionPlane],
831) -> Result<PlaneSolverResult> {
832    let target_delta = target_delta.into();
833    check_query_vec2_valid("solve_planes", "target_delta", target_delta)?;
834    let plane_count = check_collision_plane_count("solve_planes", planes.len())?;
835    for plane in planes.iter() {
836        check_query_solver_collision_plane_valid("solve_planes", plane)?;
837    }
838    let mut raw_planes = Vec::new();
839    raw_planes
840        .try_reserve_exact(planes.len())
841        .map_err(|_| crate::error::Error::FfiOutputAllocationFailed)?;
842    raw_planes.extend(planes.iter().copied().map(CollisionPlane::into_raw));
843    let _lease = transient_native_lease()?;
844    let raw = unsafe {
845        ffi::b2SolvePlanes(
846            target_delta.into_raw(),
847            if raw_planes.is_empty() {
848                core::ptr::null_mut()
849            } else {
850                raw_planes.as_mut_ptr()
851            },
852            plane_count,
853        )
854    };
855    let result = PlaneSolverResult::from_native("solve_planes", raw)?;
856    commit_native_solved_planes(planes, raw_planes)?;
857    Ok(result)
858}
859
860/// Clip a velocity or movement vector against solved collision planes.
861#[inline]
862pub fn clip_vector<V: Into<Vec2>>(vector: V, planes: &[CollisionPlane]) -> Result<Vec2> {
863    let vector = vector.into();
864    check_query_vec2_valid("clip_vector", "vector", vector)?;
865    let plane_count = check_collision_plane_count("clip_vector", planes.len())?;
866    for plane in planes.iter() {
867        check_query_collision_plane_valid("clip_vector", plane)?;
868    }
869    let _lease = transient_native_lease()?;
870    let clipped = Vec2::from_raw(unsafe {
871        ffi::b2ClipVector(vector.into_raw(), raw_collision_planes(planes), plane_count)
872    });
873    if clipped.is_valid() {
874        Ok(clipped)
875    } else {
876        Err(crate::error::Error::InvalidNativeOutput {
877            operation: "clip_vector",
878            output: "vector",
879            constraint: "a finite vector",
880        })
881    }
882}
883
884#[cfg(test)]
885mod tests {
886    use super::*;
887    use core::sync::atomic::{AtomicBool, Ordering};
888    use std::sync::Arc;
889
890    struct TrackedVec2 {
891        converted: Arc<AtomicBool>,
892        value: Vec2,
893    }
894
895    impl From<TrackedVec2> for Vec2 {
896        fn from(value: TrackedVec2) -> Self {
897            value.converted.store(true, Ordering::Relaxed);
898            value.value
899        }
900    }
901
902    fn tracked(value: Vec2) -> (TrackedVec2, Arc<AtomicBool>) {
903        let converted = Arc::new(AtomicBool::new(false));
904        (
905            TrackedVec2 {
906                converted: Arc::clone(&converted),
907                value,
908            },
909            converted,
910        )
911    }
912
913    #[test]
914    fn query_filter_default_is_pure_and_callback_safe() {
915        let _callback_guard = crate::core::callback_state::CallbackGuard::enter();
916        let filter = QueryFilter::default();
917        assert_eq!(filter.category_bits(), 1);
918        assert_eq!(filter.mask_bits(), u64::MAX);
919    }
920
921    #[test]
922    fn plane_validation_matches_box2d_normalization_tolerance() {
923        assert!(Plane::new([1.0, 0.0], 0.0).unwrap().is_valid());
924        assert!(Plane::new([1.000_005, 0.0], 0.0).unwrap().is_valid());
925        assert!(Plane::new([1.000_01, 0.0], 0.0).is_err());
926        assert!(Plane::new([f32::NAN, 0.0], 0.0).is_err());
927        assert!(Plane::new([1.0, 0.0], f32::INFINITY).is_err());
928    }
929
930    #[test]
931    fn invalid_native_solver_planes_are_not_partially_published() {
932        let plane = Plane::new([0.0, 1.0], 0.0).unwrap();
933        let original = [
934            CollisionPlane::new(plane, 1.0, true).unwrap(),
935            CollisionPlane::new(plane, 2.0, false).unwrap(),
936        ];
937        let mut output = original;
938        let mut first = original[0].into_raw();
939        first.push = 0.5;
940        let mut second = original[1].into_raw();
941        second.push = f32::NAN;
942
943        assert!(matches!(
944            commit_native_solved_planes(&mut output, vec![first, second]),
945            Err(crate::Error::InvalidNativeOutput {
946                operation: "solve_planes",
947                output: "planes",
948                ..
949            })
950        ));
951        assert_eq!(output, original);
952    }
953
954    #[test]
955    fn pure_mover_validation_is_callback_safe_and_native_calls_reject_reentry() {
956        let plane = Plane::new([0.0, 1.0], 0.0).unwrap();
957        let mut planes = [CollisionPlane::rigid(plane).unwrap()];
958        let _callback_guard = crate::core::callback_state::CallbackGuard::enter();
959
960        assert!(plane.is_valid());
961        assert_eq!(planes[0].validate(), Ok(()));
962
963        let (target, target_converted) = tracked(Vec2::new(0.0, -0.2));
964        assert_eq!(
965            solve_planes(target, &mut planes),
966            Err(crate::error::Error::InCallback)
967        );
968        assert!(target_converted.load(Ordering::Relaxed));
969
970        let (vector, vector_converted) = tracked(Vec2::new(0.0, -1.0));
971        assert_eq!(
972            clip_vector(vector, &planes),
973            Err(crate::error::Error::InCallback)
974        );
975        assert!(vector_converted.load(Ordering::Relaxed));
976
977        let mut invalid_planes = [CollisionPlane {
978            plane: Plane {
979                normal: Vec2::new(0.0, 2.0),
980                offset: 0.0,
981            },
982            push_limit: CollisionPlane::RIGID_PUSH_LIMIT,
983            push: 0.0,
984            clip_velocity: true,
985        }];
986        assert_eq!(
987            solve_planes(Vec2::ZERO, &mut invalid_planes),
988            Err(crate::error::Error::invalid_argument(
989                "solve_planes",
990                "planes[].plane",
991                "a finite plane with a unit normal",
992            ))
993        );
994    }
995}