Skip to main content

parry2d/query/sat/
sat_cuboid_cuboid.rs

1use crate::math::{Pose, Real, Vector, VectorExt, DIM};
2use crate::shape::{Cuboid, SupportMap};
3use crate::utils::WSign;
4
5/// Computes the separation distance between two cuboids along a given axis.
6///
7/// This function is part of the Separating Axis Theorem (SAT) implementation for cuboid-cuboid
8/// collision detection. It projects both cuboids onto the specified axis and computes how far
9/// apart they are (positive = separated, negative = overlapping).
10///
11/// # How It Works
12///
13/// 1. Orients the axis to point from cuboid1 toward cuboid2 (using the translation vector)
14/// 2. Finds the support points (furthest points) on each cuboid in that direction
15/// 3. Computes the signed distance between these support points along the axis
16///
17/// # Parameters
18///
19/// - `cuboid1`: The first cuboid (in its local coordinate frame)
20/// - `cuboid2`: The second cuboid
21/// - `pos12`: The position of cuboid2 relative to cuboid1 (transforms from cuboid2's space to cuboid1's space)
22/// - `axis1`: The axis direction in cuboid1's local space to test for separation
23///
24/// # Returns
25///
26/// A tuple containing:
27/// - `Real`: The separation distance along the axis
28///   - **Positive**: Shapes are separated by this distance
29///   - **Negative**: Shapes are overlapping (penetration depth is the absolute value)
30///   - **Zero**: Shapes are exactly touching
31/// - `Vector`: The oriented axis direction (pointing from cuboid1 toward cuboid2)
32///
33/// # Example
34///
35/// ```rust
36/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
37/// use parry3d::shape::Cuboid;
38/// use parry3d::query::sat::cuboid_cuboid_compute_separation_wrt_local_line;
39/// use parry3d::math::{Pose, Vector};
40///
41/// let cube1 = Cuboid::new(Vector::splat(1.0));
42/// let cube2 = Cuboid::new(Vector::splat(1.0));
43///
44/// // Position cube2 at (3, 0, 0) relative to cube1
45/// let pos12 = Pose::translation(3.0, 0.0, 0.0);
46///
47/// // Test separation along the X axis
48/// let (separation, _axis) = cuboid_cuboid_compute_separation_wrt_local_line(
49///     &cube1,
50///     &cube2,
51///     &pos12,
52///     Vector::X
53/// );
54///
55/// // Should be separated by 1.0 (distance 3.0 - half_extents 1.0 - 1.0)
56/// assert!(separation > 0.0);
57/// # }
58/// ```
59#[cfg(feature = "dim3")]
60pub fn cuboid_cuboid_compute_separation_wrt_local_line(
61    cuboid1: &Cuboid,
62    cuboid2: &Cuboid,
63    pos12: &Pose,
64    axis1: Vector,
65) -> (Real, Vector) {
66    #[expect(clippy::unnecessary_cast)]
67    let signum = pos12.translation.dot(axis1).copy_sign_to(1.0 as Real);
68    let axis1 = axis1 * signum;
69    let axis2 = pos12.rotation.inverse() * -axis1;
70    let local_pt1 = cuboid1.local_support_point(axis1);
71    let local_pt2 = cuboid2.local_support_point(axis2);
72    let pt2 = pos12 * local_pt2;
73    let separation = (pt2 - local_pt1).dot(axis1);
74    (separation, axis1)
75}
76
77/// Finds the best separating axis by testing all edge-edge combinations between two cuboids.
78///
79/// In 3D, edge-edge contact is common when two boxes collide. This function tests all possible
80/// axes formed by the cross product of edges from each cuboid to find the axis with maximum
81/// separation (or minimum penetration).
82///
83/// # Why Test Edge Cross Products?
84///
85/// When two 3D convex polyhedra collide, the separating axis (if one exists) must be either:
86/// 1. A face normal from one shape
87/// 2. A face normal from the other shape
88/// 3. **Perpendicular to an edge from each shape** (the cross product of the edges)
89///
90/// This function handles case 3. For two cuboids, there are 3 edges per cuboid (aligned with X, Y, Z),
91/// giving 3 × 3 = 9 possible edge pair combinations to test.
92///
93/// # Parameters
94///
95/// - `cuboid1`: The first cuboid (in its local coordinate frame)
96/// - `cuboid2`: The second cuboid
97/// - `pos12`: The position of cuboid2 relative to cuboid1
98///
99/// # Returns
100///
101/// A tuple containing:
102/// - `Real`: The best (maximum) separation found across all edge-edge axes
103///   - **Positive**: Shapes are separated
104///   - **Negative**: Shapes are overlapping
105/// - `Vector`: The axis direction that gives this separation
106///
107/// # Example
108///
109/// ```rust
110/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
111/// use parry3d::shape::Cuboid;
112/// use parry3d::query::sat::cuboid_cuboid_find_local_separating_edge_twoway;
113/// use parry3d::math::{Pose, Vector};
114///
115/// let cube1 = Cuboid::new(Vector::new(1.0, 1.0, 1.0));
116/// let cube2 = Cuboid::new(Vector::new(1.0, 1.0, 1.0));
117///
118/// // Rotate and position cube2 so edge-edge contact is likely
119/// let pos12 = Pose::translation(2.0, 2.0, 0.0);
120///
121/// let (separation, _axis) = cuboid_cuboid_find_local_separating_edge_twoway(
122///     &cube1,
123///     &cube2,
124///     &pos12
125/// );
126///
127/// if separation > 0.0 {
128///     println!("Separated by {} along an edge-edge axis", separation);
129/// }
130/// # }
131/// ```
132///
133/// # Note
134///
135/// This function only tests edge-edge axes. For a complete SAT test, you must also test
136/// face normals using [`cuboid_cuboid_find_local_separating_normal_oneway`].
137#[cfg(feature = "dim3")]
138pub fn cuboid_cuboid_find_local_separating_edge_twoway(
139    cuboid1: &Cuboid,
140    cuboid2: &Cuboid,
141    pos12: &Pose,
142) -> (Real, Vector) {
143    use approx::AbsDiffEq;
144    let mut best_separation = -Real::MAX;
145    let mut best_dir = Vector::ZERO;
146
147    let x2 = pos12.rotation * Vector::X;
148    let y2 = pos12.rotation * Vector::Y;
149    let z2 = pos12.rotation * Vector::Z;
150
151    // We have 3 * 3 = 9 axes to test.
152    let axes = [
153        // Vector::{x, y ,z}().cross(y2)
154        Vector::new(0.0, -x2.z, x2.y),
155        Vector::new(x2.z, 0.0, -x2.x),
156        Vector::new(-x2.y, x2.x, 0.0),
157        // Vector::{x, y ,z}().cross(y2)
158        Vector::new(0.0, -y2.z, y2.y),
159        Vector::new(y2.z, 0.0, -y2.x),
160        Vector::new(-y2.y, y2.x, 0.0),
161        // Vector::{x, y ,z}().cross(y2)
162        Vector::new(0.0, -z2.z, z2.y),
163        Vector::new(z2.z, 0.0, -z2.x),
164        Vector::new(-z2.y, z2.x, 0.0),
165    ];
166
167    for axis1 in &axes {
168        let norm1 = axis1.length();
169        if norm1 > Real::default_epsilon() {
170            let (separation, axis1) = cuboid_cuboid_compute_separation_wrt_local_line(
171                cuboid1,
172                cuboid2,
173                pos12,
174                axis1 / norm1,
175            );
176
177            if separation > best_separation {
178                best_separation = separation;
179                best_dir = axis1;
180            }
181        }
182    }
183
184    (best_separation, best_dir)
185}
186
187/// Finds the best separating axis by testing the face normals of the first cuboid.
188///
189/// This function tests the face normals (X, Y, Z axes in 2D/3D) of `cuboid1` to find which
190/// direction gives the maximum separation between the two cuboids. This is the "one-way" test
191/// that only considers faces from one cuboid.
192///
193/// # Why "One-Way"?
194///
195/// For a complete SAT test between two cuboids, you need to test:
196/// 1. Face normals from cuboid1 (this function)
197/// 2. Face normals from cuboid2 (call this function again with swapped arguments)
198/// 3. Edge-edge cross products in 3D (`cuboid_cuboid_find_local_separating_edge_twoway`)
199///
200/// By testing only one shape's normals at a time, the implementation can be more efficient
201/// and reusable.
202///
203/// # Parameters
204///
205/// - `cuboid1`: The cuboid whose face normals will be tested
206/// - `cuboid2`: The other cuboid
207/// - `pos12`: The position of cuboid2 relative to cuboid1
208///
209/// # Returns
210///
211/// A tuple containing:
212/// - `Real`: The maximum separation found among cuboid1's face normals
213///   - **Positive**: Shapes are separated by at least this distance
214///   - **Negative**: Shapes are overlapping (penetration)
215/// - `Vector`: The face normal direction that gives this separation
216///
217/// # Example
218///
219/// ```rust
220/// # #[cfg(all(feature = "dim2", feature = "f32"))] {
221/// use parry2d::shape::Cuboid;
222/// use parry2d::query::sat::cuboid_cuboid_find_local_separating_normal_oneway;
223/// use parry2d::math::{Pose, Vector};
224///
225/// let rect1 = Cuboid::new(Vector::new(1.0, 1.0));
226/// let rect2 = Cuboid::new(Vector::new(0.5, 0.5));
227///
228/// // Position rect2 to the right of rect1
229/// let pos12 = Pose::translation(2.5, 0.0);
230///
231/// // Test rect1's face normals (X and Y axes)
232/// let (sep1, normal1) = cuboid_cuboid_find_local_separating_normal_oneway(
233///     &rect1, &rect2, &pos12
234/// );
235///
236/// // Test rect2's face normals by swapping arguments and inverting the transform
237/// let (sep2, normal2) = cuboid_cuboid_find_local_separating_normal_oneway(
238///     &rect2, &rect1, &pos12.inverse()
239/// );
240///
241/// // The maximum separation indicates if shapes collide
242/// let max_separation = sep1.max(sep2);
243/// if max_separation > 0.0 {
244///     println!("Shapes are separated!");
245/// }
246/// # }
247/// ```
248pub fn cuboid_cuboid_find_local_separating_normal_oneway(
249    cuboid1: &Cuboid,
250    cuboid2: &Cuboid,
251    pos12: &Pose,
252) -> (Real, Vector) {
253    // The largest separation over all axes, which is what SAT reports.
254    let mut best_separation = -Real::MAX;
255    let mut best_dir = Vector::ZERO;
256    let mut axis_acc = Vector::ZERO;
257    let mut num_positive = 0;
258
259    for i in 0..DIM {
260        #[expect(clippy::unnecessary_cast)]
261        let sign = pos12.translation.vget(i).copy_sign_to(1.0 as Real);
262        let axis1 = Vector::ith(i, sign);
263        let axis2 = pos12.rotation.inverse() * -axis1;
264        let local_pt2 = cuboid2.local_support_point(axis2);
265        let pt2 = pos12 * local_pt2;
266        let separation = pt2.vget(i) * sign - cuboid1.half_extents.vget(i);
267
268        if separation > best_separation {
269            best_separation = separation;
270            best_dir = axis1;
271        }
272
273        if separation >= 0.0 {
274            // Add the axis weighted by the separation.
275            // If the separation is exactly zero, add a little separation to
276            // keep a little nudge towards that direction. The resulting axis remains
277            // correct as it will be a linear combination of the separating axis (so it
278            // will be contained by the vertex/edge’s normal cone).
279            axis_acc[i] += sign * separation.max(Real::EPSILON);
280            num_positive += 1;
281        }
282    }
283
284    if num_positive > 1 {
285        let axis_acc_dir = axis_acc.normalize();
286        let pt1 = cuboid1.local_support_point(axis_acc_dir);
287        let pt2 = cuboid2.support_point(pos12, -axis_acc_dir);
288        let separation = (pt2 - pt1).dot(axis_acc_dir);
289        debug_assert!(separation >= 0.0);
290        (separation, axis_acc_dir)
291    } else {
292        (best_separation, best_dir)
293    }
294}