use crate::point::Point;
use crate::shape::Shape;
use nalgebra::SVector;
const HALFSPACE_EXTENT: f64 = 1e6;
#[derive(Clone, Copy, Debug)]
pub struct HalfSpace<const D: usize> {
pub normal: SVector<f64, D>,
pub offset: f64,
}
impl<const D: usize> HalfSpace<D> {
pub fn new(normal: SVector<f64, D>, offset: f64) -> Self {
Self { normal, offset }
}
pub fn ground(axis: usize, height: f64) -> Self {
let mut normal = SVector::zeros();
normal[axis] = 1.0;
Self {
normal,
offset: height,
}
}
#[inline]
pub fn signed_distance(&self, point: &SVector<f64, D>) -> f64 {
self.normal.dot(point) - self.offset
}
pub fn project(&self, point: &SVector<f64, D>) -> SVector<f64, D> {
point - self.normal * self.signed_distance(point)
}
pub fn contact_sphere(
&self,
sphere_center: &SVector<f64, D>,
sphere_radius: f64,
) -> Option<(SVector<f64, D>, f64)> {
let dist = self.signed_distance(sphere_center);
let depth = sphere_radius - dist;
if depth <= 0.0 {
return None;
}
let contact = sphere_center - self.normal * dist;
Some((contact, depth))
}
pub fn contact_capsule(
&self,
capsule_pos: &SVector<f64, D>,
half_height: f64,
radius: f64,
axis: usize,
) -> Vec<(SVector<f64, D>, f64)> {
let mut contacts = Vec::new();
let mut axis_vec = SVector::zeros();
axis_vec[axis] = 1.0;
let center_a = capsule_pos + axis_vec * half_height;
let center_b = capsule_pos - axis_vec * half_height;
if let Some(c) = self.contact_sphere(¢er_a, radius) {
contacts.push(c);
}
if let Some(c) = self.contact_sphere(¢er_b, radius) {
contacts.push(c);
}
contacts
}
pub fn contact_box(
&self,
box_pos: &SVector<f64, D>,
half_extents: &[f64; D],
) -> Vec<(SVector<f64, D>, f64)> {
let mut contacts = Vec::new();
let num_vertices = 1usize << D;
for bits in 0..num_vertices {
let mut vertex = *box_pos;
for axis in 0..D {
if bits & (1 << axis) != 0 {
vertex[axis] += half_extents[axis];
} else {
vertex[axis] -= half_extents[axis];
}
}
let dist = self.signed_distance(&vertex);
if dist < 0.0 {
let contact = self.project(&vertex);
contacts.push((contact, -dist));
}
}
contacts
}
}
impl<const D: usize> Shape<D> for HalfSpace<D> {
fn support(&self, direction: &SVector<f64, D>) -> SVector<f64, D> {
let dot = direction.dot(&self.normal);
if dot >= 0.0 {
let tangent = direction - self.normal * dot;
let t_norm = tangent.norm();
if t_norm > 1e-15 {
self.normal * self.offset + tangent / t_norm * HALFSPACE_EXTENT
} else {
self.normal * self.offset
}
} else {
self.normal * (self.offset - HALFSPACE_EXTENT)
}
}
fn bounding_sphere(&self) -> (Point<D>, f64) {
(Point::origin(), HALFSPACE_EXTENT)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn signed_distance_above() {
let plane = HalfSpace::<3>::ground(1, 0.0); let point = SVector::from([0.0, 5.0, 0.0]);
assert!((plane.signed_distance(&point) - 5.0).abs() < 1e-12);
}
#[test]
fn signed_distance_below() {
let plane = HalfSpace::<3>::ground(1, 0.0);
let point = SVector::from([0.0, -3.0, 0.0]);
assert!((plane.signed_distance(&point) - (-3.0)).abs() < 1e-12);
}
#[test]
fn project_onto_plane() {
let plane = HalfSpace::<3>::ground(1, 0.0);
let point = SVector::from([3.0, 5.0, 7.0]);
let proj = plane.project(&point);
assert!((proj[0] - 3.0).abs() < 1e-12);
assert!((proj[1] - 0.0).abs() < 1e-12);
assert!((proj[2] - 7.0).abs() < 1e-12);
}
#[test]
fn sphere_contact_resting() {
let plane = HalfSpace::<3>::ground(1, 0.0);
let center = SVector::from([0.0, 0.5, 0.0]); let (contact, depth) = plane.contact_sphere(¢er, 1.0).unwrap();
assert!((depth - 0.5).abs() < 1e-12, "depth = {depth}");
assert!((contact[1] - 0.0).abs() < 1e-12, "contact Y = {}", contact[1]);
}
#[test]
fn sphere_no_contact() {
let plane = HalfSpace::<3>::ground(1, 0.0);
let center = SVector::from([0.0, 2.0, 0.0]);
assert!(plane.contact_sphere(¢er, 1.0).is_none());
}
#[test]
fn capsule_two_contacts() {
let plane = HalfSpace::<3>::ground(1, 0.0);
let pos = SVector::from([0.0, 0.3, 0.0]);
let contacts = plane.contact_capsule(&pos, 2.0, 0.5, 0);
assert_eq!(contacts.len(), 2);
for (_, depth) in &contacts {
assert!(
(depth - 0.2).abs() < 1e-10,
"capsule contact depth = {depth}"
);
}
}
#[test]
fn box_contact_on_plane() {
let plane = HalfSpace::<3>::ground(1, 0.0);
let pos = SVector::from([0.0, 0.5, 0.0]);
let contacts = plane.contact_box(&pos, &[1.0, 1.0, 1.0]);
assert_eq!(contacts.len(), 4, "expected 4 bottom vertices to penetrate");
for (_, depth) in &contacts {
assert!(
(depth - 0.5).abs() < 1e-10,
"box vertex depth = {depth}"
);
}
}
#[test]
fn box_no_contact() {
let plane = HalfSpace::<3>::ground(1, 0.0);
let pos = SVector::from([0.0, 5.0, 0.0]);
let contacts = plane.contact_box(&pos, &[1.0, 1.0, 1.0]);
assert!(contacts.is_empty());
}
#[test]
fn halfspace_4d() {
let plane = HalfSpace::<4>::ground(3, 0.0); let center = SVector::from([0.0, 0.0, 0.0, 0.8]);
let contact = plane.contact_sphere(¢er, 1.0);
assert!(contact.is_some());
let (_, depth) = contact.unwrap();
assert!((depth - 0.2).abs() < 1e-12, "4D depth = {depth}");
}
#[test]
fn halfspace_offset() {
let plane = HalfSpace::<3>::ground(1, 2.0); let center = SVector::from([0.0, 2.5, 0.0]);
let contact = plane.contact_sphere(¢er, 1.0);
assert!(contact.is_some());
let (_, depth) = contact.unwrap();
assert!((depth - 0.5).abs() < 1e-12, "offset plane depth = {depth}");
}
#[test]
fn halfspace_2d() {
let plane = HalfSpace::<2>::ground(1, 0.0);
let center = SVector::from([3.0, 0.5]);
let (contact, depth) = plane.contact_sphere(¢er, 1.0).unwrap();
assert!((depth - 0.5).abs() < 1e-12);
assert!((contact[0] - 3.0).abs() < 1e-12);
}
}