1use super::proxy_distance::{proxy_distance, SimplexCache};
8use super::separation::SeparationFunction;
9use super::sweep::{rotate_vec, Sweep};
10use super::toi_proxy::ToiProxy;
11use crate::math::{Real, Vector};
12
13#[derive(Copy, Clone, Debug, PartialEq, Eq)]
15pub enum SweepToiStatus {
16 Overlapped,
19 Hit,
21 Separated,
24 Failed,
26}
27
28#[derive(Copy, Clone, Debug)]
30pub struct SweepToiOutput {
31 pub status: SweepToiStatus,
33 pub fraction: Real,
35 pub point: Vector,
37 pub normal: Vector,
40}
41
42pub fn sweep_time_of_impact(
48 proxy_a: &ToiProxy,
49 sweep_a: &Sweep,
50 proxy_b: &ToiProxy,
51 sweep_b: &Sweep,
52 max_fraction: Real,
53 linear_slop: Real,
54) -> SweepToiOutput {
55 let mut output = SweepToiOutput {
56 status: SweepToiStatus::Separated,
57 fraction: max_fraction,
58 point: Vector::ZERO,
59 normal: Vector::ZERO,
60 };
61
62 let origin = sweep_a.c1;
64 let sweep_a = sweep_a.shifted(origin);
65 let sweep_b = sweep_b.shifted(origin);
66
67 #[cfg(feature = "dim2")]
68 let max_push_back_iterations = 8; #[cfg(feature = "dim3")]
70 let max_push_back_iterations = proxy_a.points().len() + proxy_b.points().len();
71
72 #[cfg(feature = "dim2")]
73 const MAX_DISTANCE_ITERATIONS: u32 = 20;
74 #[cfg(feature = "dim3")]
75 const MAX_DISTANCE_ITERATIONS: u32 = 25;
76
77 let t_max = max_fraction;
78
79 let total_radius = proxy_a.radius + proxy_b.radius;
81 let target = linear_slop.max(total_radius - linear_slop);
82 let tolerance = 0.25 * linear_slop;
83 debug_assert!(target > tolerance);
84
85 let mut t1 = 0.0;
86 let mut distance_iterations = 0u32;
87 let mut cache = SimplexCache::default();
88
89 loop {
92 let xf_a = sweep_a.transform_at(t1);
95 let xf_b = sweep_b.transform_at(t1);
96 let pos12 = xf_a.inv_mul(&xf_b);
97 let distance_output = proxy_distance(&pos12, proxy_a, proxy_b, false, &mut cache);
98
99 let world_normal = rotate_vec(&xf_a.rotation, distance_output.normal);
102 let world_point_a = xf_a.transform_point(distance_output.point_a);
103 let world_point_b = xf_a.transform_point(distance_output.point_b);
104
105 distance_iterations += 1;
106
107 let averaged_hit_point = || {
108 let pa = world_point_a + proxy_a.radius * world_normal;
109 let pb = world_point_b - proxy_b.radius * world_normal;
110 0.5 * (pa + pb) + origin
111 };
112
113 if distance_output.distance <= 0.0 {
115 output.status = SweepToiStatus::Overlapped;
116 output.fraction = 0.0;
117 break;
118 }
119
120 if distance_output.distance <= target + tolerance {
121 output.status = SweepToiStatus::Hit;
123 output.point = averaged_hit_point();
124 output.normal = world_normal;
125 output.fraction = t1;
126 break;
127 }
128
129 #[cfg(feature = "dim3")]
131 if distance_iterations == MAX_DISTANCE_ITERATIONS {
132 output.status = SweepToiStatus::Failed;
135 output.fraction = t1;
136 output.point = averaged_hit_point();
137 output.normal = world_normal;
138 break;
139 }
140
141 let mut fcn = SeparationFunction::new(
143 &cache,
144 proxy_a,
145 &sweep_a,
146 proxy_b,
147 &sweep_b,
148 world_normal,
149 t1,
150 );
151
152 let mut done = false;
155 let mut t2 = t_max;
156 let mut push_back_iterations = 0;
157 loop {
158 let (mut s2, index_a, index_b) = fcn.find_min_separation(t2);
160
161 if s2 - target > tolerance {
163 output.status = SweepToiStatus::Separated;
165 output.fraction = t_max;
166 done = true;
167 break;
168 }
169
170 if s2 >= target - tolerance {
172 t1 = t2;
174 break;
175 }
176
177 let mut s1 = fcn.evaluate(index_a, index_b, t1);
179
180 if s1 < target - tolerance {
183 output.status = SweepToiStatus::Failed;
184 output.fraction = t1;
185 done = true;
186 break;
187 }
188
189 if s1 <= target + tolerance {
191 output.status = SweepToiStatus::Hit;
193 output.point = averaged_hit_point();
194 output.normal = world_normal;
195 output.fraction = t1;
196 done = true;
197 break;
198 }
199
200 let mut root_iteration_count = 0;
202 const MAX_ROOT_ITERATIONS: u32 = 50;
203 let mut a1 = t1;
204 let mut a2 = t2;
205 loop {
206 let t = if root_iteration_count & 1 == 1 {
208 a1 + (target - s1) * (a2 - a1) / (s2 - s1)
210 } else {
211 0.5 * (a1 + a2)
213 };
214
215 root_iteration_count += 1;
216
217 let s = fcn.evaluate(index_a, index_b, t);
218
219 if (s - target).abs() <= tolerance {
221 t2 = t;
223 break;
224 }
225
226 if s > target {
228 a1 = t;
229 s1 = s;
230 } else {
231 a2 = t;
232 s2 = s;
233 }
234
235 if root_iteration_count == MAX_ROOT_ITERATIONS {
236 break;
237 }
238 }
239
240 if root_iteration_count == MAX_ROOT_ITERATIONS - 1 && fcn.uses_edge_axis() {
242 t2 = t_max;
243 fcn.force_fixed_axis(t1);
244 }
245
246 push_back_iterations += 1;
247 if push_back_iterations == max_push_back_iterations {
248 break;
249 }
250 }
251
252 if done {
253 break;
254 }
255
256 #[cfg(feature = "dim2")]
259 if distance_iterations == MAX_DISTANCE_ITERATIONS {
260 output.status = SweepToiStatus::Failed;
262 output.point = averaged_hit_point();
263 output.normal = world_normal;
264 output.fraction = t1;
265 break;
266 }
267 }
268
269 output
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use crate::math::{Pose, Rotation};
276
277 fn slop() -> Real {
278 0.005
279 }
280
281 #[cfg(feature = "dim2")]
282 fn pose(x: Real, y: Real) -> Pose {
283 Pose::from_parts(Vector::new(x, y), Rotation::identity())
284 }
285 #[cfg(feature = "dim3")]
286 fn pose(x: Real, y: Real) -> Pose {
287 Pose::from_parts(Vector::new(x, y, 0.0), Rotation::IDENTITY)
288 }
289
290 fn ball_proxy(radius: Real) -> ToiProxy<'static> {
291 ToiProxy::point(Vector::ZERO, radius)
292 }
293
294 fn cuboid_proxy(half_extent: Real) -> ToiProxy<'static> {
295 #[cfg(feature = "dim2")]
296 {
297 ToiProxy::from_array(
298 [
299 Vector::new(-half_extent, -half_extent),
300 Vector::new(half_extent, -half_extent),
301 Vector::new(half_extent, half_extent),
302 Vector::new(-half_extent, half_extent),
303 ],
304 0.0,
305 )
306 }
307 #[cfg(feature = "dim3")]
308 {
309 let h = half_extent;
310 ToiProxy::from_array(
311 [
312 Vector::new(-h, -h, -h),
313 Vector::new(h, -h, -h),
314 Vector::new(h, h, -h),
315 Vector::new(-h, h, -h),
316 Vector::new(-h, -h, h),
317 Vector::new(h, -h, h),
318 Vector::new(h, h, h),
319 Vector::new(-h, h, h),
320 ],
321 0.0,
322 )
323 }
324 }
325
326 #[test]
327 fn ball_hits_static_box() {
328 let wall = cuboid_proxy(0.5);
330 let wall_sweep = Sweep::constant(&pose(0.0, 0.0), Vector::ZERO);
331 let ball = ball_proxy(0.1);
332 let ball_sweep = Sweep::from_poses(&pose(-3.0, 0.0), &pose(3.0, 0.0), Vector::ZERO);
333
334 let result = sweep_time_of_impact(&wall, &wall_sweep, &ball, &ball_sweep, 1.0, slop());
335 assert_eq!(result.status, SweepToiStatus::Hit);
336
337 let expected = (3.0 - 0.5 - (0.1 - slop())) / 6.0;
340 assert!(
341 (result.fraction - expected).abs() < 0.001,
342 "fraction {} vs expected {expected}",
343 result.fraction
344 );
345 }
346
347 #[test]
348 fn ball_misses_box() {
349 let wall = cuboid_proxy(0.5);
351 let wall_sweep = Sweep::constant(&pose(0.0, 0.0), Vector::ZERO);
352 let ball = ball_proxy(0.1);
353 let ball_sweep = Sweep::from_poses(&pose(-3.0, 5.0), &pose(3.0, 5.0), Vector::ZERO);
354
355 let result = sweep_time_of_impact(&wall, &wall_sweep, &ball, &ball_sweep, 1.0, slop());
356 assert_eq!(result.status, SweepToiStatus::Separated);
357 assert_eq!(result.fraction, 1.0);
358 }
359
360 #[test]
361 fn overlapped_at_start_returns_fraction_zero() {
362 let wall = cuboid_proxy(0.5);
363 let wall_sweep = Sweep::constant(&pose(0.0, 0.0), Vector::ZERO);
364 let ball = ball_proxy(0.1);
365 let ball_sweep = Sweep::from_poses(&pose(0.0, 0.0), &pose(3.0, 0.0), Vector::ZERO);
366
367 let result = sweep_time_of_impact(&wall, &wall_sweep, &ball, &ball_sweep, 1.0, slop());
368 assert_eq!(result.status, SweepToiStatus::Overlapped);
369 assert_eq!(result.fraction, 0.0);
370 }
371
372 #[test]
373 fn box_vs_box_face_impact() {
374 let a = cuboid_proxy(0.5);
376 let a_sweep = Sweep::constant(&pose(0.0, 0.0), Vector::ZERO);
377 let b = cuboid_proxy(0.5);
378 let b_sweep = Sweep::from_poses(&pose(-4.0, 0.0), &pose(4.0, 0.0), Vector::ZERO);
379
380 let result = sweep_time_of_impact(&a, &a_sweep, &b, &b_sweep, 1.0, slop());
381 assert_eq!(result.status, SweepToiStatus::Hit);
382
383 let expected = (4.0 - 1.0 - slop()) / 8.0;
385 assert!(
386 (result.fraction - expected).abs() < 0.001,
387 "fraction {} vs expected {expected}",
388 result.fraction
389 );
390 }
391
392 #[test]
393 fn rotating_bar_hits_ball() {
394 let half_length = 2.0;
397 #[cfg(feature = "dim2")]
398 let (bar, start, end) = (
399 ToiProxy::from_array(
400 [
401 Vector::new(-half_length, 0.0),
402 Vector::new(half_length, 0.0),
403 ],
404 0.05,
405 ),
406 Pose::from_parts(Vector::ZERO, Rotation::identity()),
407 Pose::from_parts(
408 Vector::ZERO,
409 Rotation::from_angle(core::f32::consts::FRAC_PI_2 as Real),
410 ),
411 );
412 #[cfg(feature = "dim3")]
413 let (bar, start, end) = (
414 ToiProxy::from_array(
415 [
416 Vector::new(-half_length, 0.0, 0.0),
417 Vector::new(half_length, 0.0, 0.0),
418 ],
419 0.05,
420 ),
421 Pose::from_parts(Vector::ZERO, Rotation::IDENTITY),
422 Pose::from_parts(
423 Vector::ZERO,
424 Rotation::from_rotation_z(core::f32::consts::FRAC_PI_2 as Real),
425 ),
426 );
427
428 let bar_sweep = Sweep::from_poses(&start, &end, Vector::ZERO);
429 let ball = ball_proxy(0.1);
430 let d = 1.5 * (0.5_f64.sqrt() as Real);
432 let ball_sweep = Sweep::constant(&pose(d, d), Vector::ZERO);
433
434 let result = sweep_time_of_impact(&bar, &bar_sweep, &ball, &ball_sweep, 1.0, slop());
435 assert_eq!(result.status, SweepToiStatus::Hit);
436 assert!(
438 result.fraction > 0.3 && result.fraction < 0.5,
439 "fraction {}",
440 result.fraction
441 );
442 }
443}