1use crate::SupportMapping;
35use hoomd_vector::{Cartesian, Cross, InnerProduct, Rotate, RotationMatrix};
36
37const XENOCOLLIDE_MAX_ITER: usize = 1024;
39
40enum Discovery<const N: usize> {
42 Known(bool),
44 Found([Cartesian<N>; N]),
46}
47
48trait MinkowskiPortalRefinement<const N: usize> {
55 const TOLERANCE: f64;
57
58 fn outward_normal(portal: &[Cartesian<N>; N], interior: &Cartesian<N>) -> Cartesian<N>;
60
61 fn discover_portal<A, B>(s: &MinkowskiDifference<N, A, B>, v0: &Cartesian<N>) -> Discovery<N>
66 where
67 A: SupportMapping<Cartesian<N>>,
68 B: SupportMapping<Cartesian<N>>;
69
70 fn tolerance_check(
76 portal: &[Cartesian<N>; N],
77 v_new: &Cartesian<N>,
78 normal: &Cartesian<N>,
79 ) -> Option<bool>;
80
81 fn narrow_portal(interior: &Cartesian<N>, portal: &mut [Cartesian<N>; N], v_new: Cartesian<N>);
89}
90
91impl MinkowskiPortalRefinement<2> for Cartesian<2> {
92 const TOLERANCE: f64 = 1e-16;
93
94 #[inline]
95 fn outward_normal(portal: &[Cartesian<2>; 2], interior: &Cartesian<2>) -> Cartesian<2> {
96 let mut n = (portal[1] - portal[0]).perpendicular();
97 if (portal[0] - *interior).dot(&n) < 0.0 {
98 n = -n;
99 }
100 n
101 }
102
103 #[inline]
104 fn discover_portal<A: SupportMapping<Cartesian<2>>, B: SupportMapping<Cartesian<2>>>(
105 s: &MinkowskiDifference<2, A, B>,
106 v0: &Cartesian<2>,
107 ) -> Discovery<2> {
108 let v1 = s.composite_support_mapping(-*v0);
110
111 let mut v_perp_v1v0 = (v1 - *v0).perpendicular();
113 if v1.dot(&v_perp_v1v0) > 0.0 {
114 v_perp_v1v0 = -v_perp_v1v0;
115 }
116
117 let v2 = s.composite_support_mapping(v_perp_v1v0);
119
120 Discovery::Found([v1, v2])
124 }
125
126 #[inline]
127 fn tolerance_check(
128 portal: &[Cartesian<2>; 2],
129 v_new: &Cartesian<2>,
130 _normal: &Cartesian<2>,
131 ) -> Option<bool> {
132 let d = (*v_new - portal[0]) - (*v_new - portal[0]).project(&(portal[1] - portal[0]));
134 if d.norm_squared() < Self::TOLERANCE * v_new.norm_squared() {
135 return Some(true);
136 }
137 None
138 }
139
140 #[inline]
141 fn narrow_portal(interior: &Cartesian<2>, portal: &mut [Cartesian<2>; 2], v_new: Cartesian<2>) {
142 let mut v_perp = (v_new - *interior).perpendicular();
143 if (portal[0] - v_new).dot(&v_perp) < 0.0 {
145 v_perp = -v_perp;
146 }
147 if v_new.dot(&v_perp) < 0.0 {
148 portal[1] = v_new;
150 } else {
151 portal[0] = v_new;
153 }
154 }
155}
156
157impl MinkowskiPortalRefinement<3> for Cartesian<3> {
158 const TOLERANCE: f64 = 2e-12;
159
160 #[inline]
161 fn outward_normal(portal: &[Cartesian<3>; 3], interior: &Cartesian<3>) -> Cartesian<3> {
162 let e1 = portal[1] - portal[0];
163 let e2 = portal[2] - portal[0];
164 let mut n = e1.cross(&e2);
165 if (portal[0] - *interior).dot(&n) < 0.0 {
166 n = -n;
167 }
168 n
169 }
170
171 #[inline]
172 fn discover_portal<A: SupportMapping<Cartesian<3>>, B: SupportMapping<Cartesian<3>>>(
173 s: &MinkowskiDifference<3, A, B>,
174 v0: &Cartesian<3>,
175 ) -> Discovery<3> {
176 if v0.into_iter().all(|x| x.abs() < Self::TOLERANCE) {
178 return Discovery::Known(true);
179 }
180 let mut v1 = s.composite_support_mapping(-*v0);
182
183 if v1.dot(v0) > 0.0 {
185 return Discovery::Known(false);
186 }
187
188 let n = v1.cross(v0);
190
191 if n.into_iter().all(|x| x.abs() < Self::TOLERANCE) {
195 return Discovery::Known(true);
196 }
197
198 let mut v2 = s.composite_support_mapping(n);
200
201 if v2.dot(&n) < 0.0 {
202 return Discovery::Known(false);
203 }
204
205 let mut n = (v1 - *v0).cross(&(v2 - *v0));
207 if n.dot(v0) > 0.0 {
209 (v1, v2) = (v2, v1);
210 n = -n;
211 }
212
213 let mut count = 0_usize;
215 let v3 = loop {
216 count += 1;
217
218 if count >= XENOCOLLIDE_MAX_ITER {
219 return Discovery::Known(true);
220 }
221
222 let v3 = s.composite_support_mapping(n);
223 if v3.dot(&n) <= 0.0 {
224 return Discovery::Known(false);
225 }
226
227 if v1.cross(&v3).dot(v0) < 0.0 {
231 v2 = v3; n = (v1 - *v0).cross(&(v2 - *v0));
233 continue;
234 }
235 if v3.cross(&v2).dot(v0) < 0.0 {
236 v1 = v3; n = (v1 - *v0).cross(&(v2 - *v0));
238 continue;
239 }
240 break v3;
241 };
242
243 Discovery::Found([v1, v2, v3])
244 }
245
246 #[inline]
247 fn tolerance_check(
248 portal: &[Cartesian<3>; 3],
249 v_new: &Cartesian<3>,
250 normal: &Cartesian<3>,
251 ) -> Option<bool> {
252 let tolerance = Self::TOLERANCE * normal.norm(); let d = (*v_new - portal[0]).dot(normal);
256 if d.abs() < tolerance {
257 return Some(false);
258 }
259 let d = portal[0].dot(normal);
261 if d.abs() < tolerance {
262 return Some(true);
263 }
264 None
265 }
266
267 #[inline]
268 fn narrow_portal(interior: &Cartesian<3>, portal: &mut [Cartesian<3>; 3], v_new: Cartesian<3>) {
269 let [v1, v2, v3] = *portal;
270 let v_perp = v_new.cross(interior);
276
277 #[expect(
278 clippy::match_same_arms,
279 reason = "Clearly illustrate translation from c."
280 )]
281 match (
282 v_perp.dot(&v1) > 0.0,
283 v_perp.dot(&v2) > 0.0,
284 v_perp.dot(&v3) > 0.0,
285 ) {
286 (true, true, _) => portal[0] = v_new, (true, false, _) => portal[2] = v_new, (false, _, true) => portal[1] = v_new, (false, _, false) => portal[0] = v_new, }
291 }
292}
293
294pub struct MinkowskiDifference<
296 'a,
297 const N: usize,
298 A: SupportMapping<Cartesian<N>>,
299 B: SupportMapping<Cartesian<N>>,
300> {
301 sa: &'a A,
303 sb: &'a B,
305 v_ij: &'a Cartesian<N>,
307 q_ij: RotationMatrix<N>,
309 q_ij_inv: RotationMatrix<N>,
311}
312
313impl<'a, const N: usize, A: SupportMapping<Cartesian<N>>, B: SupportMapping<Cartesian<N>>>
314 MinkowskiDifference<'_, N, A, B>
315{
316 #[inline]
318 fn composite_support_mapping(&self, n: Cartesian<N>) -> Cartesian<N> {
319 let sb_n = self
324 .q_ij
325 .rotate(&self.sb.support_mapping(&self.q_ij_inv.rotate(&n)))
326 + *self.v_ij;
327 sb_n - self.sa.support_mapping(&-n) }
329
330 #[inline]
332 fn new<R>(
333 sa: &'a A,
334 sb: &'a B,
335 v_ij: &'a Cartesian<N>,
336 r: R,
337 ) -> MinkowskiDifference<'a, N, A, B>
338 where
339 R: Copy,
340 RotationMatrix<N>: From<R>,
341 {
342 let q_ij = RotationMatrix::<N>::from(r);
343 let q_ij_inv = q_ij.inverted();
344 MinkowskiDifference {
345 sa,
346 sb,
347 v_ij,
348 q_ij,
349 q_ij_inv,
350 }
351 }
352}
353
354#[inline]
361fn collide<const N: usize, R, A, B>(sa: &A, sb: &B, v_ij: &Cartesian<N>, q_ij: &R) -> bool
362where
363 A: SupportMapping<Cartesian<N>>,
364 B: SupportMapping<Cartesian<N>>,
365 R: Copy,
366 RotationMatrix<N>: From<R>,
367 Cartesian<N>: MinkowskiPortalRefinement<N>,
368{
369 let s = MinkowskiDifference::new(sa, sb, v_ij, *q_ij);
370 let v0 = *v_ij;
371
372 let mut portal = match Cartesian::<N>::discover_portal(&s, &v0) {
374 Discovery::Found(p) => p,
375 Discovery::Known(r) => return r,
376 };
377
378 let mut count = 0_usize;
384 loop {
385 count += 1;
386
387 let normal = Cartesian::<N>::outward_normal(&portal, &v0);
388
389 if portal[0].dot(&normal) >= 0.0 {
391 return true;
392 }
393
394 let v_new = s.composite_support_mapping(normal);
396
397 if v_new.dot(&normal) < 0.0 {
399 return false;
400 }
401
402 if let Some(result) = Cartesian::<N>::tolerance_check(&portal, &v_new, &normal) {
404 return result;
405 }
406
407 Cartesian::<N>::narrow_portal(&v0, &mut portal, v_new);
409
410 if count >= XENOCOLLIDE_MAX_ITER {
411 return true;
412 }
413 }
414}
415
416#[inline(never)]
418pub fn collide2d<R: Copy, A: SupportMapping<Cartesian<2>>, B: SupportMapping<Cartesian<2>>>(
419 sa: &A,
420 sb: &B,
421 v_ij: &Cartesian<2>,
422 q_ij: &R,
423) -> bool
424where
425 RotationMatrix<2>: From<R>,
426{
427 collide::<2, R, A, B>(sa, sb, v_ij, q_ij)
428}
429
430#[inline(never)]
432pub fn collide3d<R, A, B>(sa: &A, sb: &B, v_ij: &Cartesian<3>, q_ij: &R) -> bool
433where
434 A: SupportMapping<Cartesian<3>>,
435 B: SupportMapping<Cartesian<3>>,
436 R: Copy,
437 RotationMatrix<3>: From<R>,
438{
439 collide::<3, R, A, B>(sa, sb, v_ij, q_ij)
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445 use crate::IntersectsAt;
446 use rstest::*;
447
448 use crate::shape::{Circle, Hypercuboid, Hypersphere};
449 use hoomd_utility::valid::PositiveReal;
450 use hoomd_vector::{Angle, Rotation, Versor};
451
452 #[rstest(
453 v => [[0.1, 0.1], [999.9, 0.0], [0.0, 5.123_f64.next_down()], [0.0, 5.123_000_001]],
454 radius => [0.001, 1.0, 4.123, 99.05],
455 o_ij => [
456 Angle::default(),
457 Angle::from(std::f64::consts::PI / 3.0),
458 Angle::from(1.234)
459 ],
460 )]
461 fn test_discs_collide(v: [f64; 2], radius: f64, o_ij: Angle) {
462 let (s0, s1) = (
463 Hypersphere {
464 radius: 1.0.try_into().expect("test value is a positive real"),
465 },
466 Circle {
467 radius: radius.try_into().expect("test value is a positive real"),
468 },
469 );
470
471 let overlaps = collide2d(&s0, &s1, &v.into(), &o_ij);
472
473 assert_eq!(overlaps, s0.intersects_at(&s1, &Cartesian::from(v), &o_ij));
474 }
475 #[rstest(
476 v => [[0.1, 0.1, 0.1], [999.9, 0.0, -10.9], [0.0, 5.123, 0.0], [0.0, 0.0, 5.123_000_001]],
477 radius => [0.001, 1.0, 4.123, 99.05],
478 o_ij => [
479 Versor::default(),
480 Versor::from_axis_angle(
481 [1.0, 0.0, 0.0].try_into().unwrap(), std::f64::consts::FRAC_PI_2
482 ),
483 Versor::from_axis_angle([0.0, 1.0, 0.0].try_into().unwrap(), 0.1234)
484 ]
485 )]
486 fn test_spheres_collide(v: [f64; 3], radius: f64, o_ij: Versor) {
487 let (s0, s1) = (
488 Hypersphere {
489 radius: 1.0.try_into().expect("test value is a positive real"),
490 },
491 Hypersphere::<3> {
492 radius: radius.try_into().expect("test value is a positive real"),
493 },
494 );
495 let overlaps = collide3d(&s0, &s1, &v.into(), &o_ij);
496
497 assert_eq!(
498 overlaps,
499 s0.intersects_at(&s1, &Cartesian::from(v), &o_ij),
500 "Xenocollide result did not match standard implementation!"
501 );
502 }
503
504 #[rstest(
505 v => [[0.1, 0.1], [999.9, 0.0], [0.0, 5.123], [0.0, 5.123_000_000_000_001]],
506 rect => [[1.0.try_into().expect("test value is a positive real"), 1.0.try_into().expect("test value is a positive real")], [999.0.try_into().expect("test value is a positive real"), 0.1.try_into().expect("test value is a positive real")], [1.0.try_into().expect("test value is a positive real"), (2.0*4.623).try_into().expect("test value is a positive real")]]
507 )]
508 fn test_aabrs_collide(v: [f64; 2], rect: [PositiveReal; 2]) {
509 let c0 = Hypercuboid { edge_lengths: rect };
510 let c1 = Hypercuboid {
511 edge_lengths: [1.0.try_into().expect("test value is a positive real"); 2],
512 };
513 let theta = Angle::from(0.0);
514
515 let overlaps = collide2d(&c0, &c1, &v.into(), &theta);
516 assert_eq!(overlaps, c0.intersects_aligned(&c1, &v.into()));
517 }
518 #[rstest(
519 v => [[0.1, 2.1, 0.1], [999.9, 0.0, 0.05], [0.0, 5.123, 0.0], [0.0, 5.123_000_000_001, 0.0]],
520 aabb => [[1.0.try_into().expect("test value is a positive real"), 1.0.try_into().expect("test value is a positive real"), 1.0.try_into().expect("test value is a positive real")], [999.0.try_into().expect("test value is a positive real"), 0.1.try_into().expect("test value is a positive real"), 0.5.try_into().expect("test value is a positive real")], [1.0.try_into().expect("test value is a positive real"), (2.0*4.623).try_into().expect("test value is a positive real"), 5.0.try_into().expect("test value is a positive real")]]
521
522 )]
523 fn test_aabbs_collide(v: [f64; 3], aabb: [PositiveReal; 3]) {
524 let c0 = Hypercuboid { edge_lengths: aabb };
525 let c1 = Hypercuboid {
526 edge_lengths: [1.0.try_into().expect("test value is a positive real"); 3],
527 };
528 let theta = Versor::identity();
529
530 let overlaps = collide3d(&c0, &c1, &v.into(), &theta);
531 assert_eq!(overlaps, c0.intersects_aligned(&c1, &v.into()));
532 }
533}