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}