#[cfg(feature = "std")]
use crate::collider::Contact;
use crate::math::{Fix128, QuatFix, Vec3Fix};
#[cfg(not(feature = "std"))]
use alloc::boxed::Box;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct SdfContact {
pub body_index: usize,
pub collider_index: usize,
pub point: Vec3Fix,
pub normal: Vec3Fix,
pub depth: Fix128,
pub approach_speed: Fix128,
pub substep: usize,
pub collider_position: Vec3Fix,
pub collider_rotation: QuatFix,
pub collider_scale: Fix128,
}
pub trait SdfField: Send + Sync {
fn distance(&self, x: f32, y: f32, z: f32) -> f32;
fn normal(&self, x: f32, y: f32, z: f32) -> (f32, f32, f32);
fn distance_and_normal(&self, x: f32, y: f32, z: f32) -> (f32, (f32, f32, f32)) {
(self.distance(x, y, z), self.normal(x, y, z))
}
fn generation(&self) -> u64 {
0
}
}
type NormalFn = Box<dyn Fn(f32, f32, f32) -> (f32, f32, f32) + Send + Sync>;
pub struct ClosureSdf {
eval_fn: Box<dyn Fn(f32, f32, f32) -> f32 + Send + Sync>,
normal_fn: NormalFn,
}
impl ClosureSdf {
pub fn new(
eval_fn: impl Fn(f32, f32, f32) -> f32 + Send + Sync + 'static,
normal_fn: impl Fn(f32, f32, f32) -> (f32, f32, f32) + Send + Sync + 'static,
) -> Self {
Self {
eval_fn: Box::new(eval_fn),
normal_fn: Box::new(normal_fn),
}
}
}
impl SdfField for ClosureSdf {
#[inline]
fn distance(&self, x: f32, y: f32, z: f32) -> f32 {
(self.eval_fn)(x, y, z)
}
#[inline]
fn normal(&self, x: f32, y: f32, z: f32) -> (f32, f32, f32) {
(self.normal_fn)(x, y, z)
}
}
pub trait SdfQuery {
fn query_distance(&self, x: f32, y: f32, z: f32) -> f32;
fn query_normal(&self, x: f32, y: f32, z: f32) -> (f32, f32, f32);
}
impl<T: SdfField + ?Sized> SdfQuery for T {
#[inline]
fn query_distance(&self, x: f32, y: f32, z: f32) -> f32 {
self.distance(x, y, z)
}
#[inline]
fn query_normal(&self, x: f32, y: f32, z: f32) -> (f32, f32, f32) {
self.normal(x, y, z)
}
}
#[derive(Clone, Copy, Debug)]
pub struct ClosureSdfQuery<D, N> {
distance_fn: D,
normal_fn: N,
}
impl<D, N> ClosureSdfQuery<D, N>
where
D: Fn(f32, f32, f32) -> f32,
N: Fn(f32, f32, f32) -> (f32, f32, f32),
{
#[must_use]
pub fn new(distance_fn: D, normal_fn: N) -> Self {
Self {
distance_fn,
normal_fn,
}
}
}
impl<D, N> SdfQuery for ClosureSdfQuery<D, N>
where
D: Fn(f32, f32, f32) -> f32,
N: Fn(f32, f32, f32) -> (f32, f32, f32),
{
#[inline]
fn query_distance(&self, x: f32, y: f32, z: f32) -> f32 {
(self.distance_fn)(x, y, z)
}
#[inline]
fn query_normal(&self, x: f32, y: f32, z: f32) -> (f32, f32, f32) {
(self.normal_fn)(x, y, z)
}
}
#[cfg(feature = "std")]
#[derive(Clone, Copy, Debug)]
pub struct DistanceSdfQuery<D> {
distance_fn: D,
}
#[cfg(feature = "std")]
impl<D> DistanceSdfQuery<D>
where
D: Fn(f32, f32, f32) -> f32,
{
#[must_use]
pub fn new(distance_fn: D) -> Self {
Self { distance_fn }
}
}
#[cfg(feature = "std")]
impl<D> SdfQuery for DistanceSdfQuery<D>
where
D: Fn(f32, f32, f32) -> f32,
{
#[inline]
fn query_distance(&self, x: f32, y: f32, z: f32) -> f32 {
(self.distance_fn)(x, y, z)
}
#[inline]
fn query_normal(&self, x: f32, y: f32, z: f32) -> (f32, f32, f32) {
fd_normal(&self.distance_fn, FD_NORMAL_BASE_EPS, x, y, z)
}
}
#[derive(Debug, Clone)]
pub struct SdfUnion<A, B> {
a: A,
b: B,
}
impl<A: SdfField, B: SdfField> SdfUnion<A, B> {
#[must_use]
pub fn new(a: A, b: B) -> Self {
Self { a, b }
}
}
impl<A: SdfField, B: SdfField> SdfField for SdfUnion<A, B> {
#[inline]
fn distance(&self, x: f32, y: f32, z: f32) -> f32 {
crate::det_math::min(self.a.distance(x, y, z), self.b.distance(x, y, z))
}
fn normal(&self, x: f32, y: f32, z: f32) -> (f32, f32, f32) {
if self.a.distance(x, y, z) <= self.b.distance(x, y, z) {
self.a.normal(x, y, z)
} else {
self.b.normal(x, y, z)
}
}
fn generation(&self) -> u64 {
self.a.generation().wrapping_add(self.b.generation())
}
}
#[cfg(feature = "std")]
#[inline]
#[must_use]
pub(crate) fn fd_normal_step(base_eps: f32, x: f32, y: f32, z: f32) -> f32 {
let scale = crate::det_math::max(crate::det_math::max(x.abs(), y.abs()), z.abs());
crate::det_math::max(base_eps, 1.0e-4 * scale)
}
pub const FD_NORMAL_BASE_EPS: f32 = 1.0e-3;
#[cfg(feature = "std")]
#[inline]
#[must_use]
pub(crate) fn fd_normal<F: Fn(f32, f32, f32) -> f32>(
f: F,
base_eps: f32,
x: f32,
y: f32,
z: f32,
) -> (f32, f32, f32) {
let e = fd_normal_step(base_eps, x, y, z);
let dx = f(x + e, y, z) - f(x - e, y, z);
let dy = f(x, y + e, z) - f(x, y - e, z);
let dz = f(x, y, z + e) - f(x, y, z - e);
let len = dz.mul_add(dz, dx.mul_add(dx, dy * dy)).sqrt();
if len < 1e-10 {
(0.0, 1.0, 0.0)
} else {
(dx / len, dy / len, dz / len)
}
}
pub struct SdfCollider {
pub field: Box<dyn SdfField>,
pub position: Vec3Fix,
pub rotation: QuatFix,
pub scale: Fix128,
pub body_index: usize,
pub(crate) inv_rotation: QuatFix,
pub(crate) scale_f32: f32,
pub(crate) inv_scale_f32: f32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SdfFrame {
position: Vec3Fix,
rotation: QuatFix,
inv_rotation: QuatFix,
scale_f32: f32,
inv_scale_f32: f32,
}
impl SdfFrame {
pub const IDENTITY: Self = Self {
position: Vec3Fix::ZERO,
rotation: QuatFix::IDENTITY,
inv_rotation: QuatFix::IDENTITY,
scale_f32: 1.0,
inv_scale_f32: 1.0,
};
#[must_use]
pub fn new(position: Vec3Fix, rotation: QuatFix, scale: Fix128) -> Self {
let rotation = rotation.unit_rotation();
let s = scale.to_f32();
Self {
position,
rotation,
inv_rotation: rotation.conjugate(),
scale_f32: s,
inv_scale_f32: if s.abs() < 1e-10 { 1.0 } else { 1.0 / s },
}
}
#[must_use]
pub fn scale_f32(&self) -> f32 {
self.scale_f32
}
#[cfg(feature = "std")]
#[inline]
pub(crate) fn world_to_local(&self, world_point: Vec3Fix) -> (f32, f32, f32) {
let relative = world_point - self.position;
let local = self.inv_rotation.rotate_vec(relative);
let (lx, ly, lz) = local.to_f32();
let inv_s = self.inv_scale_f32;
(lx * inv_s, ly * inv_s, lz * inv_s)
}
#[cfg(feature = "std")]
#[inline]
pub(crate) fn local_normal_to_world(&self, nx: f32, ny: f32, nz: f32) -> Vec3Fix {
let local_n = Vec3Fix::from_f32(nx, ny, nz);
self.rotation.rotate_vec(local_n).normalize()
}
}
pub const SDF_STATIC: usize = usize::MAX;
impl SdfCollider {
#[must_use]
pub fn new_static(field: Box<dyn SdfField>, position: Vec3Fix, rotation: QuatFix) -> Self {
let rotation = rotation.unit_rotation();
Self {
field,
position,
inv_rotation: rotation.conjugate(),
rotation,
scale: Fix128::ONE,
body_index: SDF_STATIC,
scale_f32: 1.0,
inv_scale_f32: 1.0,
}
}
#[must_use]
pub fn new_dynamic(field: Box<dyn SdfField>, body_index: usize) -> Self {
Self {
field,
position: Vec3Fix::ZERO,
rotation: QuatFix::IDENTITY,
inv_rotation: QuatFix::IDENTITY,
scale: Fix128::ONE,
body_index,
scale_f32: 1.0,
inv_scale_f32: 1.0,
}
}
#[must_use]
pub fn with_scale(mut self, scale: Fix128) -> Self {
self.scale = scale;
let s = scale.to_f32();
self.scale_f32 = s;
self.inv_scale_f32 = if s.abs() < 1e-10 { 1.0 } else { 1.0 / s };
self
}
pub fn update_cache(&mut self) {
self.rotation = self.rotation.unit_rotation();
self.inv_rotation = self.rotation.conjugate();
let s = self.scale.to_f32();
self.scale_f32 = s;
self.inv_scale_f32 = if s.abs() < 1e-10 { 1.0 } else { 1.0 / s };
}
pub fn set_pose(&mut self, position: Vec3Fix, rotation: QuatFix) {
self.position = position;
self.rotation = rotation;
self.update_cache();
}
pub fn sync_to_body(&mut self, bodies: &[crate::solver::RigidBody]) -> bool {
if self.body_index == SDF_STATIC {
return false;
}
match bodies.get(self.body_index) {
Some(body) => {
self.set_pose(body.position, body.rotation);
true
}
None => false,
}
}
#[cfg(feature = "std")]
#[inline]
pub(crate) fn world_to_local(&self, world_point: Vec3Fix) -> (f32, f32, f32) {
self.frame().world_to_local(world_point)
}
#[cfg(feature = "std")]
#[inline]
pub(crate) fn local_normal_to_world(&self, nx: f32, ny: f32, nz: f32) -> Vec3Fix {
self.frame().local_normal_to_world(nx, ny, nz)
}
#[must_use]
pub fn frame(&self) -> SdfFrame {
SdfFrame {
position: self.position,
rotation: self.rotation,
inv_rotation: self.inv_rotation,
scale_f32: self.scale_f32,
inv_scale_f32: self.inv_scale_f32,
}
}
}
pub fn sync_dynamic_sdf_colliders(
colliders: &mut [SdfCollider],
bodies: &[crate::solver::RigidBody],
) -> usize {
let mut moved = 0;
for collider in colliders {
if collider.sync_to_body(bodies) {
moved += 1;
}
}
moved
}
#[cfg(feature = "std")]
#[must_use]
pub fn collide_point_sdf(point: Vec3Fix, sdf: &SdfCollider) -> Option<Contact> {
collide_point_sdf_field(point, &*sdf.field, &sdf.frame())
}
#[cfg(feature = "std")]
#[must_use]
pub fn collide_point_sdf_field<F: SdfQuery + ?Sized>(
point: Vec3Fix,
field: &F,
frame: &SdfFrame,
) -> Option<Contact> {
let (lx, ly, lz) = frame.world_to_local(point);
let dist = field.query_distance(lx, ly, lz);
let world_dist = dist * frame.scale_f32();
if world_dist >= 0.0 {
return None; }
let (nx, ny, nz) = field.query_normal(lx, ly, lz);
let depth = Fix128::from_f32(-world_dist);
let normal = frame.local_normal_to_world(nx, ny, nz);
let surface_point = point + normal * depth;
Some(Contact {
depth,
normal,
point_a: point,
point_b: surface_point,
})
}
#[cfg(feature = "std")]
#[must_use]
pub fn collide_sphere_sdf(center: Vec3Fix, radius: Fix128, sdf: &SdfCollider) -> Option<Contact> {
collide_sphere_sdf_field(center, radius, &*sdf.field, &sdf.frame())
}
#[cfg(feature = "std")]
#[must_use]
pub fn collide_sphere_sdf_field<F: SdfQuery + ?Sized>(
center: Vec3Fix,
radius: Fix128,
field: &F,
frame: &SdfFrame,
) -> Option<Contact> {
let (lx, ly, lz) = frame.world_to_local(center);
let dist = field.query_distance(lx, ly, lz);
let world_dist = dist * frame.scale_f32();
let radius_f32 = radius.to_f32();
let penetration = radius_f32 - world_dist;
if penetration <= 0.0 {
return None; }
let (nx, ny, nz) = field.query_normal(lx, ly, lz);
let depth = Fix128::from_f32(penetration);
let normal = frame.local_normal_to_world(nx, ny, nz);
let point_a = center - normal * radius;
let point_b = center - normal * Fix128::from_f32(world_dist);
Some(Contact {
depth,
normal,
point_a,
point_b,
})
}
#[cfg(feature = "std")]
#[must_use]
pub fn collide_capsule_sdf(
a: Vec3Fix,
b: Vec3Fix,
radius: Fix128,
sdf: &SdfCollider,
) -> Option<Contact> {
let mid = Vec3Fix::new((a.x + b.x).half(), (a.y + b.y).half(), (a.z + b.z).half());
let samples = [a, mid, b];
let mut best: Option<Contact> = None;
for &point in &samples {
if let Some(contact) = collide_sphere_sdf(point, radius, sdf) {
match &best {
Some(prev) if prev.depth >= contact.depth => {}
_ => best = Some(contact),
}
}
}
best
}
#[cfg(feature = "std")]
#[must_use]
pub(crate) fn box_sample_points(
center: Vec3Fix,
half_extents: Vec3Fix,
rotation: QuatFix,
) -> [Vec3Fix; 27] {
let mut points = [center; 27];
let steps = [-Fix128::ONE, Fix128::ZERO, Fix128::ONE];
let mut n = 0;
for &i in &steps {
for &j in &steps {
for &k in &steps {
let local =
Vec3Fix::new(half_extents.x * i, half_extents.y * j, half_extents.z * k);
points[n] = center + rotation.unit_rotation().rotate_vec(local);
n += 1;
}
}
}
points
}
#[cfg(feature = "std")]
#[must_use]
pub(crate) fn collide_points_sdf(points: &[Vec3Fix], sdf: &SdfCollider) -> Option<Contact> {
let mut best: Option<Contact> = None;
for &point in points {
if let Some(contact) = collide_point_sdf(point, sdf) {
match &best {
Some(prev) if prev.depth >= contact.depth => {}
_ => best = Some(contact),
}
}
}
best
}
#[cfg(feature = "std")]
#[must_use]
pub fn collide_aabb_sdf(min: Vec3Fix, max: Vec3Fix, sdf: &SdfCollider) -> Option<Contact> {
let center = Vec3Fix::new(
(min.x + max.x).half(),
(min.y + max.y).half(),
(min.z + max.z).half(),
);
let half = Vec3Fix::new(
(max.x - min.x).half(),
(max.y - min.y).half(),
(max.z - min.z).half(),
);
collide_points_sdf(&box_sample_points(center, half, QuatFix::IDENTITY), sdf)
}
#[cfg(feature = "std")]
#[must_use]
pub fn detect_sdf_contacts(
bodies: &[crate::solver::RigidBody],
sdf_colliders: &[SdfCollider],
collision_radius: Fix128,
) -> Vec<(usize, Contact)> {
let mut contacts = Vec::new();
for (body_idx, body) in bodies.iter().enumerate() {
if body.is_static() {
continue;
}
for sdf in sdf_colliders {
if sdf.body_index == body_idx {
continue;
}
if let Some(contact) = collide_sphere_sdf(body.position, collision_radius, sdf) {
contacts.push((body_idx, contact));
}
}
}
contacts
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
fn unit_sphere() -> ClosureSdf {
ClosureSdf::new(
|x, y, z| z.mul_add(z, x.mul_add(x, y * y)).sqrt() - 1.0,
|x, y, z| {
let len = z.mul_add(z, x.mul_add(x, y * y)).sqrt();
if len < 1e-10 {
(0.0, 1.0, 0.0) } else {
(x / len, y / len, z / len)
}
},
)
}
fn ground_plane() -> ClosureSdf {
ClosureSdf::new(|_x, y, _z| y, |_x, _y, _z| (0.0, 1.0, 0.0))
}
#[test]
fn test_point_outside_sphere() {
let sdf =
SdfCollider::new_static(Box::new(unit_sphere()), Vec3Fix::ZERO, QuatFix::IDENTITY);
let point = Vec3Fix::from_f32(2.0, 0.0, 0.0);
let result = collide_point_sdf(point, &sdf);
assert!(result.is_none(), "Point outside sphere should not collide");
}
#[test]
fn test_point_inside_sphere() {
let sdf =
SdfCollider::new_static(Box::new(unit_sphere()), Vec3Fix::ZERO, QuatFix::IDENTITY);
let point = Vec3Fix::from_f32(0.5, 0.0, 0.0);
let result = collide_point_sdf(point, &sdf);
assert!(result.is_some(), "Point inside sphere should collide");
let contact = result.unwrap();
let depth = contact.depth.to_f32();
assert!(
(depth - 0.5).abs() < 0.05,
"Depth should be ~0.5, got {depth}"
);
let (nx, _, _) = contact.normal.to_f32();
assert!(nx > 0.9, "Normal should point in +X, got {nx}");
}
#[test]
fn test_sphere_vs_ground() {
let sdf =
SdfCollider::new_static(Box::new(ground_plane()), Vec3Fix::ZERO, QuatFix::IDENTITY);
let center = Vec3Fix::from_f32(0.0, 0.5, 0.0);
let radius = Fix128::ONE;
let result = collide_sphere_sdf(center, radius, &sdf);
assert!(result.is_some(), "Sphere should penetrate ground");
let contact = result.unwrap();
let depth = contact.depth.to_f32();
assert!(
(depth - 0.5).abs() < 0.05,
"Penetration should be ~0.5, got {depth}"
);
}
#[test]
fn test_sphere_above_ground() {
let sdf =
SdfCollider::new_static(Box::new(ground_plane()), Vec3Fix::ZERO, QuatFix::IDENTITY);
let center = Vec3Fix::from_f32(0.0, 2.0, 0.0);
let radius = Fix128::ONE;
let result = collide_sphere_sdf(center, radius, &sdf);
assert!(result.is_none(), "Sphere above ground should not collide");
}
#[test]
fn test_capsule_vs_ground() {
let sdf =
SdfCollider::new_static(Box::new(ground_plane()), Vec3Fix::ZERO, QuatFix::IDENTITY);
let a = Vec3Fix::from_f32(-1.0, 0.3, 0.0);
let b = Vec3Fix::from_f32(1.0, 0.3, 0.0);
let radius = Fix128::from_f32(0.5);
let result = collide_capsule_sdf(a, b, radius, &sdf);
assert!(result.is_some(), "Capsule should penetrate ground");
let contact = result.unwrap();
let depth = contact.depth.to_f32();
assert!(
(depth - 0.2).abs() < 0.05,
"Penetration should be ~0.2, got {depth}"
);
}
#[test]
fn test_translated_sdf() {
let sdf = SdfCollider::new_static(
Box::new(unit_sphere()),
Vec3Fix::from_f32(5.0, 0.0, 0.0),
QuatFix::IDENTITY,
);
let point = Vec3Fix::from_f32(5.5, 0.0, 0.0);
let result = collide_point_sdf(point, &sdf);
assert!(
result.is_some(),
"Point inside translated sphere should collide"
);
let origin = Vec3Fix::ZERO;
let result2 = collide_point_sdf(origin, &sdf);
assert!(
result2.is_none(),
"Origin should be outside translated sphere"
);
}
#[test]
fn test_scaled_sdf() {
let sdf =
SdfCollider::new_static(Box::new(unit_sphere()), Vec3Fix::ZERO, QuatFix::IDENTITY)
.with_scale(Fix128::from_int(3));
let point = Vec3Fix::from_f32(2.0, 0.0, 0.0);
let result = collide_point_sdf(point, &sdf);
assert!(
result.is_some(),
"Point inside scaled sphere should collide"
);
let point_outside = Vec3Fix::from_f32(4.0, 0.0, 0.0);
let result2 = collide_point_sdf(point_outside, &sdf);
assert!(
result2.is_none(),
"Point outside scaled sphere should not collide"
);
}
#[test]
fn test_aabb_vs_ground() {
let sdf =
SdfCollider::new_static(Box::new(ground_plane()), Vec3Fix::ZERO, QuatFix::IDENTITY);
let min = Vec3Fix::from_f32(-1.0, -0.5, -1.0);
let max = Vec3Fix::from_f32(1.0, 1.0, 1.0);
let result = collide_aabb_sdf(min, max, &sdf);
assert!(result.is_some(), "AABB below ground should collide");
let contact = result.unwrap();
let depth = contact.depth.to_f32();
assert!(
(depth - 0.5).abs() < 0.05,
"Penetration should be ~0.5, got {depth}"
);
}
#[test]
fn detect_sdf_contacts_reports_penetrating_dynamic_bodies_with_closed_form_depth() {
use crate::solver::RigidBody;
let ground =
SdfCollider::new_static(Box::new(ground_plane()), Vec3Fix::ZERO, QuatFix::IDENTITY);
let attached = SdfCollider::new_dynamic(Box::new(unit_sphere()), 5);
let colliders = [ground, attached];
let radius = Fix128::from_ratio(1, 2);
let mut bodies = vec![
RigidBody::new_dynamic(Vec3Fix::from_f32(5.0, 0.25, 0.0), Fix128::ONE),
RigidBody::new_dynamic(Vec3Fix::from_f32(0.0, 2.0, 0.0), Fix128::ONE),
RigidBody::new_static(Vec3Fix::from_f32(0.0, -1.0, 0.0)),
RigidBody::new_dynamic(Vec3Fix::from_f32(0.0, 0.0, 5.0), Fix128::ONE),
RigidBody::new_dynamic(Vec3Fix::from_f32(1.2, 3.0, 0.0), Fix128::ONE),
RigidBody::new_dynamic(Vec3Fix::from_f32(1.2, 0.0, 0.0), Fix128::ONE),
];
let contacts = detect_sdf_contacts(&bodies, &colliders, radius);
let idxs: Vec<usize> = contacts.iter().map(|(i, _)| *i).collect();
assert_eq!(idxs, vec![0, 3, 5]);
let expected_depth = [0.25_f32, 0.5, 0.5];
for (k, want) in expected_depth.iter().enumerate() {
let d = contacts[k].1.depth.to_f32();
assert!(
(d - want).abs() < 1e-5,
"contact {k} depth {d}, want {want}"
);
let (nx, ny, nz) = contacts[k].1.normal.to_f32();
assert!(nx.abs() < 1e-5 && (ny - 1.0).abs() < 1e-5 && nz.abs() < 1e-5);
let (_, pby, _) = contacts[k].1.point_b.to_f32();
let (_, pay, _) = contacts[k].1.point_a.to_f32();
let (_, y, _) = bodies[idxs[k]].position.to_f32();
assert!(pby.abs() < 1e-5, "contact {k} point_b.y {pby}");
assert!(
(pay - (y - 0.5)).abs() < 1e-5,
"contact {k} point_a.y {pay}"
);
}
bodies.push(RigidBody::new_dynamic(
Vec3Fix::from_f32(1.2, 0.0, 0.0),
Fix128::ONE,
));
let c2 = detect_sdf_contacts(&bodies, &colliders, radius);
let idxs2: Vec<usize> = c2.iter().map(|(i, _)| *i).collect();
assert_eq!(idxs2, vec![0, 3, 5, 6, 6]);
let sphere_contact = &c2[4].1;
let d = sphere_contact.depth.to_f32();
assert!((d - 0.3).abs() < 1e-5, "sphere depth {d}");
let (nx, ny, nz) = sphere_contact.normal.to_f32();
assert!((nx - 1.0).abs() < 1e-5 && ny.abs() < 1e-5 && nz.abs() < 1e-5);
let none = detect_sdf_contacts(&bodies[3..4], &colliders[..1], Fix128::ZERO);
assert!(none.is_empty());
assert!(detect_sdf_contacts(&bodies, &[], radius).is_empty());
}
}