Skip to main content

boxdd/
collision.rs

1//! Standalone low-level collision geometry helpers.
2//!
3//! This module wraps Box2D's standalone collision algorithms without exposing raw FFI
4//! structs. It is intentionally more explicit than the high-level `World` query API and
5//! is useful when you want to run geometric tests or contact-manifold generation without
6//! a world instance.
7//!
8//! Pairwise distance, cast, and manifold APIs run in shape A's local frame: callers pass
9//! the transform and translation of B in A, and outputs are returned in A. World poses are
10//! deliberately outside this module's contract.
11
12use crate::{
13    core::{
14        foundation::transient_native_lease,
15        math::{Rot, Transform},
16    },
17    error::{Error, Result},
18    query::Aabb,
19    shapes::{Capsule, ChainSegment, Circle, Polygon, Segment},
20    types::Vec2,
21};
22use boxdd_sys::ffi;
23use core::fmt;
24
25/// Maximum number of points supported by a Box2D shape proxy.
26pub const MAX_SHAPE_PROXY_POINTS: usize = ffi::B2_MAX_POLYGON_VERTICES as usize;
27
28/// Maximum number of points in a standalone collision manifold.
29pub const MAX_LOCAL_MANIFOLD_POINTS: usize = 2;
30
31const _: () = {
32    assert!(core::mem::size_of::<Vec2>() == core::mem::size_of::<ffi::b2Vec2>());
33    assert!(core::mem::align_of::<Vec2>() == core::mem::align_of::<ffi::b2Vec2>());
34};
35
36#[inline]
37fn check_collision_vec2_valid(
38    operation: &'static str,
39    argument: &'static str,
40    value: Vec2,
41) -> Result<()> {
42    if value.is_valid() {
43        Ok(())
44    } else {
45        Err(Error::invalid_argument(
46            operation,
47            argument,
48            "a finite vector",
49        ))
50    }
51}
52
53#[inline]
54fn check_collision_rot_valid(
55    operation: &'static str,
56    argument: &'static str,
57    value: Rot,
58) -> Result<()> {
59    if value.is_valid() {
60        Ok(())
61    } else {
62        Err(Error::invalid_argument(
63            operation,
64            argument,
65            "a normalized finite rotation",
66        ))
67    }
68}
69
70#[inline]
71fn check_collision_transform_valid(operation: &'static str, value: Transform) -> Result<()> {
72    if value.is_valid() {
73        Ok(())
74    } else {
75        Err(Error::invalid_argument(
76            operation,
77            "transform_b_in_a",
78            "a finite rigid transform",
79        ))
80    }
81}
82
83#[inline]
84fn check_collision_non_negative_finite_scalar(
85    operation: &'static str,
86    argument: &'static str,
87    value: f32,
88) -> Result<()> {
89    if crate::is_valid_float(value) && value >= 0.0 {
90        Ok(())
91    } else {
92        Err(Error::invalid_argument(
93            operation,
94            argument,
95            "a finite value greater than or equal to zero",
96        ))
97    }
98}
99
100#[inline]
101fn check_collision_finite_scalar(
102    operation: &'static str,
103    argument: &'static str,
104    value: f32,
105) -> Result<()> {
106    if crate::is_valid_float(value) {
107        Ok(())
108    } else {
109        Err(Error::invalid_argument(
110            operation,
111            argument,
112            "a finite value",
113        ))
114    }
115}
116
117#[inline]
118fn check_collision_non_negative_int(
119    operation: &'static str,
120    argument: &'static str,
121    value: i32,
122) -> Result<()> {
123    if value >= 0 {
124        Ok(())
125    } else {
126        Err(Error::invalid_argument(
127            operation,
128            argument,
129            "a non-negative native int",
130        ))
131    }
132}
133
134#[inline]
135fn collision_unit_vector_is_valid(value: Vec2) -> bool {
136    value.is_valid() && (1.0 - (value.x * value.x + value.y * value.y)).abs() < 100.0 * f32::EPSILON
137}
138
139#[inline]
140fn check_collision_unit_interval_scalar(
141    operation: &'static str,
142    argument: &'static str,
143    value: f32,
144) -> Result<()> {
145    if crate::is_valid_float(value) && (0.0..=1.0).contains(&value) {
146        Ok(())
147    } else {
148        Err(Error::invalid_argument(
149            operation,
150            argument,
151            "a finite value in 0.0..=1.0",
152        ))
153    }
154}
155
156struct RayCastAxisInput {
157    origin: f32,
158    translation: f32,
159    lower: f32,
160    upper: f32,
161    enter_normal: Vec2,
162    exit_normal: Vec2,
163}
164
165struct RayCastAxisState {
166    tmin: f32,
167    tmax: f32,
168    normal: Vec2,
169}
170
171#[inline]
172fn ray_cast_axis(input: RayCastAxisInput, state: &mut RayCastAxisState) -> bool {
173    if input.translation.abs() < f32::EPSILON {
174        return input.lower <= input.origin && input.origin <= input.upper;
175    }
176
177    let inv_translation = 1.0 / input.translation;
178    let mut t1 = (input.lower - input.origin) * inv_translation;
179    let mut t2 = (input.upper - input.origin) * inv_translation;
180    let mut n1 = input.enter_normal;
181    let mut n2 = input.exit_normal;
182
183    if t1 > t2 {
184        core::mem::swap(&mut t1, &mut t2);
185        core::mem::swap(&mut n1, &mut n2);
186    }
187
188    if t1 > state.tmin {
189        state.tmin = t1;
190        state.normal = n1;
191    }
192
193    if t2 < state.tmax {
194        state.tmax = t2;
195    }
196
197    state.tmin <= state.tmax
198}
199
200/// A Box2D point-cloud proxy used by distance, shape-cast, and TOI algorithms.
201///
202/// Construction fails when the iterator is empty, contains more than
203/// [`MAX_SHAPE_PROXY_POINTS`] points, or contains invalid Box2D coordinates/radius data.
204#[doc(alias = "shape_proxy")]
205#[derive(Copy, Clone)]
206pub struct ShapeProxy {
207    raw: ffi::b2ShapeProxy,
208}
209
210impl ShapeProxy {
211    /// Build a proxy from `1..=MAX_SHAPE_PROXY_POINTS` valid points and an external radius.
212    pub fn new<I, P>(points: I, radius: f32) -> Result<Self>
213    where
214        I: IntoIterator<Item = P>,
215        P: Into<Vec2>,
216    {
217        let (raw_points, count) = collect_shape_proxy_points("ShapeProxy::new", points, radius)?;
218        Ok(Self {
219            raw: ffi::b2ShapeProxy {
220                points: raw_points,
221                count,
222                radius,
223            },
224        })
225    }
226
227    /// Build a proxy and apply a rigid transform to every stored point.
228    #[doc(alias = "b2MakeOffsetProxy")]
229    pub fn offset_from_points<I, P>(points: I, radius: f32, transform: Transform) -> Result<Self>
230    where
231        I: IntoIterator<Item = P>,
232        P: Into<Vec2>,
233    {
234        Self::offset_from_points_for(
235            "ShapeProxy::offset_from_points",
236            "points/transform",
237            points,
238            radius,
239            transform,
240        )
241    }
242
243    pub(crate) fn offset_from_points_for<I, P>(
244        operation: &'static str,
245        transformed_argument: &'static str,
246        points: I,
247        radius: f32,
248        transform: Transform,
249    ) -> Result<Self>
250    where
251        I: IntoIterator<Item = P>,
252        P: Into<Vec2>,
253    {
254        let (mut raw_points, count) = collect_shape_proxy_points(operation, points, radius)?;
255        if !transform.is_valid() {
256            return Err(Error::invalid_argument(
257                operation,
258                "transform",
259                "a finite rigid transform",
260            ));
261        }
262        for point in raw_points.iter_mut().take(count as usize) {
263            let transformed = transform.transform_point(Vec2::from_raw(*point));
264            if !transformed.is_valid() {
265                return Err(Error::invalid_argument(
266                    operation,
267                    transformed_argument,
268                    "a transform whose proxy points remain finite",
269                ));
270            }
271            *point = transformed.into_raw();
272        }
273        Ok(Self {
274            raw: ffi::b2ShapeProxy {
275                points: raw_points,
276                count,
277                radius,
278            },
279        })
280    }
281
282    /// The points stored in this proxy.
283    #[inline]
284    pub fn points(&self) -> &[Vec2] {
285        let count = self.count();
286        unsafe { core::slice::from_raw_parts(self.raw.points.as_ptr().cast::<Vec2>(), count) }
287    }
288
289    /// The number of points stored in this proxy.
290    #[inline]
291    pub fn count(&self) -> usize {
292        self.raw.count.clamp(0, MAX_SHAPE_PROXY_POINTS as i32) as usize
293    }
294
295    /// The proxy's external radius.
296    #[inline]
297    pub fn radius(&self) -> f32 {
298        self.raw.radius
299    }
300
301    /// Validate this proxy for Box2D standalone collision algorithms.
302    pub fn validate(&self) -> Result<()> {
303        if !(1..=MAX_SHAPE_PROXY_POINTS as i32).contains(&self.raw.count) {
304            return Err(Error::invalid_argument(
305                "ShapeProxy::validate",
306                "points",
307                "between 1 and Box2D's maximum shape-proxy point count",
308            ));
309        }
310        check_collision_non_negative_finite_scalar(
311            "ShapeProxy::validate",
312            "radius",
313            self.raw.radius,
314        )?;
315        for point in self.points().iter().copied() {
316            check_collision_vec2_valid("ShapeProxy::validate", "points", point)?;
317        }
318        Ok(())
319    }
320
321    #[inline]
322    pub(crate) fn into_raw(self) -> ffi::b2ShapeProxy {
323        self.raw
324    }
325
326    #[inline]
327    fn raw(self) -> ffi::b2ShapeProxy {
328        self.into_raw()
329    }
330}
331
332fn collect_shape_proxy_points<I, P>(
333    operation: &'static str,
334    points: I,
335    radius: f32,
336) -> Result<([ffi::b2Vec2; MAX_SHAPE_PROXY_POINTS], i32)>
337where
338    I: IntoIterator<Item = P>,
339    P: Into<Vec2>,
340{
341    check_collision_non_negative_finite_scalar(operation, "radius", radius)?;
342    let mut raw_points = [ffi::b2Vec2 { x: 0.0, y: 0.0 }; MAX_SHAPE_PROXY_POINTS];
343    let mut count = 0usize;
344
345    for point in points {
346        if count == MAX_SHAPE_PROXY_POINTS {
347            return Err(Error::invalid_argument(
348                operation,
349                "points",
350                "no more than Box2D's maximum shape-proxy point count",
351            ));
352        }
353        let point = point.into();
354        check_collision_vec2_valid(operation, "points", point)?;
355        raw_points[count] = point.into_raw();
356        count += 1;
357    }
358
359    if count == 0 {
360        return Err(Error::invalid_argument(
361            operation,
362            "points",
363            "at least one point",
364        ));
365    }
366
367    Ok((raw_points, count as i32))
368}
369
370impl fmt::Debug for ShapeProxy {
371    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
372        f.debug_struct("ShapeProxy")
373            .field("points", &self.points())
374            .field("radius", &self.radius())
375            .finish()
376    }
377}
378
379/// Input for shape-specific casts against circles, capsules, segments, and polygons.
380#[doc(alias = "shape_cast_input")]
381#[derive(Copy, Clone, Debug)]
382pub struct ShapeCastInput {
383    pub(crate) proxy: ShapeProxy,
384    pub(crate) translation: Vec2,
385    pub(crate) max_fraction: f32,
386    pub(crate) can_encroach: bool,
387}
388
389impl ShapeCastInput {
390    /// Build a shape cast over `proxy` moving by `translation`.
391    #[inline]
392    pub fn new<T: Into<Vec2>>(proxy: ShapeProxy, translation: T) -> Result<Self> {
393        let input = Self {
394            proxy,
395            translation: translation.into(),
396            max_fraction: 1.0,
397            can_encroach: false,
398        };
399        input.validate()?;
400        Ok(input)
401    }
402
403    /// Limit the portion of `translation` considered by the cast.
404    #[inline]
405    pub fn with_max_fraction(mut self, max_fraction: f32) -> Result<Self> {
406        check_collision_unit_interval_scalar(
407            "ShapeCastInput::with_max_fraction",
408            "max_fraction",
409            max_fraction,
410        )?;
411        self.max_fraction = max_fraction;
412        Ok(self)
413    }
414
415    /// Allow encroachment when initially touching.
416    #[inline]
417    pub fn with_can_encroach(mut self, can_encroach: bool) -> Self {
418        self.can_encroach = can_encroach;
419        self
420    }
421
422    #[inline]
423    pub const fn proxy(self) -> ShapeProxy {
424        self.proxy
425    }
426
427    #[inline]
428    pub const fn translation(self) -> Vec2 {
429        self.translation
430    }
431
432    #[inline]
433    pub const fn max_fraction(self) -> f32 {
434        self.max_fraction
435    }
436
437    #[inline]
438    pub const fn can_encroach(self) -> bool {
439        self.can_encroach
440    }
441
442    /// Validate this input before crossing the Box2D FFI boundary.
443    pub fn validate(&self) -> Result<()> {
444        self.proxy.validate()?;
445        check_collision_vec2_valid("ShapeCastInput::validate", "translation", self.translation)?;
446        check_collision_unit_interval_scalar(
447            "ShapeCastInput::validate",
448            "max_fraction",
449            self.max_fraction,
450        )
451    }
452
453    #[inline]
454    pub fn into_raw(self) -> ffi::b2ShapeCastInput {
455        ffi::b2ShapeCastInput {
456            proxy: self.proxy.into_raw(),
457            translation: self.translation.into_raw(),
458            maxFraction: self.max_fraction,
459            canEncroach: self.can_encroach,
460        }
461    }
462}
463
464/// Warm-start cache for repeated GJK distance calls.
465#[doc(alias = "simplex_cache")]
466#[derive(Copy, Clone)]
467pub struct SimplexCache {
468    raw: ffi::b2SimplexCache,
469}
470
471impl Default for SimplexCache {
472    fn default() -> Self {
473        Self {
474            raw: ffi::b2SimplexCache {
475                count: 0,
476                indexA: [0; 3],
477                indexB: [0; 3],
478            },
479        }
480    }
481}
482
483impl SimplexCache {
484    /// Create a zeroed cache for the first distance query.
485    #[inline]
486    pub fn new() -> Self {
487        Self::default()
488    }
489
490    /// Reset the cache to its initial zeroed state.
491    #[inline]
492    pub fn clear(&mut self) {
493        *self = Self::default();
494    }
495
496    /// The number of cached simplex points.
497    #[inline]
498    pub fn count(&self) -> usize {
499        self.raw.count.min(3) as usize
500    }
501
502    /// Cached simplex indices for shape A.
503    #[inline]
504    pub fn index_a(&self) -> &[u8] {
505        &self.raw.indexA[..self.count()]
506    }
507
508    /// Cached simplex indices for shape B.
509    #[inline]
510    pub fn index_b(&self) -> &[u8] {
511        &self.raw.indexB[..self.count()]
512    }
513
514    #[inline]
515    fn raw_mut(&mut self) -> *mut ffi::b2SimplexCache {
516        &mut self.raw
517    }
518
519    fn validate_for(
520        &self,
521        operation: &'static str,
522        proxy_a_count: usize,
523        proxy_b_count: usize,
524    ) -> Result<()> {
525        let count = usize::from(self.raw.count);
526        if count > 3 {
527            return Err(Error::invalid_argument(
528                operation,
529                "cache.count",
530                "a simplex point count in 0..=3",
531            ));
532        }
533        if self.raw.indexA[..count]
534            .iter()
535            .any(|index| usize::from(*index) >= proxy_a_count)
536        {
537            return Err(Error::invalid_argument(
538                operation,
539                "cache.index_a",
540                "indices within shape A's proxy points",
541            ));
542        }
543        if self.raw.indexB[..count]
544            .iter()
545            .any(|index| usize::from(*index) >= proxy_b_count)
546        {
547            return Err(Error::invalid_argument(
548                operation,
549                "cache.index_b",
550                "indices within shape B's proxy points",
551            ));
552        }
553        Ok(())
554    }
555
556    fn validate_native_for(
557        &self,
558        operation: &'static str,
559        proxy_a_count: usize,
560        proxy_b_count: usize,
561    ) -> Result<()> {
562        self.validate_for(operation, proxy_a_count, proxy_b_count)
563            .map_err(|_| Error::InvalidNativeOutput {
564                operation,
565                output: "simplex_cache",
566                constraint: "at most three in-range proxy point indices",
567            })
568    }
569}
570
571impl fmt::Debug for SimplexCache {
572    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
573        f.debug_struct("SimplexCache")
574            .field("count", &self.count())
575            .field("index_a", &self.index_a())
576            .field("index_b", &self.index_b())
577            .finish()
578    }
579}
580
581fn commit_native_simplex_cache(
582    cache: Option<&mut SimplexCache>,
583    staged: SimplexCache,
584    operation: &'static str,
585    proxy_a_count: usize,
586    proxy_b_count: usize,
587) -> Result<()> {
588    staged.validate_native_for(operation, proxy_a_count, proxy_b_count)?;
589    if let Some(cache) = cache {
590        *cache = staged;
591    }
592    Ok(())
593}
594
595/// A contact point expressed in shape A's local frame.
596///
597/// Unlike [`crate::types::ManifoldPoint`], this is purely geometric data from a
598/// standalone collision query. It does not contain solver impulses or persistence state.
599#[doc(alias = "local_manifold_point")]
600#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
601#[derive(Copy, Clone, Debug, Default, PartialEq)]
602pub struct LocalManifoldPoint {
603    /// Contact point in shape A's local frame.
604    pub point: Vec2,
605    /// Signed separation; negative values indicate penetration.
606    pub separation: f32,
607    /// Stable feature-pair identifier supplied by Box2D.
608    pub id: u16,
609}
610
611impl LocalManifoldPoint {
612    #[inline]
613    pub fn from_raw(raw: ffi::b2LocalManifoldPoint) -> Result<Self> {
614        let point = Self::from_raw_unvalidated(raw);
615        point.validate_for("LocalManifoldPoint::from_raw")?;
616        Ok(point)
617    }
618
619    #[inline]
620    const fn from_raw_unvalidated(raw: ffi::b2LocalManifoldPoint) -> Self {
621        Self {
622            point: Vec2::from_raw(raw.point),
623            separation: raw.separation,
624            id: raw.id,
625        }
626    }
627
628    pub fn validate(&self) -> Result<()> {
629        self.validate_for("LocalManifoldPoint::validate")
630    }
631
632    fn validate_for(&self, operation: &'static str) -> Result<()> {
633        check_collision_vec2_valid(operation, "point", self.point)?;
634        check_collision_finite_scalar(operation, "separation", self.separation)
635    }
636
637    #[inline]
638    pub const fn into_raw(self) -> ffi::b2LocalManifoldPoint {
639        ffi::b2LocalManifoldPoint {
640            point: self.point.into_raw(),
641            separation: self.separation,
642            id: self.id,
643        }
644    }
645}
646
647/// Pure geometric contact manifold expressed in shape A's local frame.
648///
649/// `normal` points from shape A to shape B whenever that direction is defined. For coincident
650/// geometry, Box2D emits a zero normal; Safe Rust replaces it with the deterministic positive-X
651/// unit vector. Every point returned by [`Self::points`] is also expressed in A's frame. Convert
652/// them with shape A's pose only at a presentation or world-query boundary; standalone collision
653/// never consumes world coordinates.
654#[doc(alias = "local_manifold")]
655#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
656#[derive(Copy, Clone, Debug, Default, PartialEq)]
657pub struct LocalManifold {
658    pub normal: Vec2,
659    pub contact_points: [LocalManifoldPoint; MAX_LOCAL_MANIFOLD_POINTS],
660    pub point_count: i32,
661}
662
663impl LocalManifold {
664    /// The initialized contact points in this manifold.
665    #[inline]
666    pub fn points(&self) -> &[LocalManifoldPoint] {
667        &self.contact_points[..self.point_count()]
668    }
669
670    /// The number of initialized contact points.
671    #[inline]
672    pub fn point_count(&self) -> usize {
673        self.point_count.clamp(0, MAX_LOCAL_MANIFOLD_POINTS as i32) as usize
674    }
675
676    /// Whether this query found no contact points.
677    #[inline]
678    pub fn is_empty(&self) -> bool {
679        self.point_count() == 0
680    }
681
682    #[inline]
683    pub fn from_raw(raw: ffi::b2LocalManifold) -> Result<Self> {
684        let manifold = Self::from_raw_unvalidated(raw);
685        manifold.validate_for("LocalManifold::from_raw")?;
686        Ok(manifold)
687    }
688
689    #[inline]
690    fn from_native(operation: &'static str, raw: ffi::b2LocalManifold) -> Result<Self> {
691        let mut manifold = Self::from_raw_unvalidated(raw);
692        // Box2D deliberately returns a zero normal for coincident centers. The contact direction
693        // is undefined there, so expose a deterministic unit vector instead of weakening the Safe
694        // value invariant or rejecting otherwise valid native output.
695        if manifold.point_count > 0 && manifold.normal == Vec2::ZERO {
696            manifold.normal = Vec2::new(1.0, 0.0);
697        }
698        manifold
699            .validate_for(operation)
700            .map_err(|_| Error::InvalidNativeOutput {
701                operation,
702                output: "local_manifold",
703                constraint: "zero to two finite contact points and a unit normal when non-empty",
704            })?;
705        Ok(manifold)
706    }
707
708    #[inline]
709    fn from_raw_unvalidated(raw: ffi::b2LocalManifold) -> Self {
710        Self {
711            normal: Vec2::from_raw(raw.normal),
712            contact_points: raw.points.map(LocalManifoldPoint::from_raw_unvalidated),
713            point_count: raw.pointCount,
714        }
715    }
716
717    pub fn validate(&self) -> Result<()> {
718        self.validate_for("LocalManifold::validate")
719    }
720
721    fn validate_for(&self, operation: &'static str) -> Result<()> {
722        if !(0..=MAX_LOCAL_MANIFOLD_POINTS as i32).contains(&self.point_count) {
723            return Err(Error::invalid_argument(
724                operation,
725                "point_count",
726                "a contact point count in 0..=2",
727            ));
728        }
729        check_collision_vec2_valid(operation, "normal", self.normal)?;
730        if self.point_count > 0 && !collision_unit_vector_is_valid(self.normal) {
731            return Err(Error::invalid_argument(
732                operation,
733                "normal",
734                "a finite unit vector when the manifold is non-empty",
735            ));
736        }
737        for point in self.points() {
738            point.validate_for(operation)?;
739        }
740        Ok(())
741    }
742
743    #[inline]
744    pub fn into_raw(self) -> ffi::b2LocalManifold {
745        ffi::b2LocalManifold {
746            normal: self.normal.into_raw(),
747            points: self.contact_points.map(LocalManifoldPoint::into_raw),
748            pointCount: self.point_count,
749        }
750    }
751}
752
753/// Result of [`segment_distance`].
754#[doc(alias = "segment_distance_result")]
755#[derive(Copy, Clone, Debug)]
756pub struct SegmentDistanceResult {
757    pub closest1: Vec2,
758    pub closest2: Vec2,
759    pub fraction1: f32,
760    pub fraction2: f32,
761    pub distance_squared: f32,
762}
763
764impl SegmentDistanceResult {
765    #[inline]
766    pub fn from_raw(raw: ffi::b2SegmentDistanceResult) -> Result<Self> {
767        let result = Self {
768            closest1: Vec2::from_raw(raw.closest1),
769            closest2: Vec2::from_raw(raw.closest2),
770            fraction1: raw.fraction1,
771            fraction2: raw.fraction2,
772            distance_squared: raw.distanceSquared,
773        };
774        result.validate_for("SegmentDistanceResult::from_raw")?;
775        Ok(result)
776    }
777
778    fn from_native(operation: &'static str, raw: ffi::b2SegmentDistanceResult) -> Result<Self> {
779        Self::from_raw(raw).map_err(|_| Error::InvalidNativeOutput {
780            operation,
781            output: "segment_distance",
782            constraint: "finite points, unit-interval fractions, and a non-negative squared distance",
783        })
784    }
785
786    pub fn validate(&self) -> Result<()> {
787        self.validate_for("SegmentDistanceResult::validate")
788    }
789
790    fn validate_for(&self, operation: &'static str) -> Result<()> {
791        check_collision_vec2_valid(operation, "closest1", self.closest1)?;
792        check_collision_vec2_valid(operation, "closest2", self.closest2)?;
793        check_collision_unit_interval_scalar(operation, "fraction1", self.fraction1)?;
794        check_collision_unit_interval_scalar(operation, "fraction2", self.fraction2)?;
795        check_collision_non_negative_finite_scalar(
796            operation,
797            "distance_squared",
798            self.distance_squared,
799        )
800    }
801}
802
803/// Low-level ray-cast or shape-cast output.
804#[doc(alias = "cast_output")]
805#[derive(Copy, Clone, Debug)]
806pub struct CastOutput {
807    pub normal: Vec2,
808    pub point: Vec2,
809    pub fraction: f32,
810    pub iterations: i32,
811    pub hit: bool,
812}
813
814impl CastOutput {
815    pub const MISS: Self = Self {
816        normal: Vec2::ZERO,
817        point: Vec2::ZERO,
818        fraction: 0.0,
819        iterations: 0,
820        hit: false,
821    };
822
823    #[inline]
824    pub fn from_raw(raw: ffi::b2CastOutput) -> Result<Self> {
825        let output = Self {
826            normal: Vec2::from_raw(raw.normal),
827            point: Vec2::from_raw(raw.point),
828            fraction: raw.fraction,
829            iterations: raw.iterations,
830            hit: raw.hit,
831        };
832        output.validate_for("CastOutput::from_raw")?;
833        Ok(output)
834    }
835
836    pub(crate) fn from_native(operation: &'static str, raw: ffi::b2CastOutput) -> Result<Self> {
837        let output = Self {
838            normal: Vec2::from_raw(raw.normal),
839            point: Vec2::from_raw(raw.point),
840            fraction: raw.fraction,
841            iterations: raw.iterations,
842            hit: raw.hit,
843        };
844        output
845            .validate_for(operation)
846            .map_err(|_| Error::InvalidNativeOutput {
847                operation,
848                output: "cast_output",
849                constraint: "finite hit data, a unit-interval fraction, and non-negative iterations",
850            })?;
851        Ok(output)
852    }
853
854    pub fn validate(&self) -> Result<()> {
855        self.validate_for("CastOutput::validate")
856    }
857
858    fn validate_for(&self, operation: &'static str) -> Result<()> {
859        check_collision_vec2_valid(operation, "normal", self.normal)?;
860        check_collision_vec2_valid(operation, "point", self.point)?;
861        check_collision_unit_interval_scalar(operation, "fraction", self.fraction)?;
862        check_collision_non_negative_int(operation, "iterations", self.iterations)?;
863        if self.hit && self.fraction > 0.0 && !collision_unit_vector_is_valid(self.normal) {
864            return Err(Error::invalid_argument(
865                operation,
866                "normal",
867                "a finite unit vector for a non-overlap hit",
868            ));
869        }
870        if self.hit
871            && self.fraction == 0.0
872            && self.normal != Vec2::ZERO
873            && !collision_unit_vector_is_valid(self.normal)
874        {
875            return Err(Error::invalid_argument(
876                operation,
877                "normal",
878                "a finite unit vector, or zero for an initial overlap",
879            ));
880        }
881        Ok(())
882    }
883}
884
885/// Input for [`shape_distance`], evaluated entirely in shape A's local frame.
886#[doc(alias = "distance_input")]
887#[derive(Copy, Clone, Debug)]
888pub struct DistanceInput {
889    pub(crate) proxy_a: ShapeProxy,
890    pub(crate) proxy_b: ShapeProxy,
891    /// Transform of shape B in shape A's local frame.
892    pub(crate) transform_b_in_a: Transform,
893    pub(crate) use_radii: bool,
894}
895
896impl DistanceInput {
897    /// Build distance input with `use_radii = false`.
898    ///
899    /// `transform_b_in_a` maps shape B's local coordinates into shape A's local frame.
900    #[inline]
901    pub fn new(
902        proxy_a: ShapeProxy,
903        proxy_b: ShapeProxy,
904        transform_b_in_a: Transform,
905    ) -> Result<Self> {
906        let input = Self {
907            proxy_a,
908            proxy_b,
909            transform_b_in_a,
910            use_radii: false,
911        };
912        input.validate()?;
913        Ok(input)
914    }
915
916    /// Set whether proxy radii should affect the distance result.
917    #[inline]
918    pub fn with_radii(mut self, use_radii: bool) -> Self {
919        self.use_radii = use_radii;
920        self
921    }
922
923    #[inline]
924    pub const fn proxy_a(self) -> ShapeProxy {
925        self.proxy_a
926    }
927
928    #[inline]
929    pub const fn proxy_b(self) -> ShapeProxy {
930        self.proxy_b
931    }
932
933    #[inline]
934    pub const fn transform_b_in_a(self) -> Transform {
935        self.transform_b_in_a
936    }
937
938    #[inline]
939    pub const fn use_radii(self) -> bool {
940        self.use_radii
941    }
942
943    /// Validate this input before crossing the Box2D FFI boundary.
944    pub fn validate(&self) -> Result<()> {
945        self.proxy_a.validate()?;
946        self.proxy_b.validate()?;
947        check_collision_transform_valid("DistanceInput::validate", self.transform_b_in_a)?;
948        Ok(())
949    }
950
951    #[inline]
952    pub fn into_raw(self) -> ffi::b2DistanceInput {
953        ffi::b2DistanceInput {
954            proxyA: self.proxy_a.raw(),
955            proxyB: self.proxy_b.raw(),
956            transform: self.transform_b_in_a.into_raw(),
957            useRadii: self.use_radii,
958        }
959    }
960}
961
962/// Output from [`shape_distance`], expressed in shape A's local frame.
963#[doc(alias = "distance_output")]
964#[derive(Copy, Clone, Debug)]
965pub struct DistanceOutput {
966    pub point_a: Vec2,
967    pub point_b: Vec2,
968    pub normal: Vec2,
969    pub distance: f32,
970    pub iterations: i32,
971    pub simplex_count: i32,
972}
973
974impl DistanceOutput {
975    #[inline]
976    pub fn from_raw(raw: ffi::b2DistanceOutput) -> Result<Self> {
977        let output = Self {
978            point_a: Vec2::from_raw(raw.pointA),
979            point_b: Vec2::from_raw(raw.pointB),
980            normal: Vec2::from_raw(raw.normal),
981            distance: raw.distance,
982            iterations: raw.iterations,
983            simplex_count: raw.simplexCount,
984        };
985        output.validate_for("DistanceOutput::from_raw")?;
986        Ok(output)
987    }
988
989    fn from_native(operation: &'static str, raw: ffi::b2DistanceOutput) -> Result<Self> {
990        let output = Self {
991            point_a: Vec2::from_raw(raw.pointA),
992            point_b: Vec2::from_raw(raw.pointB),
993            normal: Vec2::from_raw(raw.normal),
994            distance: raw.distance,
995            iterations: raw.iterations,
996            simplex_count: raw.simplexCount,
997        };
998        output
999            .validate_for(operation)
1000            .map_err(|_| Error::InvalidNativeOutput {
1001                operation,
1002                output: "distance_output",
1003                constraint: "finite points and distance with valid normal and non-negative counters",
1004            })?;
1005        Ok(output)
1006    }
1007
1008    pub fn validate(&self) -> Result<()> {
1009        self.validate_for("DistanceOutput::validate")
1010    }
1011
1012    fn validate_for(&self, operation: &'static str) -> Result<()> {
1013        check_collision_vec2_valid(operation, "point_a", self.point_a)?;
1014        check_collision_vec2_valid(operation, "point_b", self.point_b)?;
1015        check_collision_vec2_valid(operation, "normal", self.normal)?;
1016        check_collision_non_negative_finite_scalar(operation, "distance", self.distance)?;
1017        check_collision_non_negative_int(operation, "iterations", self.iterations)?;
1018        check_collision_non_negative_int(operation, "simplex_count", self.simplex_count)?;
1019        if self.distance > 0.0 && !collision_unit_vector_is_valid(self.normal) {
1020            return Err(Error::invalid_argument(
1021                operation,
1022                "normal",
1023                "a finite unit vector when distance is positive",
1024            ));
1025        }
1026        if self.distance == 0.0
1027            && self.normal != Vec2::ZERO
1028            && !collision_unit_vector_is_valid(self.normal)
1029        {
1030            return Err(Error::invalid_argument(
1031                operation,
1032                "normal",
1033                "a finite unit vector, or zero when distance is zero",
1034            ));
1035        }
1036        Ok(())
1037    }
1038}
1039
1040/// Input for [`shape_cast`], evaluated entirely in shape A's local frame.
1041#[doc(alias = "shape_cast_pair_input")]
1042#[derive(Copy, Clone, Debug)]
1043pub struct ShapeCastPairInput {
1044    pub(crate) proxy_a: ShapeProxy,
1045    pub(crate) proxy_b: ShapeProxy,
1046    /// Transform of shape B in shape A's local frame.
1047    pub(crate) transform_b_in_a: Transform,
1048    /// Translation of shape B expressed in shape A's local frame.
1049    pub(crate) translation_b_in_a: Vec2,
1050    pub(crate) max_fraction: f32,
1051    pub(crate) can_encroach: bool,
1052}
1053
1054impl ShapeCastPairInput {
1055    /// Build a shape cast where B starts at `transform_b_in_a` and moves by
1056    /// `translation_b_in_a`, both expressed in shape A's local frame.
1057    #[inline]
1058    pub fn new<V: Into<Vec2>>(
1059        proxy_a: ShapeProxy,
1060        proxy_b: ShapeProxy,
1061        transform_b_in_a: Transform,
1062        translation_b_in_a: V,
1063    ) -> Result<Self> {
1064        let input = Self {
1065            proxy_a,
1066            proxy_b,
1067            transform_b_in_a,
1068            translation_b_in_a: translation_b_in_a.into(),
1069            max_fraction: 1.0,
1070            can_encroach: false,
1071        };
1072        input.validate()?;
1073        Ok(input)
1074    }
1075
1076    /// Limit the portion of `translation_b_in_a` considered by the cast.
1077    #[inline]
1078    pub fn with_max_fraction(mut self, max_fraction: f32) -> Result<Self> {
1079        check_collision_unit_interval_scalar(
1080            "ShapeCastPairInput::with_max_fraction",
1081            "max_fraction",
1082            max_fraction,
1083        )?;
1084        self.max_fraction = max_fraction;
1085        Ok(self)
1086    }
1087
1088    /// Allow shapes with radius to encroach slightly when initially touching.
1089    #[inline]
1090    pub fn with_can_encroach(mut self, can_encroach: bool) -> Self {
1091        self.can_encroach = can_encroach;
1092        self
1093    }
1094
1095    #[inline]
1096    pub const fn proxy_a(self) -> ShapeProxy {
1097        self.proxy_a
1098    }
1099
1100    #[inline]
1101    pub const fn proxy_b(self) -> ShapeProxy {
1102        self.proxy_b
1103    }
1104
1105    #[inline]
1106    pub const fn transform_b_in_a(self) -> Transform {
1107        self.transform_b_in_a
1108    }
1109
1110    #[inline]
1111    pub const fn translation_b_in_a(self) -> Vec2 {
1112        self.translation_b_in_a
1113    }
1114
1115    #[inline]
1116    pub const fn max_fraction(self) -> f32 {
1117        self.max_fraction
1118    }
1119
1120    #[inline]
1121    pub const fn can_encroach(self) -> bool {
1122        self.can_encroach
1123    }
1124
1125    /// Validate this input before crossing the Box2D FFI boundary.
1126    pub fn validate(&self) -> Result<()> {
1127        self.proxy_a.validate()?;
1128        self.proxy_b.validate()?;
1129        check_collision_transform_valid("ShapeCastPairInput::validate", self.transform_b_in_a)?;
1130        check_collision_vec2_valid(
1131            "ShapeCastPairInput::validate",
1132            "translation_b_in_a",
1133            self.translation_b_in_a,
1134        )?;
1135        check_collision_unit_interval_scalar(
1136            "ShapeCastPairInput::validate",
1137            "max_fraction",
1138            self.max_fraction,
1139        )?;
1140        Ok(())
1141    }
1142
1143    #[inline]
1144    pub fn into_raw(self) -> ffi::b2ShapeCastPairInput {
1145        ffi::b2ShapeCastPairInput {
1146            proxyA: self.proxy_a.raw(),
1147            proxyB: self.proxy_b.raw(),
1148            transform: self.transform_b_in_a.into_raw(),
1149            translationB: self.translation_b_in_a.into_raw(),
1150            maxFraction: self.max_fraction,
1151            canEncroach: self.can_encroach,
1152        }
1153    }
1154}
1155
1156/// Sweep input used by continuous collision algorithms.
1157#[doc(alias = "sweep")]
1158#[derive(Copy, Clone, Debug)]
1159pub struct Sweep {
1160    pub(crate) local_center: Vec2,
1161    pub(crate) c1: Vec2,
1162    pub(crate) c2: Vec2,
1163    pub(crate) q1: Rot,
1164    pub(crate) q2: Rot,
1165}
1166
1167impl Sweep {
1168    #[inline]
1169    pub fn new<LC: Into<Vec2>, C1: Into<Vec2>, C2: Into<Vec2>>(
1170        local_center: LC,
1171        c1: C1,
1172        c2: C2,
1173        q1: Rot,
1174        q2: Rot,
1175    ) -> Result<Self> {
1176        let sweep = Self {
1177            local_center: local_center.into(),
1178            c1: c1.into(),
1179            c2: c2.into(),
1180            q1,
1181            q2,
1182        };
1183        sweep.validate()?;
1184        Ok(sweep)
1185    }
1186
1187    #[inline]
1188    /// Construct from a raw Box2D sweep after validating its invariants.
1189    pub fn from_raw(raw: ffi::b2Sweep) -> Result<Self> {
1190        let sweep = Self {
1191            local_center: Vec2::from_raw(raw.localCenter),
1192            c1: Vec2::from_raw(raw.c1),
1193            c2: Vec2::from_raw(raw.c2),
1194            q1: Rot::from_raw(raw.q1)?,
1195            q2: Rot::from_raw(raw.q2)?,
1196        };
1197        sweep.validate()?;
1198        Ok(sweep)
1199    }
1200
1201    #[inline]
1202    pub const fn local_center(self) -> Vec2 {
1203        self.local_center
1204    }
1205
1206    #[inline]
1207    pub const fn start_center(self) -> Vec2 {
1208        self.c1
1209    }
1210
1211    #[inline]
1212    pub const fn end_center(self) -> Vec2 {
1213        self.c2
1214    }
1215
1216    #[inline]
1217    pub const fn start_rotation(self) -> Rot {
1218        self.q1
1219    }
1220
1221    #[inline]
1222    pub const fn end_rotation(self) -> Rot {
1223        self.q2
1224    }
1225
1226    #[inline]
1227    pub fn into_raw(self) -> ffi::b2Sweep {
1228        ffi::b2Sweep {
1229            localCenter: self.local_center.into_raw(),
1230            c1: self.c1.into_raw(),
1231            c2: self.c2.into_raw(),
1232            q1: self.q1.into_raw(),
1233            q2: self.q2.into_raw(),
1234        }
1235    }
1236
1237    /// Validate this sweep for Box2D continuous-collision algorithms.
1238    pub fn validate(&self) -> Result<()> {
1239        check_collision_vec2_valid("Sweep::validate", "local_center", self.local_center)?;
1240        check_collision_vec2_valid("Sweep::validate", "c1", self.c1)?;
1241        check_collision_vec2_valid("Sweep::validate", "c2", self.c2)?;
1242        check_collision_rot_valid("Sweep::validate", "q1", self.q1)?;
1243        check_collision_rot_valid("Sweep::validate", "q2", self.q2)?;
1244        Ok(())
1245    }
1246
1247    /// Evaluate the sweep transform at `time` in the `[0, 1]` interval.
1248    ///
1249    /// The complete sweep and time are validated before foundation activity is leased and before
1250    /// Box2D is called.
1251    #[inline]
1252    pub fn transform_at(self, time: f32) -> Result<Transform> {
1253        self.validate()?;
1254        check_collision_unit_interval_scalar("Sweep::transform_at", "time", time)?;
1255        let _lease = transient_native_lease()?;
1256        let raw = self.into_raw();
1257        Transform::from_raw(unsafe { ffi::b2GetSweepTransform(&raw, time) }).map_err(|_| {
1258            Error::InvalidNativeOutput {
1259                operation: "Sweep::transform_at",
1260                output: "transform",
1261                constraint: "a finite rigid transform",
1262            }
1263        })
1264    }
1265}
1266
1267/// Input for [`time_of_impact`].
1268#[doc(alias = "toi_input")]
1269#[derive(Copy, Clone, Debug)]
1270pub struct ToiInput {
1271    pub(crate) proxy_a: ShapeProxy,
1272    pub(crate) proxy_b: ShapeProxy,
1273    pub(crate) sweep_a: Sweep,
1274    pub(crate) sweep_b: Sweep,
1275    pub(crate) max_fraction: f32,
1276}
1277
1278impl ToiInput {
1279    /// Build TOI input with `max_fraction = 1.0`.
1280    #[inline]
1281    pub fn new(
1282        proxy_a: ShapeProxy,
1283        proxy_b: ShapeProxy,
1284        sweep_a: Sweep,
1285        sweep_b: Sweep,
1286    ) -> Result<Self> {
1287        let input = Self {
1288            proxy_a,
1289            proxy_b,
1290            sweep_a,
1291            sweep_b,
1292            max_fraction: 1.0,
1293        };
1294        input.validate()?;
1295        Ok(input)
1296    }
1297
1298    /// Limit the sweep interval to `[0, max_fraction]`.
1299    #[inline]
1300    pub fn with_max_fraction(mut self, max_fraction: f32) -> Result<Self> {
1301        check_collision_unit_interval_scalar(
1302            "ToiInput::with_max_fraction",
1303            "max_fraction",
1304            max_fraction,
1305        )?;
1306        self.max_fraction = max_fraction;
1307        Ok(self)
1308    }
1309
1310    #[inline]
1311    pub const fn proxy_a(self) -> ShapeProxy {
1312        self.proxy_a
1313    }
1314
1315    #[inline]
1316    pub const fn proxy_b(self) -> ShapeProxy {
1317        self.proxy_b
1318    }
1319
1320    #[inline]
1321    pub const fn sweep_a(self) -> Sweep {
1322        self.sweep_a
1323    }
1324
1325    #[inline]
1326    pub const fn sweep_b(self) -> Sweep {
1327        self.sweep_b
1328    }
1329
1330    #[inline]
1331    pub const fn max_fraction(self) -> f32 {
1332        self.max_fraction
1333    }
1334
1335    /// Validate this input before crossing the Box2D FFI boundary.
1336    pub fn validate(&self) -> Result<()> {
1337        self.proxy_a.validate()?;
1338        self.proxy_b.validate()?;
1339        self.sweep_a.validate()?;
1340        self.sweep_b.validate()?;
1341        check_collision_unit_interval_scalar(
1342            "ToiInput::validate",
1343            "max_fraction",
1344            self.max_fraction,
1345        )?;
1346        Ok(())
1347    }
1348
1349    #[inline]
1350    pub fn into_raw(self) -> ffi::b2TOIInput {
1351        ffi::b2TOIInput {
1352            proxyA: self.proxy_a.raw(),
1353            proxyB: self.proxy_b.raw(),
1354            sweepA: self.sweep_a.into_raw(),
1355            sweepB: self.sweep_b.into_raw(),
1356            maxFraction: self.max_fraction,
1357        }
1358    }
1359}
1360
1361/// Result state from [`time_of_impact`].
1362#[doc(alias = "toi_state")]
1363#[repr(u32)]
1364#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1365pub enum ToiState {
1366    Unknown = ffi::b2TOIState_b2_toiStateUnknown,
1367    Failed = ffi::b2TOIState_b2_toiStateFailed,
1368    Overlapped = ffi::b2TOIState_b2_toiStateOverlapped,
1369    Hit = ffi::b2TOIState_b2_toiStateHit,
1370    Separated = ffi::b2TOIState_b2_toiStateSeparated,
1371}
1372
1373impl ToiState {
1374    #[inline]
1375    pub const fn from_raw(raw: ffi::b2TOIState) -> Option<Self> {
1376        match raw {
1377            ffi::b2TOIState_b2_toiStateUnknown => Some(Self::Unknown),
1378            ffi::b2TOIState_b2_toiStateFailed => Some(Self::Failed),
1379            ffi::b2TOIState_b2_toiStateOverlapped => Some(Self::Overlapped),
1380            ffi::b2TOIState_b2_toiStateHit => Some(Self::Hit),
1381            ffi::b2TOIState_b2_toiStateSeparated => Some(Self::Separated),
1382            _ => None,
1383        }
1384    }
1385}
1386
1387/// Output from [`time_of_impact`].
1388#[doc(alias = "toi_output")]
1389#[derive(Copy, Clone, Debug)]
1390pub struct ToiOutput {
1391    pub state: ToiState,
1392    pub point: Vec2,
1393    pub normal: Vec2,
1394    pub fraction: f32,
1395}
1396
1397impl ToiOutput {
1398    #[inline]
1399    pub fn from_raw(raw: ffi::b2TOIOutput) -> Result<Self> {
1400        let output = Self {
1401            state: ToiState::from_raw(raw.state).ok_or_else(|| {
1402                Error::invalid_argument("ToiOutput::from_raw", "state", "a known Box2D TOI state")
1403            })?,
1404            point: Vec2::from_raw(raw.point),
1405            normal: Vec2::from_raw(raw.normal),
1406            fraction: raw.fraction,
1407        };
1408        output.validate_for("ToiOutput::from_raw")?;
1409        Ok(output)
1410    }
1411
1412    fn from_native(operation: &'static str, raw: ffi::b2TOIOutput) -> Result<Self> {
1413        let state = ToiState::from_raw(raw.state).ok_or(Error::InvalidNativeOutput {
1414            operation,
1415            output: "toi_output.state",
1416            constraint: "a known Box2D TOI state",
1417        })?;
1418        let output = Self {
1419            state,
1420            point: Vec2::from_raw(raw.point),
1421            normal: Vec2::from_raw(raw.normal),
1422            fraction: raw.fraction,
1423        };
1424        output
1425            .validate_for(operation)
1426            .map_err(|_| Error::InvalidNativeOutput {
1427                operation,
1428                output: "toi_output",
1429                constraint: "finite hit data and a unit-interval fraction",
1430            })?;
1431        Ok(output)
1432    }
1433
1434    pub fn validate(&self) -> Result<()> {
1435        self.validate_for("ToiOutput::validate")
1436    }
1437
1438    fn validate_for(&self, operation: &'static str) -> Result<()> {
1439        check_collision_vec2_valid(operation, "point", self.point)?;
1440        check_collision_vec2_valid(operation, "normal", self.normal)?;
1441        check_collision_unit_interval_scalar(operation, "fraction", self.fraction)?;
1442        if self.state == ToiState::Hit && !collision_unit_vector_is_valid(self.normal) {
1443            return Err(Error::invalid_argument(
1444                operation,
1445                "normal",
1446                "a finite unit vector for a TOI hit",
1447            ));
1448        }
1449        Ok(())
1450    }
1451}
1452
1453/// Compute the closest points between two line segments.
1454pub fn segment_distance<P1, Q1, P2, Q2>(
1455    p1: P1,
1456    q1: Q1,
1457    p2: P2,
1458    q2: Q2,
1459) -> Result<SegmentDistanceResult>
1460where
1461    P1: Into<Vec2>,
1462    Q1: Into<Vec2>,
1463    P2: Into<Vec2>,
1464    Q2: Into<Vec2>,
1465{
1466    let p1 = p1.into();
1467    let q1 = q1.into();
1468    let p2 = p2.into();
1469    let q2 = q2.into();
1470    check_collision_vec2_valid("segment_distance", "p1", p1)?;
1471    check_collision_vec2_valid("segment_distance", "q1", q1)?;
1472    check_collision_vec2_valid("segment_distance", "p2", p2)?;
1473    check_collision_vec2_valid("segment_distance", "q2", q2)?;
1474    let _lease = transient_native_lease()?;
1475    SegmentDistanceResult::from_native("segment_distance", unsafe {
1476        ffi::b2SegmentDistance(p1.into_raw(), q1.into_raw(), p2.into_raw(), q2.into_raw())
1477    })
1478}
1479
1480/// Compute the closest distance between two shape proxies.
1481pub fn shape_distance(input: DistanceInput, cache: &mut SimplexCache) -> Result<DistanceOutput> {
1482    input.validate()?;
1483    let proxy_a_count = input.proxy_a.count();
1484    let proxy_b_count = input.proxy_b.count();
1485    cache.validate_for("shape_distance", proxy_a_count, proxy_b_count)?;
1486    let raw_input = input.into_raw();
1487    let mut staged_cache = *cache;
1488    let _lease = transient_native_lease()?;
1489    let output = DistanceOutput::from_native("shape_distance", unsafe {
1490        ffi::b2ShapeDistance(&raw_input, staged_cache.raw_mut(), core::ptr::null_mut(), 0)
1491    })?;
1492    if output.simplex_count != 0 {
1493        return Err(Error::InvalidNativeOutput {
1494            operation: "shape_distance",
1495            output: "distance_output.simplex_count",
1496            constraint: "zero when no simplex output buffer was supplied",
1497        });
1498    }
1499    commit_native_simplex_cache(
1500        Some(cache),
1501        staged_cache,
1502        "shape_distance",
1503        proxy_a_count,
1504        proxy_b_count,
1505    )?;
1506    Ok(output)
1507}
1508
1509/// Cast shape B against shape A.
1510///
1511/// The hit point and normal are returned in shape A's local frame.
1512pub fn shape_cast(input: ShapeCastPairInput) -> Result<CastOutput> {
1513    input.validate()?;
1514    let raw_input = input.into_raw();
1515    let _lease = transient_native_lease()?;
1516    CastOutput::from_native("shape_cast", unsafe { ffi::b2ShapeCast(&raw_input) })
1517}
1518
1519/// Compute the time of impact between two moving shape proxies.
1520pub fn time_of_impact(input: ToiInput) -> Result<ToiOutput> {
1521    input.validate()?;
1522    let raw_input = input.into_raw();
1523    let _lease = transient_native_lease()?;
1524    ToiOutput::from_native("time_of_impact", unsafe { ffi::b2TimeOfImpact(&raw_input) })
1525}
1526
1527/// Compute the contact manifold between two circles.
1528///
1529/// `transform_b_in_a` maps shape B's local coordinates into shape A's local frame.
1530/// The returned manifold is expressed in shape A's frame.
1531#[doc(alias = "b2CollideCircles")]
1532pub fn collide_circles(
1533    circle_a: Circle,
1534    circle_b: Circle,
1535    transform_b_in_a: Transform,
1536) -> Result<LocalManifold> {
1537    circle_a.validate()?;
1538    circle_b.validate()?;
1539    check_collision_transform_valid("collide_circles", transform_b_in_a)?;
1540    let raw_a = circle_a.into_raw();
1541    let raw_b = circle_b.into_raw();
1542    let _lease = transient_native_lease()?;
1543    LocalManifold::from_native("collide_circles", unsafe {
1544        ffi::b2CollideCircles(&raw_a, &raw_b, transform_b_in_a.into_raw())
1545    })
1546}
1547
1548/// Compute the contact manifold between a capsule and a circle.
1549///
1550/// `transform_b_in_a` maps the circle into the capsule's local frame. The returned
1551/// manifold is expressed in the capsule's frame.
1552#[doc(alias = "b2CollideCapsuleAndCircle")]
1553pub fn collide_capsule_and_circle(
1554    capsule_a: Capsule,
1555    circle_b: Circle,
1556    transform_b_in_a: Transform,
1557) -> Result<LocalManifold> {
1558    capsule_a.validate()?;
1559    circle_b.validate()?;
1560    check_collision_transform_valid("collide_capsule_and_circle", transform_b_in_a)?;
1561    let raw_a = capsule_a.into_raw();
1562    let raw_b = circle_b.into_raw();
1563    let _lease = transient_native_lease()?;
1564    LocalManifold::from_native("collide_capsule_and_circle", unsafe {
1565        ffi::b2CollideCapsuleAndCircle(&raw_a, &raw_b, transform_b_in_a.into_raw())
1566    })
1567}
1568
1569/// Compute the contact manifold between a segment and a circle.
1570///
1571/// `transform_b_in_a` maps the circle into the segment's local frame. The returned
1572/// manifold is expressed in the segment's frame.
1573#[doc(alias = "b2CollideSegmentAndCircle")]
1574pub fn collide_segment_and_circle(
1575    segment_a: Segment,
1576    circle_b: Circle,
1577    transform_b_in_a: Transform,
1578) -> Result<LocalManifold> {
1579    segment_a.validate()?;
1580    circle_b.validate()?;
1581    check_collision_transform_valid("collide_segment_and_circle", transform_b_in_a)?;
1582    let raw_a = segment_a.into_raw();
1583    let raw_b = circle_b.into_raw();
1584    let _lease = transient_native_lease()?;
1585    LocalManifold::from_native("collide_segment_and_circle", unsafe {
1586        ffi::b2CollideSegmentAndCircle(&raw_a, &raw_b, transform_b_in_a.into_raw())
1587    })
1588}
1589
1590/// Compute the contact manifold between a polygon and a circle.
1591///
1592/// `transform_b_in_a` maps the circle into the polygon's local frame. The returned
1593/// manifold is expressed in the polygon's frame.
1594#[doc(alias = "b2CollidePolygonAndCircle")]
1595pub fn collide_polygon_and_circle(
1596    polygon_a: Polygon,
1597    circle_b: Circle,
1598    transform_b_in_a: Transform,
1599) -> Result<LocalManifold> {
1600    polygon_a.validate()?;
1601    circle_b.validate()?;
1602    check_collision_transform_valid("collide_polygon_and_circle", transform_b_in_a)?;
1603    let raw_a = polygon_a.into_raw();
1604    let raw_b = circle_b.into_raw();
1605    let _lease = transient_native_lease()?;
1606    LocalManifold::from_native("collide_polygon_and_circle", unsafe {
1607        ffi::b2CollidePolygonAndCircle(&raw_a, &raw_b, transform_b_in_a.into_raw())
1608    })
1609}
1610
1611/// Compute the contact manifold between two capsules.
1612///
1613/// `transform_b_in_a` maps capsule B into capsule A's local frame. The returned
1614/// manifold is expressed in capsule A's frame.
1615#[doc(alias = "b2CollideCapsules")]
1616pub fn collide_capsules(
1617    capsule_a: Capsule,
1618    capsule_b: Capsule,
1619    transform_b_in_a: Transform,
1620) -> Result<LocalManifold> {
1621    capsule_a.validate()?;
1622    capsule_b.validate()?;
1623    check_collision_transform_valid("collide_capsules", transform_b_in_a)?;
1624    let raw_a = capsule_a.into_raw();
1625    let raw_b = capsule_b.into_raw();
1626    let _lease = transient_native_lease()?;
1627    LocalManifold::from_native("collide_capsules", unsafe {
1628        ffi::b2CollideCapsules(&raw_a, &raw_b, transform_b_in_a.into_raw())
1629    })
1630}
1631
1632/// Compute the contact manifold between a segment and a capsule.
1633///
1634/// `transform_b_in_a` maps the capsule into the segment's local frame. The returned
1635/// manifold is expressed in the segment's frame.
1636#[doc(alias = "b2CollideSegmentAndCapsule")]
1637pub fn collide_segment_and_capsule(
1638    segment_a: Segment,
1639    capsule_b: Capsule,
1640    transform_b_in_a: Transform,
1641) -> Result<LocalManifold> {
1642    segment_a.validate()?;
1643    capsule_b.validate()?;
1644    check_collision_transform_valid("collide_segment_and_capsule", transform_b_in_a)?;
1645    let raw_a = segment_a.into_raw();
1646    let raw_b = capsule_b.into_raw();
1647    let _lease = transient_native_lease()?;
1648    LocalManifold::from_native("collide_segment_and_capsule", unsafe {
1649        ffi::b2CollideSegmentAndCapsule(&raw_a, &raw_b, transform_b_in_a.into_raw())
1650    })
1651}
1652
1653/// Compute the contact manifold between a polygon and a capsule.
1654///
1655/// `transform_b_in_a` maps the capsule into the polygon's local frame. The returned
1656/// manifold is expressed in the polygon's frame.
1657#[doc(alias = "b2CollidePolygonAndCapsule")]
1658pub fn collide_polygon_and_capsule(
1659    polygon_a: Polygon,
1660    capsule_b: Capsule,
1661    transform_b_in_a: Transform,
1662) -> Result<LocalManifold> {
1663    polygon_a.validate()?;
1664    capsule_b.validate()?;
1665    check_collision_transform_valid("collide_polygon_and_capsule", transform_b_in_a)?;
1666    let raw_a = polygon_a.into_raw();
1667    let raw_b = capsule_b.into_raw();
1668    let _lease = transient_native_lease()?;
1669    LocalManifold::from_native("collide_polygon_and_capsule", unsafe {
1670        ffi::b2CollidePolygonAndCapsule(&raw_a, &raw_b, transform_b_in_a.into_raw())
1671    })
1672}
1673
1674/// Compute the contact manifold between two polygons.
1675///
1676/// `transform_b_in_a` maps polygon B into polygon A's local frame. The returned
1677/// manifold is expressed in polygon A's frame.
1678#[doc(alias = "b2CollidePolygons")]
1679pub fn collide_polygons(
1680    polygon_a: Polygon,
1681    polygon_b: Polygon,
1682    transform_b_in_a: Transform,
1683) -> Result<LocalManifold> {
1684    polygon_a.validate()?;
1685    polygon_b.validate()?;
1686    check_collision_transform_valid("collide_polygons", transform_b_in_a)?;
1687    let raw_a = polygon_a.into_raw();
1688    let raw_b = polygon_b.into_raw();
1689    let _lease = transient_native_lease()?;
1690    LocalManifold::from_native("collide_polygons", unsafe {
1691        ffi::b2CollidePolygons(&raw_a, &raw_b, transform_b_in_a.into_raw())
1692    })
1693}
1694
1695/// Compute the contact manifold between a segment and a polygon.
1696///
1697/// `transform_b_in_a` maps the polygon into the segment's local frame. The returned
1698/// manifold is expressed in the segment's frame.
1699#[doc(alias = "b2CollideSegmentAndPolygon")]
1700pub fn collide_segment_and_polygon(
1701    segment_a: Segment,
1702    polygon_b: Polygon,
1703    transform_b_in_a: Transform,
1704) -> Result<LocalManifold> {
1705    segment_a.validate()?;
1706    polygon_b.validate()?;
1707    check_collision_transform_valid("collide_segment_and_polygon", transform_b_in_a)?;
1708    let raw_a = segment_a.into_raw();
1709    let raw_b = polygon_b.into_raw();
1710    let _lease = transient_native_lease()?;
1711    LocalManifold::from_native("collide_segment_and_polygon", unsafe {
1712        ffi::b2CollideSegmentAndPolygon(&raw_a, &raw_b, transform_b_in_a.into_raw())
1713    })
1714}
1715
1716/// Compute the contact manifold between a chain segment and a circle.
1717///
1718/// `transform_b_in_a` maps the circle into the chain segment's local frame. The
1719/// returned manifold is expressed in the chain segment's frame.
1720#[doc(alias = "b2CollideChainSegmentAndCircle")]
1721pub fn collide_chain_segment_and_circle(
1722    segment_a: ChainSegment,
1723    circle_b: Circle,
1724    transform_b_in_a: Transform,
1725) -> Result<LocalManifold> {
1726    segment_a.validate()?;
1727    circle_b.validate()?;
1728    check_collision_transform_valid("collide_chain_segment_and_circle", transform_b_in_a)?;
1729    let raw_a = segment_a.into_raw();
1730    let raw_b = circle_b.into_raw();
1731    let _lease = transient_native_lease()?;
1732    LocalManifold::from_native("collide_chain_segment_and_circle", unsafe {
1733        ffi::b2CollideChainSegmentAndCircle(&raw_a, &raw_b, transform_b_in_a.into_raw())
1734    })
1735}
1736
1737/// Compute the contact manifold between a chain segment and a capsule.
1738///
1739/// `transform_b_in_a` maps the capsule into the chain segment's local frame. The
1740/// returned manifold is expressed in the chain segment's frame.
1741///
1742/// Provide `cache` when repeatedly colliding against nearby rounded shapes to
1743/// warm-start the internal edge solver.
1744#[doc(alias = "b2CollideChainSegmentAndCapsule")]
1745pub fn collide_chain_segment_and_capsule(
1746    segment_a: ChainSegment,
1747    capsule_b: Capsule,
1748    transform_b_in_a: Transform,
1749    cache: Option<&mut SimplexCache>,
1750) -> Result<LocalManifold> {
1751    segment_a.validate()?;
1752    capsule_b.validate()?;
1753    check_collision_transform_valid("collide_chain_segment_and_capsule", transform_b_in_a)?;
1754    let raw_a = segment_a.into_raw();
1755    let raw_b = capsule_b.into_raw();
1756    let mut staged_cache = cache.as_deref().copied().unwrap_or_default();
1757    staged_cache.validate_for("collide_chain_segment_and_capsule", 2, 2)?;
1758    let _lease = transient_native_lease()?;
1759    let manifold = LocalManifold::from_native("collide_chain_segment_and_capsule", unsafe {
1760        ffi::b2CollideChainSegmentAndCapsule(
1761            &raw_a,
1762            &raw_b,
1763            transform_b_in_a.into_raw(),
1764            staged_cache.raw_mut(),
1765        )
1766    })?;
1767    commit_native_simplex_cache(
1768        cache,
1769        staged_cache,
1770        "collide_chain_segment_and_capsule",
1771        2,
1772        2,
1773    )?;
1774    Ok(manifold)
1775}
1776
1777/// Compute the contact manifold between a chain segment and a polygon.
1778///
1779/// `transform_b_in_a` maps the polygon into the chain segment's local frame. The
1780/// returned manifold is expressed in the chain segment's frame.
1781///
1782/// Provide `cache` when repeatedly colliding against nearby rounded polygons to
1783/// warm-start the internal edge solver.
1784#[doc(alias = "b2CollideChainSegmentAndPolygon")]
1785pub fn collide_chain_segment_and_polygon(
1786    segment_a: ChainSegment,
1787    polygon_b: Polygon,
1788    transform_b_in_a: Transform,
1789    cache: Option<&mut SimplexCache>,
1790) -> Result<LocalManifold> {
1791    segment_a.validate()?;
1792    polygon_b.validate()?;
1793    check_collision_transform_valid("collide_chain_segment_and_polygon", transform_b_in_a)?;
1794    let raw_a = segment_a.into_raw();
1795    let raw_b = polygon_b.into_raw();
1796    let proxy_b_count = usize::try_from(raw_b.count).map_err(|_| {
1797        Error::invalid_argument(
1798            "collide_chain_segment_and_polygon",
1799            "polygon_b",
1800            "a polygon with a representable point count",
1801        )
1802    })?;
1803    let mut staged_cache = cache.as_deref().copied().unwrap_or_default();
1804    staged_cache.validate_for("collide_chain_segment_and_polygon", 2, proxy_b_count)?;
1805    let _lease = transient_native_lease()?;
1806    let manifold = LocalManifold::from_native("collide_chain_segment_and_polygon", unsafe {
1807        ffi::b2CollideChainSegmentAndPolygon(
1808            &raw_a,
1809            &raw_b,
1810            transform_b_in_a.into_raw(),
1811            staged_cache.raw_mut(),
1812        )
1813    })?;
1814    commit_native_simplex_cache(
1815        cache,
1816        staged_cache,
1817        "collide_chain_segment_and_polygon",
1818        2,
1819        proxy_b_count,
1820    )?;
1821    Ok(manifold)
1822}
1823
1824impl Aabb {
1825    /// Check whether this AABB is valid for Box2D queries.
1826    #[inline]
1827    pub fn is_valid(self) -> bool {
1828        let width = self.upper.x - self.lower.x;
1829        let height = self.upper.y - self.lower.y;
1830        width >= 0.0 && height >= 0.0 && self.lower.is_valid() && self.upper.is_valid()
1831    }
1832
1833    /// Validate this AABB for collision queries.
1834    #[inline]
1835    pub fn validate(self) -> Result<()> {
1836        if self.is_valid() {
1837            Ok(())
1838        } else {
1839            Err(Error::invalid_argument(
1840                "Aabb::validate",
1841                "self",
1842                "finite ordered lower and upper bounds",
1843            ))
1844        }
1845    }
1846
1847    /// Ray cast against this AABB using Box2D-style `origin + translation`.
1848    ///
1849    /// Initial overlap returns a hit with zero fraction, zero normal, and `point = origin`.
1850    pub fn ray_cast<VO: Into<Vec2>, VT: Into<Vec2>>(
1851        self,
1852        origin: VO,
1853        translation: VT,
1854    ) -> Result<CastOutput> {
1855        let origin = origin.into();
1856        let translation = translation.into();
1857        self.validate()?;
1858        check_collision_vec2_valid("Aabb::ray_cast", "origin", origin)?;
1859        check_collision_vec2_valid("Aabb::ray_cast", "translation", translation)?;
1860        Ok(self.ray_cast_validated(origin, translation))
1861    }
1862
1863    fn ray_cast_validated(self, origin: Vec2, translation: Vec2) -> CastOutput {
1864        let mut axis_state = RayCastAxisState {
1865            tmin: 0.0,
1866            tmax: 1.0,
1867            normal: Vec2::ZERO,
1868        };
1869
1870        if !ray_cast_axis(
1871            RayCastAxisInput {
1872                origin: origin.x,
1873                translation: translation.x,
1874                lower: self.lower.x,
1875                upper: self.upper.x,
1876                enter_normal: Vec2::new(-1.0, 0.0),
1877                exit_normal: Vec2::new(1.0, 0.0),
1878            },
1879            &mut axis_state,
1880        ) {
1881            return CastOutput::MISS;
1882        }
1883
1884        if !ray_cast_axis(
1885            RayCastAxisInput {
1886                origin: origin.y,
1887                translation: translation.y,
1888                lower: self.lower.y,
1889                upper: self.upper.y,
1890                enter_normal: Vec2::new(0.0, -1.0),
1891                exit_normal: Vec2::new(0.0, 1.0),
1892            },
1893            &mut axis_state,
1894        ) {
1895            return CastOutput::MISS;
1896        }
1897
1898        if !(0.0..=1.0).contains(&axis_state.tmin) {
1899            return CastOutput::MISS;
1900        }
1901
1902        CastOutput {
1903            normal: axis_state.normal,
1904            point: Vec2::new(
1905                origin.x + axis_state.tmin * translation.x,
1906                origin.y + axis_state.tmin * translation.y,
1907            ),
1908            fraction: axis_state.tmin,
1909            iterations: 0,
1910            hit: true,
1911        }
1912    }
1913}
1914
1915#[cfg(test)]
1916mod tests {
1917    use super::*;
1918
1919    #[test]
1920    fn shape_proxy_owns_point_validation_and_storage_invariants() {
1921        let maximum =
1922            ShapeProxy::new((0..MAX_SHAPE_PROXY_POINTS).map(|_| [0.0_f32, 0.0]), 0.25).unwrap();
1923        assert_eq!(maximum.count(), MAX_SHAPE_PROXY_POINTS);
1924
1925        assert_eq!(
1926            ShapeProxy::new((0..=MAX_SHAPE_PROXY_POINTS).map(|_| [0.0_f32, 0.0]), 0.25,)
1927                .unwrap_err(),
1928            Error::invalid_argument(
1929                "ShapeProxy::new",
1930                "points",
1931                "no more than Box2D's maximum shape-proxy point count",
1932            )
1933        );
1934
1935        let raw = ShapeProxy::new([[1.0_f32, 2.0]], 0.25).unwrap().into_raw();
1936        assert_eq!(raw.count, 1);
1937        assert_eq!(raw.radius, 0.25);
1938        assert_eq!(raw.points[0].x, 1.0);
1939        assert_eq!(raw.points[0].y, 2.0);
1940        assert!(
1941            raw.points[1..]
1942                .iter()
1943                .all(|point| point.x == 0.0 && point.y == 0.0)
1944        );
1945
1946        assert_eq!(
1947            ShapeProxy::offset_from_points(
1948                [[f32::MAX, 0.0]],
1949                0.0,
1950                Transform::from_pos_angle([f32::MAX, 0.0], 0.0).unwrap(),
1951            )
1952            .unwrap_err(),
1953            Error::invalid_argument(
1954                "ShapeProxy::offset_from_points",
1955                "points/transform",
1956                "a transform whose proxy points remain finite",
1957            )
1958        );
1959    }
1960
1961    #[test]
1962    fn invalid_native_simplex_cache_is_not_published() {
1963        let original = SimplexCache {
1964            raw: ffi::b2SimplexCache {
1965                count: 1,
1966                indexA: [0, 0, 0],
1967                indexB: [0, 0, 0],
1968            },
1969        };
1970
1971        for staged in [
1972            SimplexCache {
1973                raw: ffi::b2SimplexCache {
1974                    count: 4,
1975                    indexA: [0, 0, 0],
1976                    indexB: [0, 0, 0],
1977                },
1978            },
1979            SimplexCache {
1980                raw: ffi::b2SimplexCache {
1981                    count: 1,
1982                    indexA: [2, 0, 0],
1983                    indexB: [0, 0, 0],
1984                },
1985            },
1986        ] {
1987            let mut target = original;
1988            assert_eq!(
1989                commit_native_simplex_cache(Some(&mut target), staged, "test_query", 2, 2),
1990                Err(Error::InvalidNativeOutput {
1991                    operation: "test_query",
1992                    output: "simplex_cache",
1993                    constraint: "at most three in-range proxy point indices",
1994                })
1995            );
1996            assert_eq!(target.raw.count, original.raw.count);
1997            assert_eq!(target.raw.indexA, original.raw.indexA);
1998            assert_eq!(target.raw.indexB, original.raw.indexB);
1999        }
2000    }
2001
2002    #[test]
2003    fn aabb_validation_matches_upstream_finite_and_ordering_rules() {
2004        assert!(
2005            Aabb::new([0.0_f32, 0.0], [0.0_f32, 0.0])
2006                .unwrap()
2007                .is_valid()
2008        );
2009        assert!(
2010            Aabb::new([f32::MIN, f32::MIN], [f32::MAX, f32::MAX])
2011                .unwrap()
2012                .is_valid()
2013        );
2014
2015        for invalid in [
2016            Aabb::new([1.0_f32, 0.0], [0.0_f32, 1.0]),
2017            Aabb::new([0.0_f32, 1.0], [1.0_f32, 0.0]),
2018            Aabb::new([f32::NAN, 0.0], [1.0_f32, 1.0]),
2019            Aabb::new([0.0_f32, 0.0], [f32::INFINITY, 1.0]),
2020            Aabb::new([f32::NEG_INFINITY, 0.0], [1.0_f32, 1.0]),
2021        ] {
2022            assert!(invalid.is_err());
2023        }
2024    }
2025
2026    #[test]
2027    fn worldless_native_collision_calls_obey_the_callback_gate() {
2028        crate::Foundation::initialize_default().unwrap();
2029
2030        let proxy = ShapeProxy::new([[0.0_f32, 0.0]], 0.0).unwrap();
2031        let circle = Circle::new([0.0_f32, 0.0], 0.5).unwrap();
2032        let invalid_circle = Circle {
2033            center: Vec2::new(f32::NAN, 0.0),
2034            radius: 0.5,
2035        };
2036        let aabb = Aabb::new([-1.0_f32, -1.0], [1.0_f32, 1.0]).unwrap();
2037
2038        {
2039            let _callback_guard = crate::core::callback_state::CallbackGuard::enter();
2040            let proxy_input_was_materialized = std::cell::Cell::new(false);
2041
2042            assert_eq!(
2043                segment_distance(
2044                    [0.0_f32, 0.0],
2045                    [1.0_f32, 0.0],
2046                    [0.0_f32, 1.0],
2047                    [1.0_f32, 1.0],
2048                )
2049                .unwrap_err(),
2050                Error::InCallback
2051            );
2052            assert_eq!(
2053                collide_circles(circle, circle, Transform::IDENTITY).unwrap_err(),
2054                Error::InCallback
2055            );
2056            assert_eq!(
2057                collide_circles(invalid_circle, circle, Transform::IDENTITY).unwrap_err(),
2058                Error::invalid_argument(
2059                    "Circle::validate",
2060                    "circle",
2061                    "finite center coordinates and a finite non-negative radius",
2062                )
2063            );
2064            let callback_proxy = ShapeProxy::new(
2065                core::iter::once_with(|| {
2066                    proxy_input_was_materialized.set(true);
2067                    [0.0_f32, 0.0]
2068                }),
2069                0.0,
2070            )
2071            .unwrap();
2072            assert_eq!(callback_proxy.points(), &[Vec2::ZERO]);
2073            assert!(proxy_input_was_materialized.get());
2074            assert!(aabb.is_valid());
2075            assert!(aabb.ray_cast([-2.0_f32, 0.0], [4.0_f32, 0.0]).unwrap().hit);
2076            assert_eq!(
2077                segment_distance(
2078                    [f32::NAN, 0.0],
2079                    [1.0_f32, 0.0],
2080                    [0.0_f32, 1.0],
2081                    [1.0_f32, 1.0],
2082                )
2083                .unwrap_err(),
2084                Error::invalid_argument("segment_distance", "p1", "a finite vector")
2085            );
2086        }
2087
2088        assert!(
2089            shape_distance(
2090                DistanceInput::new(proxy, proxy, Transform::IDENTITY).unwrap(),
2091                &mut SimplexCache::default(),
2092            )
2093            .is_ok()
2094        );
2095    }
2096
2097    #[test]
2098    fn sweep_transform_validation_precedes_foundation_activity() {
2099        let valid_sweep = Sweep::new(
2100            [0.0_f32, 0.0],
2101            [0.0_f32, 0.0],
2102            [1.0_f32, 0.0],
2103            Rot::IDENTITY,
2104            Rot::IDENTITY,
2105        )
2106        .unwrap();
2107        let invalid_sweep = Sweep {
2108            local_center: Vec2::new(f32::NAN, 0.0),
2109            c1: Vec2::ZERO,
2110            c2: Vec2::new(1.0, 0.0),
2111            q1: Rot::IDENTITY,
2112            q2: Rot::IDENTITY,
2113        };
2114        let _callback_guard = crate::core::callback_state::CallbackGuard::enter();
2115
2116        assert_eq!(
2117            invalid_sweep.transform_at(0.5).unwrap_err(),
2118            Error::invalid_argument("Sweep::validate", "local_center", "a finite vector",)
2119        );
2120        assert_eq!(
2121            valid_sweep.transform_at(f32::NAN).unwrap_err(),
2122            Error::invalid_argument("Sweep::transform_at", "time", "a finite value in 0.0..=1.0",)
2123        );
2124        assert_eq!(
2125            valid_sweep.transform_at(0.5).unwrap_err(),
2126            Error::InCallback
2127        );
2128    }
2129}