1use crate::narrowphase::epa::Epa;
6use crate::narrowphase::gjk::{Gjk, GjkResult};
7use crate::narrowphase::specialized;
8use crate::types::{CollisionPair, Contact, ContactManifold};
9use oxiphysics_core::Transform;
10use oxiphysics_geometry::{BoxShape, Capsule, Shape, Sphere};
11
12use super::types::{
13 CompoundDispatchResult, CompoundShape, ConcaveMesh, ContactPatch, MeshTriangle,
14 NarrowPhaseDispatcher, NarrowPhaseResult, ShapeType, SimpleHeightField, SpeculativeConfig,
15};
16
17pub type SpecBatchEntry<'a> = (
19 &'a dyn Shape,
20 &'a Transform,
21 [f64; 3],
22 &'a dyn Shape,
23 &'a Transform,
24 [f64; 3],
25 CollisionPair,
26);
27
28pub(super) fn shape_type_ordinal(t: ShapeType) -> u32 {
30 match t {
31 ShapeType::Sphere => 0,
32 ShapeType::Box => 1,
33 ShapeType::Capsule => 2,
34 ShapeType::Cylinder => 3,
35 ShapeType::Cone => 4,
36 ShapeType::ConvexHull => 5,
37 ShapeType::TriangleMesh => 6,
38 ShapeType::Compound => 7,
39 ShapeType::HeightField => 8,
40 ShapeType::Plane => 9,
41 ShapeType::Custom(id) => 100 + id,
42 }
43}
44pub type NarrowPhaseFn = fn(
46 shape_a: &dyn Shape,
47 transform_a: &Transform,
48 shape_b: &dyn Shape,
49 transform_b: &Transform,
50 pair: CollisionPair,
51) -> NarrowPhaseResult;
52pub(super) fn sphere_capsule_dispatch(
53 shape_a: &dyn Shape,
54 transform_a: &Transform,
55 shape_b: &dyn Shape,
56 transform_b: &Transform,
57 pair: CollisionPair,
58) -> NarrowPhaseResult {
59 use oxiphysics_core::math::Vec3;
60 let sphere = unsafe { &*(shape_a as *const dyn Shape as *const Sphere) };
61 let capsule = unsafe { &*(shape_b as *const dyn Shape as *const Capsule) };
62 let sphere_center = transform_a.position;
63 let half_h = capsule.half_height;
64 let cap_up = transform_b.rotation * Vec3::new(0.0, half_h, 0.0);
65 let cap_a = transform_b.position + cap_up;
66 let cap_b = transform_b.position - cap_up;
67 let ab = cap_b - cap_a;
68 let ab_len2 = ab.dot(&ab);
69 let t = if ab_len2 > 1e-12 {
70 ((sphere_center - cap_a).dot(&ab) / ab_len2).clamp(0.0, 1.0)
71 } else {
72 0.5
73 };
74 let closest = cap_a + ab * t;
75 let diff = sphere_center - closest;
76 let dist = diff.norm();
77 let combined = sphere.radius + capsule.radius;
78 if dist >= combined {
79 return NarrowPhaseResult::separated();
80 }
81 let normal = if dist > 1e-10 {
82 diff / dist
83 } else {
84 Vec3::new(0.0, 1.0, 0.0)
85 };
86 let depth = combined - dist;
87 let point_a = sphere_center - normal * sphere.radius;
88 let point_b = closest + normal * capsule.radius;
89 let contact = Contact::new(point_a, point_b, normal, depth);
90 let mut m = ContactManifold::new(pair);
91 m.add_contact(contact);
92 NarrowPhaseResult::contact(m)
93}
94pub(super) fn gjk_fallback_dispatch(
95 shape_a: &dyn Shape,
96 transform_a: &Transform,
97 shape_b: &dyn Shape,
98 transform_b: &Transform,
99 pair: CollisionPair,
100) -> NarrowPhaseResult {
101 match gjk_epa(shape_a, transform_a, shape_b, transform_b, pair) {
102 Some(manifold) => NarrowPhaseResult::contact(manifold),
103 None => NarrowPhaseResult::separated(),
104 }
105}
106pub fn dispatch_compound(
109 dispatcher: &NarrowPhaseDispatcher,
110 compound_shapes: &[(ShapeType, &dyn Shape, Transform)],
111 other_shape: &dyn Shape,
112 other_type: ShapeType,
113 other_transform: &Transform,
114 base_pair: CollisionPair,
115) -> NarrowPhaseResult {
116 let mut all_contacts = Vec::new();
117 for (child_type, child_shape, child_transform) in compound_shapes {
118 let result = dispatcher.dispatch(
119 *child_shape,
120 *child_type,
121 child_transform,
122 other_shape,
123 other_type,
124 other_transform,
125 base_pair,
126 );
127 if let Some(manifold) = result.manifold {
128 all_contacts.extend(manifold.contacts);
129 }
130 }
131 if all_contacts.is_empty() {
132 NarrowPhaseResult::separated()
133 } else {
134 let mut manifold = ContactManifold::new(base_pair);
135 for c in all_contacts {
136 manifold.add_contact(c);
137 }
138 NarrowPhaseResult::contact(manifold)
139 }
140}
141pub(super) fn try_specialized(
143 shape_a: &dyn Shape,
144 transform_a: &Transform,
145 shape_b: &dyn Shape,
146 transform_b: &Transform,
147) -> Option<Contact> {
148 let name_a = type_name(shape_a);
149 let name_b = type_name(shape_b);
150 match (name_a, name_b) {
151 ("Sphere", "Sphere") => {
152 let s1 = as_sphere(shape_a)?;
153 let s2 = as_sphere(shape_b)?;
154 specialized::sphere_sphere(s1, transform_a, s2, transform_b)
155 }
156 ("Sphere", "BoxShape") => {
157 let s = as_sphere(shape_a)?;
158 let b = as_box(shape_b)?;
159 specialized::sphere_box(s, transform_a, b, transform_b)
160 }
161 ("BoxShape", "Sphere") => {
162 let b = as_box(shape_a)?;
163 let s = as_sphere(shape_b)?;
164 let mut c = specialized::sphere_box(s, transform_b, b, transform_a)?;
165 c.normal = -c.normal;
166 std::mem::swap(&mut c.point_a, &mut c.point_b);
167 Some(c)
168 }
169 ("BoxShape", "BoxShape") => {
170 let b1 = as_box(shape_a)?;
171 let b2 = as_box(shape_b)?;
172 specialized::box_box_sat(b1, transform_a, b2, transform_b)
173 }
174 ("Capsule", "Capsule") => {
175 let c1 = as_capsule(shape_a)?;
176 let c2 = as_capsule(shape_b)?;
177 specialized::capsule_capsule(c1, transform_a, c2, transform_b)
178 }
179 _ => None,
180 }
181}
182pub(super) fn gjk_epa(
183 shape_a: &dyn Shape,
184 transform_a: &Transform,
185 shape_b: &dyn Shape,
186 transform_b: &Transform,
187 pair: CollisionPair,
188) -> Option<ContactManifold> {
189 let result = Gjk::query(shape_a, transform_a, shape_b, transform_b);
190 match result {
191 GjkResult::Intersecting(simplex) => {
192 let contact =
193 Epa::penetration_depth(shape_a, transform_a, shape_b, transform_b, &simplex)?;
194 let mut manifold = ContactManifold::new(pair);
195 manifold.add_contact(contact);
196 Some(manifold)
197 }
198 GjkResult::Separated { .. } => None,
199 }
200}
201pub(super) fn type_name(shape: &dyn Shape) -> &'static str {
203 let debug = format!("{:?}", shape);
204 if debug.starts_with("Sphere") {
205 "Sphere"
206 } else if debug.starts_with("BoxShape") {
207 "BoxShape"
208 } else if debug.starts_with("Capsule") {
209 "Capsule"
210 } else if debug.starts_with("Cylinder") {
211 "Cylinder"
212 } else if debug.starts_with("Cone") {
213 "Cone"
214 } else {
215 "Unknown"
216 }
217}
218pub(super) fn as_sphere(shape: &dyn Shape) -> Option<&Sphere> {
219 let s = shape.support_point(&oxiphysics_core::math::Vec3::new(1.0, 0.0, 0.0));
220 let radius = s.x;
221 if radius > 0.0 {
222 let ptr = shape as *const dyn Shape as *const Sphere;
223 Some(unsafe { &*ptr })
224 } else {
225 None
226 }
227}
228pub(super) fn as_box(shape: &dyn Shape) -> Option<&BoxShape> {
229 let ptr = shape as *const dyn Shape as *const BoxShape;
230 Some(unsafe { &*ptr })
231}
232pub(super) fn as_capsule(shape: &dyn Shape) -> Option<&Capsule> {
233 let ptr = shape as *const dyn Shape as *const Capsule;
234 Some(unsafe { &*ptr })
235}
236pub fn dispatch_compound_compound(
241 dispatcher: &NarrowPhaseDispatcher,
242 compound_a: &CompoundShape,
243 transform_a: &Transform,
244 compound_b: &CompoundShape,
245 transform_b: &Transform,
246 pair_id: u32,
247 use_aabb_pruning: bool,
248) -> CompoundDispatchResult {
249 let mut manifolds = Vec::new();
250 let mut tests = 0usize;
251 let mut pruned = 0usize;
252 let mut pair_counter = (pair_id as usize) * 1000;
253 for (ia, (child_a, ta_local)) in compound_a
254 .children
255 .iter()
256 .zip(compound_a.local_transforms.iter())
257 .enumerate()
258 {
259 let world_a = compose_transforms(transform_a, ta_local);
260 for (ib, (child_b, tb_local)) in compound_b
261 .children
262 .iter()
263 .zip(compound_b.local_transforms.iter())
264 .enumerate()
265 {
266 let world_b = compose_transforms(transform_b, tb_local);
267 if use_aabb_pruning {
268 let pa = world_a.position;
269 let pb = world_b.position;
270 let dist2 = {
271 let dx = pa.x - pb.x;
272 let dy = pa.y - pb.y;
273 let dz = pa.z - pb.z;
274 dx * dx + dy * dy + dz * dz
275 };
276 if dist2 > 25.0 {
277 pruned += 1;
278 continue;
279 }
280 }
281 let sub_pair = CollisionPair::new(ia, ib + pair_counter);
282 pair_counter += 1;
283 tests += 1;
284 let result = dispatcher.dispatch(
285 child_a.as_ref(),
286 compound_a.child_types[ia],
287 &world_a,
288 child_b.as_ref(),
289 compound_b.child_types[ib],
290 &world_b,
291 sub_pair,
292 );
293 if result.has_contact()
294 && let Some(m) = result.manifold
295 {
296 manifolds.push(m);
297 }
298 }
299 }
300 CompoundDispatchResult {
301 manifolds,
302 tests_performed: tests,
303 pairs_pruned: pruned,
304 }
305}
306pub(super) fn compose_transforms(parent: &Transform, local: &Transform) -> Transform {
308 parent.compose(local)
309}
310pub fn dispatch_heightfield_sphere(
314 hf: &SimpleHeightField,
315 sphere_center: [f64; 3],
316 sphere_radius: f64,
317 pair: CollisionPair,
318) -> NarrowPhaseResult {
319 if let Some((point, normal, depth)) = hf.test_sphere(sphere_center, sphere_radius) {
320 let contact = Contact {
321 point_a: oxiphysics_core::math::Vec3::new(point[0], point[1], point[2]),
322 point_b: oxiphysics_core::math::Vec3::new(
323 sphere_center[0] - normal[0] * sphere_radius,
324 sphere_center[1] - normal[1] * sphere_radius,
325 sphere_center[2] - normal[2] * sphere_radius,
326 ),
327 normal: oxiphysics_core::math::Vec3::new(normal[0], normal[1], normal[2]),
328 depth,
329 };
330 let mut m = ContactManifold::new(pair);
331 m.add_contact(contact);
332 NarrowPhaseResult::contact(m)
333 } else {
334 NarrowPhaseResult::separated()
335 }
336}
337pub fn mesh_mesh_aabb_mid_phase(
341 triangles_a: &[MeshTriangle],
342 triangles_b: &[MeshTriangle],
343) -> Vec<(usize, usize)> {
344 let mut pairs = Vec::new();
345 for (ia, ta) in triangles_a.iter().enumerate() {
346 for (ib, tb) in triangles_b.iter().enumerate() {
347 if ta.aabb.overlaps(&tb.aabb) {
348 pairs.push((ia, ib));
349 }
350 }
351 }
352 pairs
353}
354#[cfg(test)]
355mod tests {
356 use super::*;
357 use crate::CollisionPair;
358 use crate::Contact;
359 use crate::ContactManifold;
360 use crate::NarrowPhaseDispatcher;
361
362 use crate::narrowphase::Aabb;
363
364 use crate::narrowphase::DispatchConfig;
365 use crate::narrowphase::DispatchKey;
366
367 use crate::narrowphase::DispatchStats;
368 use crate::narrowphase::ShapeFeatureCache;
369 use crate::narrowphase::SimpleHeightField;
370
371 use crate::narrowphase::dispatch::ShapeType;
372
373 use oxiphysics_core::Vec3;
374 use std::collections::HashMap;
375 #[test]
376 fn test_dispatch_sphere_sphere() {
377 let s1 = Sphere::new(1.0);
378 let s2 = Sphere::new(1.0);
379 let t1 = Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
380 let t2 = Transform::from_position(Vec3::new(1.5, 0.0, 0.0));
381 let pair = CollisionPair::new(0, 1);
382 let manifold = NarrowPhaseDispatcher::generate_contacts(&s1, &t1, &s2, &t2, pair);
383 assert!(manifold.is_some());
384 let m = manifold.unwrap();
385 assert_eq!(m.contacts.len(), 1);
386 }
387 #[test]
388 fn test_dispatch_separated() {
389 let s1 = Sphere::new(1.0);
390 let s2 = Sphere::new(1.0);
391 let t1 = Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
392 let t2 = Transform::from_position(Vec3::new(5.0, 0.0, 0.0));
393 let pair = CollisionPair::new(0, 1);
394 let manifold = NarrowPhaseDispatcher::generate_contacts(&s1, &t1, &s2, &t2, pair);
395 assert!(manifold.is_none());
396 }
397 #[test]
398 fn test_dispatch_key_canonical() {
399 let k1 = DispatchKey::new(ShapeType::Sphere, ShapeType::Box);
400 let k2 = DispatchKey::new(ShapeType::Box, ShapeType::Sphere);
401 assert_eq!(k1, k2, "DispatchKey should be order-independent");
402 }
403 #[test]
404 fn test_dispatch_key_same() {
405 let k = DispatchKey::new(ShapeType::Capsule, ShapeType::Capsule);
406 assert_eq!(k.0, k.1);
407 }
408 #[test]
409 fn test_default_dispatcher_sphere_sphere() {
410 let dispatcher = NarrowPhaseDispatcher::default();
411 let s1 = Sphere::new(1.0);
412 let s2 = Sphere::new(1.0);
413 let t1 = Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
414 let t2 = Transform::from_position(Vec3::new(1.5, 0.0, 0.0));
415 let pair = CollisionPair::new(0, 1);
416 let result = dispatcher.dispatch(
417 &s1,
418 ShapeType::Sphere,
419 &t1,
420 &s2,
421 ShapeType::Sphere,
422 &t2,
423 pair,
424 );
425 assert!(
426 result.manifold.is_some(),
427 "overlapping spheres should produce contact"
428 );
429 }
430 #[test]
431 fn test_default_dispatcher_sphere_sphere_separated() {
432 let dispatcher = NarrowPhaseDispatcher::default();
433 let s1 = Sphere::new(1.0);
434 let s2 = Sphere::new(1.0);
435 let t1 = Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
436 let t2 = Transform::from_position(Vec3::new(10.0, 0.0, 0.0));
437 let pair = CollisionPair::new(0, 1);
438 let result = dispatcher.dispatch(
439 &s1,
440 ShapeType::Sphere,
441 &t1,
442 &s2,
443 ShapeType::Sphere,
444 &t2,
445 pair,
446 );
447 assert!(
448 result.manifold.is_none(),
449 "separated spheres should produce no contact"
450 );
451 }
452 #[test]
453 fn test_register_custom_pair() {
454 let mut dispatcher = NarrowPhaseDispatcher::empty();
455 dispatcher.register_pair(
456 ShapeType::Sphere,
457 ShapeType::Sphere,
458 |_sa, _ta, _sb, _tb, pair| {
459 use crate::types::Contact;
460 let contact = Contact {
461 point_a: Vec3::zeros(),
462 point_b: Vec3::zeros(),
463 normal: Vec3::new(0.0, 1.0, 0.0),
464 depth: 1.0,
465 };
466 let mut m = ContactManifold::new(pair);
467 m.add_contact(contact);
468 NarrowPhaseResult::contact(m)
469 },
470 );
471 let s1 = Sphere::new(1.0);
472 let s2 = Sphere::new(1.0);
473 let t1 = Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
474 let t2 = Transform::from_position(Vec3::new(100.0, 0.0, 0.0));
475 let pair = CollisionPair::new(0, 1);
476 let result = dispatcher.dispatch(
477 &s1,
478 ShapeType::Sphere,
479 &t1,
480 &s2,
481 ShapeType::Sphere,
482 &t2,
483 pair,
484 );
485 assert!(
486 result.manifold.is_some(),
487 "custom algorithm should produce contact"
488 );
489 }
490 #[test]
491 fn test_narrow_phase_result_separated() {
492 let r = NarrowPhaseResult::separated();
493 assert!(r.manifold.is_none());
494 assert!(!r.has_contact());
495 assert!(r.penetration_depth().is_none());
496 }
497 #[test]
498 fn test_narrow_phase_result_has_contact() {
499 let pair = CollisionPair::new(0, 1);
500 let mut m = ContactManifold::new(pair);
501 m.add_contact(Contact {
502 point_a: Vec3::zeros(),
503 point_b: Vec3::zeros(),
504 normal: Vec3::new(0.0, 1.0, 0.0),
505 depth: 0.5,
506 });
507 let r = NarrowPhaseResult::contact(m);
508 assert!(r.has_contact());
509 assert!((r.penetration_depth().unwrap() - 0.5).abs() < 1e-10);
510 }
511 #[test]
512 fn test_shape_type_all_variants() {
513 let types = [
514 ShapeType::Sphere,
515 ShapeType::Box,
516 ShapeType::Capsule,
517 ShapeType::Cylinder,
518 ShapeType::Cone,
519 ShapeType::ConvexHull,
520 ShapeType::TriangleMesh,
521 ShapeType::Compound,
522 ShapeType::HeightField,
523 ShapeType::Plane,
524 ShapeType::Custom(0),
525 ];
526 let mut map: HashMap<DispatchKey, usize> = HashMap::new();
527 for (i, &t) in types.iter().enumerate() {
528 map.insert(DispatchKey::new(t, t), i);
529 }
530 assert_eq!(map.len(), types.len());
531 }
532 #[test]
533 fn test_dispatch_key_custom_types() {
534 let k1 = DispatchKey::new(ShapeType::Custom(0), ShapeType::Custom(1));
535 let k2 = DispatchKey::new(ShapeType::Custom(1), ShapeType::Custom(0));
536 assert_eq!(k1, k2, "custom type keys should be canonical");
537 }
538 #[test]
539 fn test_unregister_pair() {
540 let mut dispatcher = NarrowPhaseDispatcher::default();
541 assert!(dispatcher.has_pair(ShapeType::Sphere, ShapeType::Sphere));
542 let removed = dispatcher.unregister_pair(ShapeType::Sphere, ShapeType::Sphere);
543 assert!(removed);
544 assert!(!dispatcher.has_pair(ShapeType::Sphere, ShapeType::Sphere));
545 }
546 #[test]
547 fn test_unregister_nonexistent() {
548 let mut dispatcher = NarrowPhaseDispatcher::empty();
549 let removed = dispatcher.unregister_pair(ShapeType::Cone, ShapeType::Cone);
550 assert!(!removed);
551 }
552 #[test]
553 fn test_registered_count() {
554 let dispatcher = NarrowPhaseDispatcher::default();
555 assert_eq!(dispatcher.registered_count(), 6);
556 }
557 #[test]
558 fn test_registered_keys() {
559 let dispatcher = NarrowPhaseDispatcher::default();
560 let keys = dispatcher.registered_keys();
561 assert_eq!(keys.len(), 6);
562 }
563 #[test]
564 fn test_has_pair() {
565 let dispatcher = NarrowPhaseDispatcher::default();
566 assert!(dispatcher.has_pair(ShapeType::Sphere, ShapeType::Sphere));
567 assert!(dispatcher.has_pair(ShapeType::Sphere, ShapeType::Box));
568 assert!(dispatcher.has_pair(ShapeType::Box, ShapeType::Sphere));
569 assert!(!dispatcher.has_pair(ShapeType::Cone, ShapeType::Cone));
570 }
571 #[test]
572 fn test_sphere_capsule_overlapping() {
573 let dispatcher = NarrowPhaseDispatcher::default();
574 let sphere = Sphere::new(1.0);
575 let capsule = Capsule::new(0.5, 1.0);
576 let t1 = Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
577 let t2 = Transform::from_position(Vec3::new(1.0, 0.0, 0.0));
578 let pair = CollisionPair::new(0, 1);
579 let result = dispatcher.dispatch(
580 &sphere,
581 ShapeType::Sphere,
582 &t1,
583 &capsule,
584 ShapeType::Capsule,
585 &t2,
586 pair,
587 );
588 assert!(
589 result.has_contact(),
590 "overlapping sphere-capsule should produce contact"
591 );
592 }
593 #[test]
594 fn test_sphere_capsule_separated() {
595 let dispatcher = NarrowPhaseDispatcher::default();
596 let sphere = Sphere::new(0.5);
597 let capsule = Capsule::new(0.5, 1.0);
598 let t1 = Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
599 let t2 = Transform::from_position(Vec3::new(5.0, 0.0, 0.0));
600 let pair = CollisionPair::new(0, 1);
601 let result = dispatcher.dispatch(
602 &sphere,
603 ShapeType::Sphere,
604 &t1,
605 &capsule,
606 ShapeType::Capsule,
607 &t2,
608 pair,
609 );
610 assert!(
611 !result.has_contact(),
612 "separated sphere-capsule should not produce contact"
613 );
614 }
615 #[test]
616 fn test_sphere_capsule_contact_depth() {
617 let dispatcher = NarrowPhaseDispatcher::default();
618 let sphere = Sphere::new(1.0);
619 let capsule = Capsule::new(1.0, 1.0);
620 let t1 = Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
621 let t2 = Transform::from_position(Vec3::new(1.5, 0.0, 0.0));
622 let pair = CollisionPair::new(0, 1);
623 let result = dispatcher.dispatch(
624 &sphere,
625 ShapeType::Sphere,
626 &t1,
627 &capsule,
628 ShapeType::Capsule,
629 &t2,
630 pair,
631 );
632 assert!(result.has_contact());
633 let depth = result.penetration_depth().unwrap();
634 assert!(depth > 0.0, "penetration depth should be positive");
635 assert!(
636 (depth - 0.5).abs() < 1e-6,
637 "expected depth 0.5, got {}",
638 depth
639 );
640 }
641 #[test]
642 fn test_gjk_fallback_for_unknown_pair() {
643 let dispatcher = NarrowPhaseDispatcher::empty();
644 let s1 = Sphere::new(1.0);
645 let s2 = Sphere::new(1.0);
646 let t1 = Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
647 let t2 = Transform::from_position(Vec3::new(1.0, 0.0, 0.0));
648 let pair = CollisionPair::new(0, 1);
649 let result = dispatcher.dispatch(
650 &s1,
651 ShapeType::Sphere,
652 &t1,
653 &s2,
654 ShapeType::Sphere,
655 &t2,
656 pair,
657 );
658 assert!(
659 result.has_contact(),
660 "GJK fallback should detect overlapping spheres"
661 );
662 }
663 #[test]
664 fn test_gjk_fallback_separated() {
665 let dispatcher = NarrowPhaseDispatcher::empty();
666 let s1 = Sphere::new(1.0);
667 let s2 = Sphere::new(1.0);
668 let t1 = Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
669 let t2 = Transform::from_position(Vec3::new(10.0, 0.0, 0.0));
670 let pair = CollisionPair::new(0, 1);
671 let result = dispatcher.dispatch(
672 &s1,
673 ShapeType::Sphere,
674 &t1,
675 &s2,
676 ShapeType::Sphere,
677 &t2,
678 pair,
679 );
680 assert!(!result.has_contact());
681 }
682 #[test]
683 fn test_dispatch_config_default() {
684 let config = DispatchConfig::default();
685 assert!(config.use_gjk_fallback);
686 assert!(config.max_gjk_iterations > 0);
687 assert!(config.max_epa_iterations > 0);
688 assert!(config.contact_tolerance > 0.0);
689 }
690 #[test]
691 fn test_dispatcher_with_config() {
692 let config = DispatchConfig {
693 use_gjk_fallback: false,
694 max_gjk_iterations: 32,
695 max_epa_iterations: 32,
696 contact_tolerance: 1e-4,
697 };
698 let dispatcher = NarrowPhaseDispatcher::with_config(config);
699 assert_eq!(dispatcher.registered_count(), 0);
700 }
701 #[test]
702 fn test_heightfield_flat() {
703 let heights = vec![0.0; 9];
704 let hf = SimpleHeightField::new(heights, 3, 3, 1.0);
705 assert_eq!(hf.height_at(0, 0), 0.0);
706 assert_eq!(hf.height_at(1, 1), 0.0);
707 }
708 #[test]
709 fn test_heightfield_normal_flat() {
710 let heights = vec![0.0; 9];
711 let hf = SimpleHeightField::new(heights, 3, 3, 1.0);
712 let n = hf.normal_at(0, 0);
713 assert!((n[1] - 1.0).abs() < 1e-6);
714 }
715 #[test]
716 fn test_heightfield_sphere_collision() {
717 let heights = vec![0.0; 9];
718 let hf = SimpleHeightField::new(heights, 3, 3, 1.0);
719 let result = hf.test_sphere([1.0, 0.5, 1.0], 1.0);
720 assert!(result.is_some());
721 let (_, _, depth) = result.unwrap();
722 assert!(
723 (depth - 0.5).abs() < 1e-6,
724 "expected depth 0.5, got {}",
725 depth
726 );
727 }
728 #[test]
729 fn test_heightfield_sphere_no_collision() {
730 let heights = vec![0.0; 9];
731 let hf = SimpleHeightField::new(heights, 3, 3, 1.0);
732 let result = hf.test_sphere([1.0, 5.0, 1.0], 1.0);
733 assert!(result.is_none());
734 }
735 #[test]
736 fn test_heightfield_varied_heights() {
737 let heights = vec![0.0, 1.0, 0.0, 1.0, 2.0, 1.0, 0.0, 1.0, 0.0];
738 let hf = SimpleHeightField::new(heights, 3, 3, 1.0);
739 assert_eq!(hf.height_at(1, 1), 2.0);
740 assert_eq!(hf.height_at(0, 1), 1.0);
741 }
742 #[test]
743 fn test_dispatch_symmetric_same_as_normal() {
744 let dispatcher = NarrowPhaseDispatcher::default();
745 let s1 = Sphere::new(1.0);
746 let s2 = Sphere::new(1.0);
747 let t1 = Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
748 let t2 = Transform::from_position(Vec3::new(1.5, 0.0, 0.0));
749 let pair = CollisionPair::new(0, 1);
750 let r1 = dispatcher.dispatch(
751 &s1,
752 ShapeType::Sphere,
753 &t1,
754 &s2,
755 ShapeType::Sphere,
756 &t2,
757 pair,
758 );
759 let r2 = dispatcher.dispatch_symmetric(
760 &s1,
761 ShapeType::Sphere,
762 &t1,
763 &s2,
764 ShapeType::Sphere,
765 &t2,
766 pair,
767 );
768 assert_eq!(r1.has_contact(), r2.has_contact());
769 }
770 #[test]
771 fn test_shape_type_ordinal_ordering() {
772 assert!(shape_type_ordinal(ShapeType::Sphere) < shape_type_ordinal(ShapeType::Box));
773 assert!(shape_type_ordinal(ShapeType::Box) < shape_type_ordinal(ShapeType::Capsule));
774 assert!(shape_type_ordinal(ShapeType::HeightField) < shape_type_ordinal(ShapeType::Plane));
775 assert!(shape_type_ordinal(ShapeType::Plane) < shape_type_ordinal(ShapeType::Custom(0)));
776 }
777 #[test]
778 fn test_dispatch_key_plane() {
779 let k = DispatchKey::new(ShapeType::Plane, ShapeType::Sphere);
780 assert_eq!(k.0, ShapeType::Sphere);
781 assert_eq!(k.1, ShapeType::Plane);
782 }
783 #[test]
784 fn test_compound_shape_empty() {
785 let c = CompoundShape::new();
786 assert!(c.is_empty());
787 assert_eq!(c.len(), 0);
788 }
789 #[test]
790 fn test_compound_shape_add_children() {
791 let mut c = CompoundShape::new();
792 let s = Sphere::new(1.0);
793 let t = Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
794 c.add_child(Box::new(s), ShapeType::Sphere, t);
795 assert_eq!(c.len(), 1);
796 assert!(!c.is_empty());
797 }
798 #[test]
799 fn test_dispatch_compound_compound_no_overlap() {
800 let dispatcher = NarrowPhaseDispatcher::default();
801 let mut ca = CompoundShape::new();
802 ca.add_child(
803 Box::new(Sphere::new(0.5)),
804 ShapeType::Sphere,
805 Transform::from_position(Vec3::new(0.0, 0.0, 0.0)),
806 );
807 let mut cb = CompoundShape::new();
808 cb.add_child(
809 Box::new(Sphere::new(0.5)),
810 ShapeType::Sphere,
811 Transform::from_position(Vec3::new(0.0, 0.0, 0.0)),
812 );
813 let ta = Transform::from_position(Vec3::new(-100.0, 0.0, 0.0));
814 let tb = Transform::from_position(Vec3::new(100.0, 0.0, 0.0));
815 let result = dispatch_compound_compound(&dispatcher, &ca, &ta, &cb, &tb, 0, true);
816 assert!(
817 !result.has_contacts(),
818 "separated compounds should have no contacts"
819 );
820 }
821 #[test]
822 fn test_dispatch_compound_compound_overlap() {
823 let dispatcher = NarrowPhaseDispatcher::default();
824 let mut ca = CompoundShape::new();
825 ca.add_child(
826 Box::new(Sphere::new(1.0)),
827 ShapeType::Sphere,
828 Transform::from_position(Vec3::new(0.0, 0.0, 0.0)),
829 );
830 let mut cb = CompoundShape::new();
831 cb.add_child(
832 Box::new(Sphere::new(1.0)),
833 ShapeType::Sphere,
834 Transform::from_position(Vec3::new(0.0, 0.0, 0.0)),
835 );
836 let ta = Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
837 let tb = Transform::from_position(Vec3::new(1.0, 0.0, 0.0));
838 let result = dispatch_compound_compound(&dispatcher, &ca, &ta, &cb, &tb, 0, false);
839 assert!(
840 result.has_contacts(),
841 "overlapping compounds should have contacts"
842 );
843 }
844 #[test]
845 fn test_dispatch_compound_pruning_reduces_tests() {
846 let dispatcher = NarrowPhaseDispatcher::default();
847 let mut ca = CompoundShape::new();
848 let mut cb = CompoundShape::new();
849 ca.add_child(
850 Box::new(Sphere::new(0.1)),
851 ShapeType::Sphere,
852 Transform::from_position(Vec3::new(0.0, 0.0, 0.0)),
853 );
854 cb.add_child(
855 Box::new(Sphere::new(0.1)),
856 ShapeType::Sphere,
857 Transform::from_position(Vec3::new(0.0, 0.0, 0.0)),
858 );
859 let ta = Transform::from_position(Vec3::new(-100.0, 0.0, 0.0));
860 let tb = Transform::from_position(Vec3::new(100.0, 0.0, 0.0));
861 let pruned = dispatch_compound_compound(&dispatcher, &ca, &ta, &cb, &tb, 0, true);
862 let not_pruned = dispatch_compound_compound(&dispatcher, &ca, &ta, &cb, &tb, 0, false);
863 assert!(pruned.tests_performed <= not_pruned.tests_performed);
864 }
865 #[test]
866 fn test_heightfield_sphere_dispatch_contact() {
867 let hf = SimpleHeightField::new(vec![0.0; 9], 3, 3, 1.0);
868 let pair = CollisionPair::new(0, 1);
869 let result = dispatch_heightfield_sphere(&hf, [1.0, 0.5, 1.0], 1.0, pair);
870 assert!(
871 result.has_contact(),
872 "sphere penetrating heightfield should have contact"
873 );
874 }
875 #[test]
876 fn test_heightfield_sphere_dispatch_no_contact() {
877 let hf = SimpleHeightField::new(vec![0.0; 9], 3, 3, 1.0);
878 let pair = CollisionPair::new(0, 1);
879 let result = dispatch_heightfield_sphere(&hf, [1.0, 10.0, 1.0], 0.5, pair);
880 assert!(
881 !result.has_contact(),
882 "sphere above heightfield should have no contact"
883 );
884 }
885 #[test]
886 fn test_aabb_overlaps_true() {
887 let a = Aabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
888 let b = Aabb::new([0.5, 0.5, 0.5], [1.5, 1.5, 1.5]);
889 assert!(a.overlaps(&b));
890 }
891 #[test]
892 fn test_aabb_overlaps_false() {
893 let a = Aabb::new([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
894 let b = Aabb::new([2.0, 2.0, 2.0], [3.0, 3.0, 3.0]);
895 assert!(!a.overlaps(&b));
896 }
897 #[test]
898 fn test_aabb_surface_area() {
899 let a = Aabb::new([0.0, 0.0, 0.0], [2.0, 3.0, 4.0]);
900 let sa = a.surface_area();
901 assert!((sa - 52.0).abs() < 1e-10, "surface area should be 52: {sa}");
902 }
903 #[test]
904 fn test_mesh_triangle_aabb() {
905 let tri = MeshTriangle::new([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]], 0);
906 assert!((tri.aabb.min[0]).abs() < 1e-10);
907 assert!((tri.aabb.max[0] - 1.0).abs() < 1e-10);
908 assert!((tri.aabb.max[1] - 1.0).abs() < 1e-10);
909 }
910 #[test]
911 fn test_mesh_mesh_mid_phase_overlapping() {
912 let ta = vec![MeshTriangle::new(
913 [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]],
914 0,
915 )];
916 let tb = vec![MeshTriangle::new(
917 [[0.3, 0.0, 0.0], [1.3, 0.0, 0.0], [0.8, 1.0, 0.0]],
918 0,
919 )];
920 let pairs = mesh_mesh_aabb_mid_phase(&ta, &tb);
921 assert!(
922 !pairs.is_empty(),
923 "overlapping triangles should produce a pair"
924 );
925 }
926 #[test]
927 fn test_mesh_mesh_mid_phase_non_overlapping() {
928 let ta = vec![MeshTriangle::new(
929 [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.5, 1.0, 0.0]],
930 0,
931 )];
932 let tb = vec![MeshTriangle::new(
933 [[10.0, 0.0, 0.0], [11.0, 0.0, 0.0], [10.5, 1.0, 0.0]],
934 0,
935 )];
936 let pairs = mesh_mesh_aabb_mid_phase(&ta, &tb);
937 assert!(
938 pairs.is_empty(),
939 "separated triangles should produce no pairs"
940 );
941 }
942 #[test]
943 fn test_feature_cache_miss_then_hit() {
944 let mut cache = ShapeFeatureCache::new();
945 let v1 = cache.get_or_insert(0xAB, || [1.0, 0.0, 0.0]);
946 let v2 = cache.get_or_insert(0xAB, || [999.0, 0.0, 0.0]);
947 assert!((v1[0] - 1.0).abs() < 1e-10);
948 assert!(
949 (v2[0] - 1.0).abs() < 1e-10,
950 "should return cached value: {}",
951 v2[0]
952 );
953 assert_eq!(cache.misses, 1);
954 assert_eq!(cache.hits, 1);
955 }
956 #[test]
957 fn test_feature_cache_hit_ratio() {
958 let mut cache = ShapeFeatureCache::new();
959 cache.get_or_insert(1, || [0.0; 3]);
960 cache.get_or_insert(1, || [0.0; 3]);
961 cache.get_or_insert(1, || [0.0; 3]);
962 assert!((cache.hit_ratio() - 2.0 / 3.0).abs() < 1e-10);
963 }
964 #[test]
965 fn test_feature_cache_clear() {
966 let mut cache = ShapeFeatureCache::new();
967 cache.get_or_insert(1, || [1.0; 3]);
968 cache.clear();
969 assert_eq!(cache.hits, 0);
970 assert_eq!(cache.misses, 0);
971 cache.get_or_insert(1, || [2.0; 3]);
972 assert_eq!(cache.misses, 1);
973 }
974 #[test]
975 fn test_dispatch_stats_contact_rate() {
976 let mut stats = DispatchStats::default();
977 stats.record(true, false);
978 stats.record(false, false);
979 stats.record(true, true);
980 assert!((stats.contact_rate() - 2.0 / 3.0).abs() < 1e-10);
981 }
982 #[test]
983 fn test_dispatch_stats_specialization_rate() {
984 let mut stats = DispatchStats::default();
985 stats.record(true, false);
986 stats.record(false, false);
987 stats.record(true, true);
988 assert!((stats.specialization_rate() - 2.0 / 3.0).abs() < 1e-10);
989 }
990 #[test]
991 fn test_dispatch_stats_pruned() {
992 let mut stats = DispatchStats::default();
993 stats.record_pruned();
994 stats.record_pruned();
995 assert_eq!(stats.broad_phase_pruned, 2);
996 }
997 #[test]
998 fn test_dispatch_stats_empty_rates() {
999 let stats = DispatchStats::default();
1000 assert!((stats.contact_rate()).abs() < 1e-10);
1001 assert!((stats.specialization_rate()).abs() < 1e-10);
1002 }
1003}
1004pub(super) fn clip_polygon_by_halfspace(
1009 polygon: &[[f64; 2]],
1010 plane_point: [f64; 2],
1011 plane_normal: [f64; 2],
1012) -> Vec<[f64; 2]> {
1013 if polygon.is_empty() {
1014 return Vec::new();
1015 }
1016 let mut result = Vec::new();
1017 let n = polygon.len();
1018 for i in 0..n {
1019 let curr = polygon[i];
1020 let next = polygon[(i + 1) % n];
1021 let dc = (curr[0] - plane_point[0]) * plane_normal[0]
1022 + (curr[1] - plane_point[1]) * plane_normal[1];
1023 let dn = (next[0] - plane_point[0]) * plane_normal[0]
1024 + (next[1] - plane_point[1]) * plane_normal[1];
1025 if dc >= 0.0 {
1026 result.push(curr);
1027 }
1028 if (dc >= 0.0) != (dn >= 0.0) {
1029 let denom = dc - dn;
1030 if denom.abs() > 1e-14 {
1031 let t = dc / denom;
1032 result.push([
1033 curr[0] + t * (next[0] - curr[0]),
1034 curr[1] + t * (next[1] - curr[1]),
1035 ]);
1036 }
1037 }
1038 }
1039 result
1040}
1041pub fn clip_box_faces_2d(
1047 face_a: [[f64; 2]; 4],
1048 face_b: [[f64; 2]; 4],
1049 normal: [f64; 3],
1050 depth: f64,
1051) -> ContactPatch {
1052 let mut clipped: Vec<[f64; 2]> = face_b.to_vec();
1053 for i in 0..4 {
1054 if clipped.is_empty() {
1055 break;
1056 }
1057 let a = face_a[i];
1058 let b = face_a[(i + 1) % 4];
1059 let edge = [b[0] - a[0], b[1] - a[1]];
1060 let edge_normal = [edge[1], -edge[0]];
1061 clipped = clip_polygon_by_halfspace(&clipped, a, edge_normal);
1062 }
1063 let mut patch = ContactPatch::new(normal, depth);
1064 for p in &clipped {
1065 patch.add_point([p[0], 0.0, p[1]]);
1066 }
1067 patch
1068}
1069pub fn box_box_manifold_clip(
1074 box_a: &BoxShape,
1075 transform_a: &oxiphysics_core::Transform,
1076 box_b: &BoxShape,
1077 transform_b: &oxiphysics_core::Transform,
1078 pair: CollisionPair,
1079) -> Option<ContactManifold> {
1080 use crate::narrowphase::specialized::box_box_sat;
1081 let single_contact = box_box_sat(box_a, transform_a, box_b, transform_b)?;
1082 let mut manifold = ContactManifold::new(pair);
1083 manifold.add_contact(single_contact.clone());
1084 let n = single_contact.normal;
1085 let perp1 = if n.x.abs() < 0.9 {
1086 oxiphysics_core::math::Vec3::new(1.0, 0.0, 0.0) - n * n.x
1087 } else {
1088 oxiphysics_core::math::Vec3::new(0.0, 1.0, 0.0) - n * n.y
1089 };
1090 let perp1_len = perp1.norm();
1091 if perp1_len < 1e-10 {
1092 return Some(manifold);
1093 }
1094 let perp1 = perp1 / perp1_len;
1095 let perp2 = n.cross(&perp1);
1096 let half = [
1097 box_a.half_extents.x,
1098 box_a.half_extents.y,
1099 box_a.half_extents.z,
1100 ];
1101 let scale = *half
1102 .iter()
1103 .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
1104 .unwrap_or(&0.1);
1105 let offsets = [
1106 perp1 * scale + perp2 * scale,
1107 perp1 * scale - perp2 * scale,
1108 -perp1 * scale + perp2 * scale,
1109 -perp1 * scale - perp2 * scale,
1110 ];
1111 for off in &offsets {
1112 let shifted_ta = oxiphysics_core::Transform {
1113 position: transform_a.position + *off,
1114 rotation: transform_a.rotation,
1115 };
1116 if let Some(c) = box_box_sat(box_a, &shifted_ta, box_b, transform_b)
1117 && c.depth > 0.0
1118 {
1119 let mut adjusted = c;
1120 adjusted.point_a -= *off;
1121 adjusted.point_b -= *off;
1122 manifold.add_contact(adjusted);
1123 }
1124 }
1125 manifold.contacts.sort_by(|a, b| {
1126 b.depth
1127 .partial_cmp(&a.depth)
1128 .unwrap_or(std::cmp::Ordering::Equal)
1129 });
1130 manifold.contacts.truncate(4);
1131 Some(manifold)
1132}
1133pub fn generate_speculative_contacts(
1142 shape_a: &dyn Shape,
1143 transform_a: &oxiphysics_core::Transform,
1144 velocity_a: [f64; 3],
1145 shape_b: &dyn Shape,
1146 transform_b: &oxiphysics_core::Transform,
1147 velocity_b: [f64; 3],
1148 pair: CollisionPair,
1149 config: &SpeculativeConfig,
1150) -> Option<ContactManifold> {
1151 use crate::narrowphase::gjk::{GjkDistanceResult, gjk_distance_query};
1152 let result: GjkDistanceResult = gjk_distance_query(shape_a, transform_a, shape_b, transform_b);
1153 if result.distance > config.margin {
1154 return None;
1155 }
1156 let diff = result.closest_b - result.closest_a;
1157 let dist = diff.norm();
1158 let normal = if dist > 1e-10 {
1159 diff / dist
1160 } else {
1161 let d = transform_b.position - transform_a.position;
1162 if d.norm_squared() > 1e-10 {
1163 d.normalize()
1164 } else {
1165 oxiphysics_core::math::Vec3::new(0.0, 1.0, 0.0)
1166 }
1167 };
1168 let rel_vel = oxiphysics_core::math::Vec3::new(
1169 velocity_b[0] - velocity_a[0],
1170 velocity_b[1] - velocity_a[1],
1171 velocity_b[2] - velocity_a[2],
1172 );
1173 let closing = -rel_vel.dot(&normal);
1174 if closing < -config.velocity_scale * config.margin && result.distance > 0.0 {
1175 return None;
1176 }
1177 let depth = config.margin - result.distance;
1178 let contact = Contact {
1179 point_a: result.closest_a,
1180 point_b: result.closest_b,
1181 normal,
1182 depth,
1183 };
1184 let mut manifold = ContactManifold::new(pair);
1185 manifold.add_contact(contact);
1186 Some(manifold)
1187}
1188pub fn dispatch_concave_vs_convex(
1193 mesh: &ConcaveMesh,
1194 mesh_transform: &oxiphysics_core::Transform,
1195 _convex: &dyn Shape,
1196 convex_type: ShapeType,
1197 convex_transform: &oxiphysics_core::Transform,
1198 base_pair: CollisionPair,
1199 dispatcher: &NarrowPhaseDispatcher,
1200) -> Vec<ContactManifold> {
1201 let _ = convex_type;
1202 let _ = dispatcher;
1203 let mut manifolds = Vec::new();
1204 let inv_mesh = mesh_transform.inverse();
1205 let local_convex_pos = inv_mesh.transform_point(&convex_transform.position);
1206 let convex_aabb_expand = 2.0;
1207 for (piece_idx, piece) in mesh.pieces.iter().enumerate() {
1208 let mut rejected = false;
1209 for i in 0..3 {
1210 let lp = [local_convex_pos.x, local_convex_pos.y, local_convex_pos.z][i];
1211 if lp + convex_aabb_expand < piece.aabb_min[i]
1212 || lp - convex_aabb_expand > piece.aabb_max[i]
1213 {
1214 rejected = true;
1215 break;
1216 }
1217 }
1218 if rejected {
1219 continue;
1220 }
1221 let verts_world: Vec<oxiphysics_core::math::Vec3> = piece
1222 .vertices
1223 .iter()
1224 .map(|v| {
1225 mesh_transform.transform_point(&oxiphysics_core::math::Vec3::new(v[0], v[1], v[2]))
1226 })
1227 .collect();
1228 if verts_world.is_empty() {
1229 continue;
1230 }
1231 use crate::narrowphase::specialized::convex_convex_gjk_intersect;
1232 if convex_convex_gjk_intersect(&verts_world, mesh_transform, &verts_world, convex_transform)
1233 {
1234 let centroid: oxiphysics_core::math::Vec3 =
1235 verts_world.iter().sum::<oxiphysics_core::math::Vec3>() / verts_world.len() as f64;
1236 let diff = convex_transform.position - centroid;
1237 let dist = diff.norm();
1238 let normal = if dist > 1e-10 {
1239 diff / dist
1240 } else {
1241 oxiphysics_core::math::Vec3::new(0.0, 1.0, 0.0)
1242 };
1243 let contact = Contact {
1244 point_a: centroid,
1245 point_b: convex_transform.position,
1246 normal,
1247 depth: 0.1,
1248 };
1249 let sub_pair = CollisionPair::new(base_pair.a, base_pair.b + piece_idx * 100);
1250 let mut m = ContactManifold::new(sub_pair);
1251 m.add_contact(contact);
1252 manifolds.push(m);
1253 }
1254 }
1255 manifolds
1256}
1257pub fn dispatch_heightfield_convex_generic(
1262 hf: &SimpleHeightField,
1263 convex_pos: [f64; 3],
1264 convex_radius: f64,
1265 convex_half_height: f64,
1266 pair: CollisionPair,
1267) -> Vec<ContactManifold> {
1268 let mut manifolds = Vec::new();
1269 let search_r = (convex_radius + convex_half_height).abs();
1270 let col_min = ((convex_pos[0] - search_r) / hf.spacing).floor() as isize;
1271 let col_max = ((convex_pos[0] + search_r) / hf.spacing).ceil() as isize;
1272 let row_min = ((convex_pos[2] - search_r) / hf.spacing).floor() as isize;
1273 let row_max = ((convex_pos[2] + search_r) / hf.spacing).ceil() as isize;
1274 for row in row_min..=row_max {
1275 for col in col_min..=col_max {
1276 if row < 0 || col < 0 {
1277 continue;
1278 }
1279 let row = row as usize;
1280 let col = col as usize;
1281 if row >= hf.rows || col >= hf.cols {
1282 continue;
1283 }
1284 let sample_x = col as f64 * hf.spacing;
1285 let sample_z = row as f64 * hf.spacing;
1286 let height = hf.height_at(row, col);
1287 let dx = convex_pos[0] - sample_x;
1288 let dz = convex_pos[2] - sample_z;
1289 let horiz_dist = (dx * dx + dz * dz).sqrt();
1290 if horiz_dist < search_r {
1291 let penetration = height + convex_radius - convex_pos[1];
1292 if penetration > 0.0 {
1293 let normal = hf.normal_at(row, col);
1294 let contact = Contact {
1295 point_a: oxiphysics_core::math::Vec3::new(sample_x, height, sample_z),
1296 point_b: oxiphysics_core::math::Vec3::new(
1297 convex_pos[0],
1298 convex_pos[1] - convex_radius,
1299 convex_pos[2],
1300 ),
1301 normal: oxiphysics_core::math::Vec3::new(normal[0], normal[1], normal[2]),
1302 depth: penetration,
1303 };
1304 let cell_pair = CollisionPair::new(pair.a, pair.b + row * 1000 + col);
1305 let mut m = ContactManifold::new(cell_pair);
1306 m.add_contact(contact);
1307 manifolds.push(m);
1308 }
1309 }
1310 }
1311 }
1312 manifolds
1313}
1314pub fn merge_manifolds(
1319 manifold_a: ContactManifold,
1320 manifold_b: ContactManifold,
1321 max_contacts: usize,
1322) -> ContactManifold {
1323 let mut merged = ContactManifold::new(manifold_a.pair);
1324 for c in manifold_a.contacts {
1325 merged.add_contact(c);
1326 }
1327 for c in manifold_b.contacts {
1328 merged.add_contact(c);
1329 }
1330 merged.contacts.sort_by(|a, b| {
1331 b.depth
1332 .partial_cmp(&a.depth)
1333 .unwrap_or(std::cmp::Ordering::Equal)
1334 });
1335 merged.contacts.truncate(max_contacts);
1336 merged
1337}
1338pub fn flip_manifold(manifold: &mut ContactManifold) {
1340 for c in &mut manifold.contacts {
1341 c.normal = -c.normal;
1342 std::mem::swap(&mut c.point_a, &mut c.point_b);
1343 }
1344}
1345pub fn scale_contact_depths(manifold: &mut ContactManifold, scale: f64) {
1347 for c in &mut manifold.contacts {
1348 c.depth *= scale;
1349 }
1350}
1351pub fn prune_shallow_contacts(manifold: &mut ContactManifold, min_depth: f64) {
1353 manifold.contacts.retain(|c| c.depth >= min_depth);
1354}
1355pub fn translate_manifold(manifold: &mut ContactManifold, offset: oxiphysics_core::math::Vec3) {
1357 for c in &mut manifold.contacts {
1358 c.point_a += offset;
1359 c.point_b += offset;
1360 }
1361}
1362pub fn batch_speculative_contacts<'a>(
1366 pairs: &[SpecBatchEntry<'a>],
1367 config: &SpeculativeConfig,
1368) -> Vec<Option<ContactManifold>> {
1369 pairs
1370 .iter()
1371 .map(|(sa, ta, va, sb, tb, vb, pair)| {
1372 generate_speculative_contacts(*sa, ta, *va, *sb, tb, *vb, *pair, config)
1373 })
1374 .collect()
1375}
1376#[cfg(test)]
1377mod tests_dispatch_extended {
1378 use super::*;
1379 use crate::CollisionPair;
1380 use crate::Contact;
1381 use crate::ContactManifold;
1382 use crate::narrowphase::ContactPatch;
1383 use crate::narrowphase::ContactPatchReducer;
1384 use crate::narrowphase::ConvexPiece;
1385 use crate::narrowphase::DispatchQueue;
1386 use crate::narrowphase::clip_box_faces_2d;
1387 use crate::narrowphase::clip_polygon_by_halfspace;
1388 use crate::narrowphase::flip_manifold;
1389 use oxiphysics_core::Vec3;
1390
1391 fn merge_manifolds(a: ContactManifold, b: ContactManifold, max: usize) -> ContactManifold {
1393 let pair = a.pair;
1394 let mut merged = ContactManifold::new(pair);
1395 let mut all: Vec<Contact> = a.contacts.into_iter().chain(b.contacts).collect();
1396 all.sort_by(|x, y| {
1397 y.depth
1398 .partial_cmp(&x.depth)
1399 .unwrap_or(std::cmp::Ordering::Equal)
1400 });
1401 all.truncate(max);
1402 for c in all {
1403 merged.add_contact(c);
1404 }
1405 merged
1406 }
1407 #[test]
1408 fn test_contact_patch_empty() {
1409 let p = ContactPatch::new([0.0, 1.0, 0.0], 0.1);
1410 assert!(p.is_empty());
1411 assert_eq!(p.len(), 0);
1412 }
1413 #[test]
1414 fn test_contact_patch_add_point() {
1415 let mut p = ContactPatch::new([0.0, 1.0, 0.0], 0.1);
1416 p.add_point([1.0, 0.0, 0.0]);
1417 p.add_point([2.0, 0.0, 0.0]);
1418 assert_eq!(p.len(), 2);
1419 assert!(!p.is_empty());
1420 }
1421 #[test]
1422 fn test_clip_polygon_by_halfspace_full_inside() {
1423 let poly = vec![
1425 [0.5, 0.5, 0.0],
1426 [1.5, 0.5, 0.0],
1427 [1.5, 1.5, 0.0],
1428 [0.5, 1.5, 0.0],
1429 ];
1430 let clipped = clip_polygon_by_halfspace(&poly, [1.0, 0.0, 0.0], 0.0);
1431 assert_eq!(clipped.len(), 4, "all points inside, should keep 4");
1432 }
1433 #[test]
1434 fn test_clip_polygon_by_halfspace_full_outside() {
1435 let poly = vec![
1437 [-2.0, 0.0, 0.0],
1438 [-1.0, 0.0, 0.0],
1439 [-1.0, 1.0, 0.0],
1440 [-2.0, 1.0, 0.0],
1441 ];
1442 let clipped = clip_polygon_by_halfspace(&poly, [1.0, 0.0, 0.0], 0.0);
1443 assert!(clipped.is_empty(), "all points outside");
1444 }
1445 #[test]
1446 fn test_clip_polygon_by_halfspace_partial() {
1447 let poly = vec![
1449 [-1.0, 0.0, 0.0],
1450 [1.0, 0.0, 0.0],
1451 [1.0, 1.0, 0.0],
1452 [-1.0, 1.0, 0.0],
1453 ];
1454 let clipped = clip_polygon_by_halfspace(&poly, [1.0, 0.0, 0.0], 0.0);
1455 assert!(
1456 clipped.len() >= 2,
1457 "partial clip should keep some points: {:?}",
1458 clipped
1459 );
1460 }
1461 #[test]
1462 fn test_clip_box_faces_2d_no_overlap() {
1463 let face_a: [[f64; 2]; 4] = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1464 let face_b: [[f64; 2]; 4] = [[5.0, 5.0], [6.0, 5.0], [6.0, 6.0], [5.0, 6.0]];
1465 let patch = clip_box_faces_2d(face_a, face_b, [0.0, 1.0, 0.0], 0.1);
1466 assert!(
1467 patch.is_empty(),
1468 "non-overlapping faces should produce no contacts"
1469 );
1470 }
1471 #[test]
1472 fn test_clip_box_faces_2d_full_overlap() {
1473 let face: [[f64; 2]; 4] = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1474 let patch = clip_box_faces_2d(face, face, [0.0, 1.0, 0.0], 0.1);
1475 let _ = patch;
1476 }
1477 #[test]
1478 fn test_reducer_keeps_max_contacts() {
1479 let reducer = ContactPatchReducer::new(2);
1480 let pair = CollisionPair::new(0, 1);
1481 let mut manifold = ContactManifold::new(pair);
1482 for i in 0..6 {
1483 manifold.add_contact(Contact {
1484 point_a: Vec3::new(i as f64, 0.0, 0.0),
1485 point_b: Vec3::new(i as f64 + 0.1, 0.0, 0.0),
1486 normal: Vec3::new(0.0, 1.0, 0.0),
1487 depth: i as f64 * 0.1 + 0.01,
1488 });
1489 }
1490 reducer.reduce(&mut manifold);
1491 assert!(
1492 manifold.contacts.len() <= 2,
1493 "should keep at most 2: {}",
1494 manifold.contacts.len()
1495 );
1496 }
1497 #[test]
1498 fn test_reducer_no_reduction_needed() {
1499 let reducer = ContactPatchReducer::new(4);
1500 let pair = CollisionPair::new(0, 1);
1501 let mut manifold = ContactManifold::new(pair);
1502 for i in 0..3 {
1503 manifold.add_contact(Contact {
1504 point_a: Vec3::new(i as f64, 0.0, 0.0),
1505 point_b: Vec3::new(i as f64 + 0.1, 0.0, 0.0),
1506 normal: Vec3::new(0.0, 1.0, 0.0),
1507 depth: 0.05,
1508 });
1509 }
1510 reducer.reduce(&mut manifold);
1511 assert_eq!(
1512 manifold.contacts.len(),
1513 3,
1514 "should keep all 3 when under max"
1515 );
1516 }
1517 #[test]
1518 fn test_box_box_manifold_clip_overlap() {
1519 use oxiphysics_geometry::BoxShape;
1520 let b1 = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
1521 let b2 = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
1522 let t1 = oxiphysics_core::Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
1523 let t2 = oxiphysics_core::Transform::from_position(Vec3::new(1.5, 0.0, 0.0));
1524 let pair = CollisionPair::new(0, 1);
1525 let result = box_box_manifold_clip(&b1, &t1, &b2, &t2, pair);
1526 assert!(
1527 result.is_some(),
1528 "overlapping boxes should produce manifold"
1529 );
1530 let m = result.unwrap();
1531 assert!(!m.contacts.is_empty());
1532 }
1533 #[test]
1534 fn test_box_box_manifold_clip_separated() {
1535 let b1 = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
1536 let b2 = BoxShape::new(Vec3::new(1.0, 1.0, 1.0));
1537 let t1 = oxiphysics_core::Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
1538 let t2 = oxiphysics_core::Transform::from_position(Vec3::new(10.0, 0.0, 0.0));
1539 let pair = CollisionPair::new(0, 1);
1540 let result = box_box_manifold_clip(&b1, &t1, &b2, &t2, pair);
1541 assert!(
1542 result.is_none(),
1543 "separated boxes should produce no manifold"
1544 );
1545 }
1546 #[test]
1547 fn test_speculative_contacts_within_margin() {
1548 let s1 = Sphere::new(1.0);
1549 let s2 = Sphere::new(1.0);
1550 let t1 = oxiphysics_core::Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
1551 let t2 = oxiphysics_core::Transform::from_position(Vec3::new(2.1, 0.0, 0.0));
1552 let config = SpeculativeConfig {
1553 margin: 0.5,
1554 velocity_scale: 10.0,
1555 };
1556 let pair = CollisionPair::new(0, 1);
1557 let result = generate_speculative_contacts(
1558 &s1,
1559 &t1,
1560 [1.0, 0.0, 0.0],
1561 &s2,
1562 &t2,
1563 [-1.0, 0.0, 0.0],
1564 pair,
1565 &config,
1566 );
1567 assert!(
1568 result.is_some(),
1569 "shapes within margin should produce speculative contact"
1570 );
1571 }
1572 #[test]
1573 fn test_speculative_contacts_beyond_margin() {
1574 let s1 = Sphere::new(0.5);
1575 let s2 = Sphere::new(0.5);
1576 let t1 = oxiphysics_core::Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
1577 let t2 = oxiphysics_core::Transform::from_position(Vec3::new(20.0, 0.0, 0.0));
1578 let config = SpeculativeConfig::default();
1579 let pair = CollisionPair::new(0, 1);
1580 let result =
1581 generate_speculative_contacts(&s1, &t1, [0.0; 3], &s2, &t2, [0.0; 3], pair, &config);
1582 assert!(
1583 result.is_none(),
1584 "far separated shapes should not produce speculative contact"
1585 );
1586 }
1587 #[test]
1588 fn test_speculative_config_default() {
1589 let c = SpeculativeConfig::default();
1590 assert!(c.margin > 0.0);
1591 assert!(c.velocity_scale > 0.0);
1592 }
1593 #[test]
1594 fn test_dispatch_queue_push_pop() {
1595 let mut q = DispatchQueue::new();
1596 q.push(CollisionPair::new(0, 1), 0.5);
1597 q.push(CollisionPair::new(1, 2), 1.5);
1598 q.push(CollisionPair::new(2, 3), 0.1);
1599 let top = q.pop().unwrap();
1600 assert!(
1601 (top.priority - 1.5).abs() < 1e-10,
1602 "highest priority first: {}",
1603 top.priority
1604 );
1605 }
1606 #[test]
1607 fn test_dispatch_queue_empty() {
1608 let mut q = DispatchQueue::new();
1609 assert!(q.is_empty());
1610 assert_eq!(q.len(), 0);
1611 assert!(q.pop().is_none());
1612 }
1613 #[test]
1614 fn test_dispatch_queue_clear() {
1615 let mut q = DispatchQueue::new();
1616 q.push(CollisionPair::new(0, 1), 1.0);
1617 q.push(CollisionPair::new(1, 2), 2.0);
1618 q.clear();
1619 assert!(q.is_empty());
1620 }
1621 #[test]
1622 fn test_merge_manifolds() {
1623 let pair = CollisionPair::new(0, 1);
1624 let mut m1 = ContactManifold::new(pair);
1625 m1.add_contact(Contact {
1626 point_a: Vec3::new(0.0, 0.0, 0.0),
1627 point_b: Vec3::new(0.1, 0.0, 0.0),
1628 normal: Vec3::new(1.0, 0.0, 0.0),
1629 depth: 0.5,
1630 });
1631 let mut m2 = ContactManifold::new(pair);
1632 m2.add_contact(Contact {
1633 point_a: Vec3::new(1.0, 0.0, 0.0),
1634 point_b: Vec3::new(1.1, 0.0, 0.0),
1635 normal: Vec3::new(1.0, 0.0, 0.0),
1636 depth: 0.1,
1637 });
1638 let merged = merge_manifolds(m1, m2, 4);
1639 assert_eq!(merged.contacts.len(), 2);
1640 assert!((merged.contacts[0].depth - 0.5).abs() < 1e-10);
1641 }
1642 #[test]
1643 fn test_merge_manifolds_limits_to_max() {
1644 let pair = CollisionPair::new(0, 1);
1645 let mut m1 = ContactManifold::new(pair);
1646 let mut m2 = ContactManifold::new(pair);
1647 for i in 0..3 {
1648 m1.add_contact(Contact {
1649 point_a: Vec3::new(i as f64, 0.0, 0.0),
1650 point_b: Vec3::new(i as f64 + 0.1, 0.0, 0.0),
1651 normal: Vec3::new(1.0, 0.0, 0.0),
1652 depth: 0.01 * i as f64 + 0.01,
1653 });
1654 m2.add_contact(Contact {
1655 point_a: Vec3::new(i as f64 + 10.0, 0.0, 0.0),
1656 point_b: Vec3::new(i as f64 + 10.1, 0.0, 0.0),
1657 normal: Vec3::new(1.0, 0.0, 0.0),
1658 depth: 0.01 * i as f64 + 0.01,
1659 });
1660 }
1661 let merged = merge_manifolds(m1, m2, 4);
1662 assert!(
1663 merged.contacts.len() <= 4,
1664 "should not exceed max_contacts=4: {}",
1665 merged.contacts.len()
1666 );
1667 }
1668 #[test]
1669 fn test_flip_manifold() {
1670 let pair = CollisionPair::new(0, 1);
1671 let mut m = ContactManifold::new(pair);
1672 m.add_contact(Contact {
1673 point_a: Vec3::new(0.0, 0.0, 0.0),
1674 point_b: Vec3::new(1.0, 0.0, 0.0),
1675 normal: Vec3::new(1.0, 0.0, 0.0),
1676 depth: 0.1,
1677 });
1678 flip_manifold(&mut m);
1679 assert!(
1680 (m.contacts[0].normal.x - (-1.0)).abs() < 1e-10,
1681 "normal should be flipped"
1682 );
1683 assert!(
1684 (m.contacts[0].point_a.x - 1.0).abs() < 1e-10,
1685 "point_a and point_b should be swapped"
1686 );
1687 }
1688 #[test]
1689 fn test_scale_contact_depths() {
1690 let pair = CollisionPair::new(0, 1);
1691 let mut m = ContactManifold::new(pair);
1692 m.add_contact(Contact {
1693 point_a: Vec3::zeros(),
1694 point_b: Vec3::zeros(),
1695 normal: Vec3::new(0.0, 1.0, 0.0),
1696 depth: 1.0,
1697 });
1698 scale_contact_depths(&mut m, 2.0);
1699 assert!((m.contacts[0].depth - 2.0).abs() < 1e-10);
1700 }
1701 #[test]
1702 fn test_prune_shallow_contacts() {
1703 let pair = CollisionPair::new(0, 1);
1704 let mut m = ContactManifold::new(pair);
1705 m.add_contact(Contact {
1706 point_a: Vec3::zeros(),
1707 point_b: Vec3::zeros(),
1708 normal: Vec3::new(0.0, 1.0, 0.0),
1709 depth: 0.001,
1710 });
1711 m.add_contact(Contact {
1712 point_a: Vec3::zeros(),
1713 point_b: Vec3::zeros(),
1714 normal: Vec3::new(0.0, 1.0, 0.0),
1715 depth: 0.1,
1716 });
1717 prune_shallow_contacts(&mut m, 0.01);
1718 assert_eq!(m.contacts.len(), 1, "only the deep contact should remain");
1719 assert!((m.contacts[0].depth - 0.1).abs() < 1e-10);
1720 }
1721 #[test]
1722 fn test_translate_manifold() {
1723 let pair = CollisionPair::new(0, 1);
1724 let mut m = ContactManifold::new(pair);
1725 m.add_contact(Contact {
1726 point_a: Vec3::new(1.0, 0.0, 0.0),
1727 point_b: Vec3::new(2.0, 0.0, 0.0),
1728 normal: Vec3::new(1.0, 0.0, 0.0),
1729 depth: 0.1,
1730 });
1731 let offset = Vec3::new(5.0, 0.0, 0.0);
1732 translate_manifold(&mut m, offset);
1733 assert!((m.contacts[0].point_a.x - 6.0).abs() < 1e-10);
1734 assert!((m.contacts[0].point_b.x - 7.0).abs() < 1e-10);
1735 }
1736 #[test]
1737 fn test_heightfield_convex_generic_flat() {
1738 let hf = SimpleHeightField::new(vec![0.0; 9], 3, 3, 1.0);
1739 let pair = CollisionPair::new(0, 1);
1740 let manifolds = dispatch_heightfield_convex_generic(&hf, [1.0, 0.5, 1.0], 1.0, 0.0, pair);
1741 assert!(
1742 !manifolds.is_empty(),
1743 "should produce contacts for penetrating sphere"
1744 );
1745 }
1746 #[test]
1747 fn test_heightfield_convex_generic_no_contact() {
1748 let hf = SimpleHeightField::new(vec![0.0; 9], 3, 3, 1.0);
1749 let pair = CollisionPair::new(0, 1);
1750 let manifolds = dispatch_heightfield_convex_generic(&hf, [1.0, 10.0, 1.0], 0.1, 0.0, pair);
1751 assert!(
1752 manifolds.is_empty(),
1753 "sphere far above heightfield should have no contacts"
1754 );
1755 }
1756 #[test]
1757 fn test_dispatch_concave_empty_mesh() {
1758 let mesh = ConcaveMesh::new(vec![]);
1759 let s = Sphere::new(1.0);
1760 let tm = oxiphysics_core::Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
1761 let ts = oxiphysics_core::Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
1762 let pair = CollisionPair::new(0, 1);
1763 let dispatcher = NarrowPhaseDispatcher::default();
1764 let manifolds =
1765 dispatch_concave_vs_convex(&mesh, &tm, &s, ShapeType::Sphere, &ts, pair, &dispatcher);
1766 assert!(
1767 manifolds.is_empty(),
1768 "empty mesh should produce no contacts"
1769 );
1770 }
1771 #[test]
1772 fn test_convex_piece_aabb_overlap() {
1773 let pa = ConvexPiece::new(vec![
1774 [0.0, 0.0, 0.0],
1775 [1.0, 0.0, 0.0],
1776 [1.0, 1.0, 0.0],
1777 [0.0, 1.0, 0.0],
1778 ]);
1779 let pb = ConvexPiece::new(vec![
1780 [0.5, 0.5, 0.0],
1781 [1.5, 0.5, 0.0],
1782 [1.5, 1.5, 0.0],
1783 [0.5, 1.5, 0.0],
1784 ]);
1785 assert!(
1786 pa.aabb_overlaps(&pb, 0.0),
1787 "overlapping pieces should overlap"
1788 );
1789 }
1790 #[test]
1791 fn test_convex_piece_aabb_no_overlap() {
1792 let pa = ConvexPiece::new(vec![[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]]);
1793 let pb = ConvexPiece::new(vec![[5.0, 5.0, 5.0], [6.0, 6.0, 6.0]]);
1794 assert!(
1795 !pa.aabb_overlaps(&pb, 0.0),
1796 "separated pieces should not overlap"
1797 );
1798 }
1799 #[test]
1800 fn test_batch_speculative_empty() {
1801 let config = SpeculativeConfig::default();
1802 let results = batch_speculative_contacts(&[], &config);
1803 assert!(results.is_empty());
1804 }
1805 #[test]
1806 fn test_batch_speculative_single() {
1807 let s1 = Sphere::new(1.0);
1808 let s2 = Sphere::new(1.0);
1809 let t1 = oxiphysics_core::Transform::from_position(Vec3::new(0.0, 0.0, 0.0));
1810 let t2 = oxiphysics_core::Transform::from_position(Vec3::new(2.1, 0.0, 0.0));
1811 let config = SpeculativeConfig {
1812 margin: 0.5,
1813 velocity_scale: 10.0,
1814 };
1815 let pair = CollisionPair::new(0, 1);
1816 let pairs: Vec<_> = vec![(
1817 &s1 as &dyn Shape,
1818 &t1,
1819 [0.5, 0.0, 0.0_f64],
1820 &s2 as &dyn Shape,
1821 &t2,
1822 [-0.5, 0.0, 0.0_f64],
1823 pair,
1824 )];
1825 let results = batch_speculative_contacts(&pairs, &config);
1826 assert_eq!(results.len(), 1);
1827 }
1828}