Skip to main content

box2d_rust/
aabb.rs

1// Port of box2d-cpp-reference/src/aabb.h and src/aabb.c
2//
3// b2IsValidAABB is declared in math_functions.h and lives in math_functions.rs;
4// the AABB inline queries (contains/center/extents/union/overlaps) live there
5// too. This module holds the pieces that are specific to aabb.c/aabb.h: the
6// perimeter, in-place enlarge, world-space offset, and ray cast.
7//
8// SPDX-FileCopyrightText: 2023 Erin Catto
9// SPDX-License-Identifier: MIT
10
11// In double precision Pos coordinates are already f64, so the `as f64` promotions
12// in offset_aabb are the identity; in single precision they are real promotions.
13// Keeping the cast lets one expression compile in both modes.
14#![allow(clippy::unnecessary_cast)]
15
16use crate::collision::CastOutput;
17use crate::math_functions::{
18    abs, lerp, min_float, round_down_float, round_up_float, sub, Aabb, Pos, Vec2, VEC2_ZERO,
19};
20
21/// Get the surface area of an AABB (the perimeter length). (b2Perimeter)
22pub fn perimeter(a: Aabb) -> f32 {
23    let wx = a.upper_bound.x - a.lower_bound.x;
24    let wy = a.upper_bound.y - a.lower_bound.y;
25    2.0 * (wx + wy)
26}
27
28/// Enlarge `a` to contain `b`. (b2EnlargeAABB)
29///
30/// @return true if the AABB grew.
31pub fn enlarge_aabb(a: &mut Aabb, b: Aabb) -> bool {
32    let mut changed = false;
33    if b.lower_bound.x < a.lower_bound.x {
34        a.lower_bound.x = b.lower_bound.x;
35        changed = true;
36    }
37
38    if b.lower_bound.y < a.lower_bound.y {
39        a.lower_bound.y = b.lower_bound.y;
40        changed = true;
41    }
42
43    if a.upper_bound.x < b.upper_bound.x {
44        a.upper_bound.x = b.upper_bound.x;
45        changed = true;
46    }
47
48    if a.upper_bound.y < b.upper_bound.y {
49        a.upper_bound.y = b.upper_bound.y;
50        changed = true;
51    }
52
53    changed
54}
55
56/// Translate a relative AABB into world space, rounding outward so the float box
57/// always contains the true box far from the origin. (b2OffsetAABB)
58///
59/// Float ULP at 1e8 dwarfs the AABB margin, so plain truncation could clip a
60/// shape out of its own box; the broadphase pair order rides on the
61/// deterministic directed rounding. In single precision this collapses to a
62/// plain sum since [`round_down_float`]/[`round_up_float`] are the identity.
63pub fn offset_aabb(box_: Aabb, origin: Pos) -> Aabb {
64    Aabb {
65        lower_bound: Vec2 {
66            x: round_down_float(origin.x as f64 + box_.lower_bound.x as f64),
67            y: round_down_float(origin.y as f64 + box_.lower_bound.y as f64),
68        },
69        upper_bound: Vec2 {
70            x: round_up_float(origin.x as f64 + box_.upper_bound.x as f64),
71            y: round_up_float(origin.y as f64 + box_.upper_bound.y as f64),
72        },
73    }
74}
75
76/// Ray cast an AABB. Radius is not handled. (b2AABB_RayCast)
77// From Real-time Collision Detection, p179.
78pub fn aabb_ray_cast(a: Aabb, p1: Vec2, p2: Vec2) -> CastOutput {
79    let mut output = CastOutput::default();
80
81    let mut t_min = -f32::MAX;
82    let mut t_max = f32::MAX;
83
84    let p = p1;
85    let d = sub(p2, p1);
86    let abs_d = abs(d);
87
88    let mut normal = VEC2_ZERO;
89
90    // x-coordinate
91    if abs_d.x < f32::EPSILON {
92        // parallel
93        if p.x < a.lower_bound.x || a.upper_bound.x < p.x {
94            return output;
95        }
96    } else {
97        let inv_d = 1.0 / d.x;
98        let mut t1 = (a.lower_bound.x - p.x) * inv_d;
99        let mut t2 = (a.upper_bound.x - p.x) * inv_d;
100
101        // Sign of the normal vector.
102        let mut s = -1.0;
103
104        if t1 > t2 {
105            core::mem::swap(&mut t1, &mut t2);
106            s = 1.0;
107        }
108
109        // Push the min up
110        if t1 > t_min {
111            normal.y = 0.0;
112            normal.x = s;
113            t_min = t1;
114        }
115
116        // Pull the max down
117        t_max = min_float(t_max, t2);
118
119        if t_min > t_max {
120            return output;
121        }
122    }
123
124    // y-coordinate
125    if abs_d.y < f32::EPSILON {
126        // parallel
127        if p.y < a.lower_bound.y || a.upper_bound.y < p.y {
128            return output;
129        }
130    } else {
131        let inv_d = 1.0 / d.y;
132        let mut t1 = (a.lower_bound.y - p.y) * inv_d;
133        let mut t2 = (a.upper_bound.y - p.y) * inv_d;
134
135        // Sign of the normal vector.
136        let mut s = -1.0;
137
138        if t1 > t2 {
139            core::mem::swap(&mut t1, &mut t2);
140            s = 1.0;
141        }
142
143        // Push the min up
144        if t1 > t_min {
145            normal.x = 0.0;
146            normal.y = s;
147            t_min = t1;
148        }
149
150        // Pull the max down
151        t_max = min_float(t_max, t2);
152
153        if t_min > t_max {
154            return output;
155        }
156    }
157
158    // Does the ray start inside the box?
159    if t_min < 0.0 {
160        return output;
161    }
162
163    // Does the ray intersect beyond the segment length?
164    if 1.0 < t_min {
165        return output;
166    }
167
168    // Intersection.
169    output.fraction = t_min;
170    output.normal = normal;
171    output.point = lerp(p1, p2, t_min);
172    output.hit = true;
173    output
174}