use crate::collider::{hypot3, Support, AABB};
use crate::math::{Fix128, QuatFix, Vec3Fix};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Cylinder {
pub center: Vec3Fix,
pub half_height: Fix128,
pub radius: Fix128,
pub rotation: QuatFix,
}
impl Cylinder {
#[inline]
#[must_use]
pub const fn new(center: Vec3Fix, half_height: Fix128, radius: Fix128) -> Self {
Self {
center,
half_height,
radius,
rotation: QuatFix::IDENTITY,
}
}
#[inline]
#[must_use]
pub const fn with_rotation(
center: Vec3Fix,
half_height: Fix128,
radius: Fix128,
rotation: QuatFix,
) -> Self {
Self {
center,
half_height,
radius,
rotation,
}
}
#[must_use]
pub fn aabb(&self) -> AABB {
let r = self.unit_rotation();
let u = r.rotate_vec(Vec3Fix::new(self.radius, Fix128::ZERO, Fix128::ZERO));
let a = r.rotate_vec(Vec3Fix::new(Fix128::ZERO, self.half_height, Fix128::ZERO));
let w = r.rotate_vec(Vec3Fix::new(Fix128::ZERO, Fix128::ZERO, self.radius));
let total = Vec3Fix::new(
a.x.abs() + hypot3(u.x, w.x, Fix128::ZERO),
a.y.abs() + hypot3(u.y, w.y, Fix128::ZERO),
a.z.abs() + hypot3(u.z, w.z, Fix128::ZERO),
);
AABB::new(self.center - total, self.center + total)
}
#[inline]
#[must_use]
pub fn volume(&self) -> Fix128 {
let pi = Fix128::PI;
let two = Fix128::from_int(2);
pi * self.radius * self.radius * two * self.half_height.abs()
}
#[inline]
#[must_use]
pub fn surface_area(&self) -> Fix128 {
let pi = Fix128::PI;
let two = Fix128::from_int(2);
let (r, h) = (self.radius.abs(), self.half_height.abs());
two * pi * r * (r + two * h)
}
fn unit_rotation(&self) -> QuatFix {
let q = self.rotation;
let n2 = q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w;
if (n2 - Fix128::ONE).abs() <= Fix128::from_raw(0, 1 << 24) {
q
} else {
q.normalize()
}
}
#[must_use]
pub fn inertia_diagonal(&self, mass: Fix128) -> Vec3Fix {
let r2 = self.radius * self.radius;
let h = self.half_height * Fix128::from_int(2);
let h2 = h * h;
let three = Fix128::from_int(3);
let twelve = Fix128::from_int(12);
let two = Fix128::from_int(2);
let ixx = mass * (three * r2 + h2) / twelve;
let iyy = mass * r2 / two;
Vec3Fix::new(ixx, iyy, ixx)
}
}
impl Support for Cylinder {
fn support(&self, direction: Vec3Fix) -> Vec3Fix {
let direction = direction.rescaled_direction();
let rotation = self.unit_rotation();
let (half_height, radius) = (self.half_height.abs(), self.radius.abs());
let local_dir = rotation.conjugate().rotate_vec(direction);
let y_support = if local_dir.y >= Fix128::ZERO {
half_height
} else {
-half_height
};
let xz_len_sq = local_dir.x * local_dir.x + local_dir.z * local_dir.z;
let local_support = if xz_len_sq.is_zero() {
Vec3Fix::new(Fix128::ZERO, y_support, Fix128::ZERO)
} else {
let xz_len = xz_len_sq.sqrt();
let x_support = radius * local_dir.x / xz_len;
let z_support = radius * local_dir.z / xz_len;
Vec3Fix::new(x_support, y_support, z_support)
};
self.center + rotation.rotate_vec(local_support)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cylinder_support_up() {
let cyl = Cylinder::new(Vec3Fix::ZERO, Fix128::from_int(2), Fix128::ONE);
let s = cyl.support(Vec3Fix::UNIT_Y);
assert_eq!(s.y.hi, 2);
}
#[test]
fn test_cylinder_support_side() {
let cyl = Cylinder::new(Vec3Fix::ZERO, Fix128::from_int(2), Fix128::from_int(3));
let s = cyl.support(Vec3Fix::UNIT_X);
assert_eq!(s.x.hi, 3); assert_eq!(s.y.hi, 2); }
#[test]
fn test_cylinder_support_down() {
let cyl = Cylinder::new(Vec3Fix::ZERO, Fix128::from_int(2), Fix128::ONE);
let s = cyl.support(-Vec3Fix::UNIT_Y);
assert_eq!(s.y.hi, -2);
}
#[test]
fn test_cylinder_aabb() {
let cyl = Cylinder::new(Vec3Fix::from_int(5, 0, 0), Fix128::from_int(2), Fix128::ONE);
let aabb = cyl.aabb();
assert_eq!(aabb.min.x.hi, 4); assert_eq!(aabb.max.x.hi, 6); assert_eq!(aabb.min.y.hi, -2); assert_eq!(aabb.max.y.hi, 2); }
#[test]
fn test_cylinder_volume() {
let cyl = Cylinder::new(Vec3Fix::ZERO, Fix128::ONE, Fix128::ONE);
let v = cyl.volume();
assert!(
v.hi >= 6 && v.hi <= 7,
"Volume should be ~6.28, got {}",
v.hi
);
}
#[test]
fn test_cylinder_inertia() {
let cyl = Cylinder::new(Vec3Fix::ZERO, Fix128::ONE, Fix128::ONE);
let mass = Fix128::from_int(12);
let inertia = cyl.inertia_diagonal(mass);
assert_eq!(inertia.x.hi, 7);
assert_eq!(inertia.y.hi, 6);
}
#[test]
fn test_cylinder_surface_area() {
let cyl = Cylinder::new(Vec3Fix::ZERO, Fix128::ONE, Fix128::ONE);
let sa = cyl.surface_area();
assert!(
sa.hi >= 18 && sa.hi <= 19,
"Surface area should be ~18.85, got {}",
sa.hi
);
}
#[test]
fn test_rotated_cylinder_support() {
let rot = QuatFix::from_axis_angle(Vec3Fix::UNIT_Z, Fix128::HALF_PI);
let cyl = Cylinder::with_rotation(Vec3Fix::ZERO, Fix128::from_int(3), Fix128::ONE, rot);
let s = cyl.support(Vec3Fix::UNIT_X);
assert!(
s.x > Fix128::ONE,
"Rotated cylinder support should extend along X"
);
}
#[test]
fn test_cylinder_gjk_collision() {
use crate::collider::{gjk, Sphere};
let cyl = Cylinder::new(Vec3Fix::ZERO, Fix128::from_int(2), Fix128::ONE);
let sphere = Sphere::new(Vec3Fix::from_int(0, 0, 0), Fix128::ONE);
let result = gjk(&cyl, &sphere);
assert!(
result.colliding,
"Overlapping cylinder and sphere should collide"
);
}
#[test]
fn test_cylinder_gjk_no_collision() {
use crate::collider::{gjk, Sphere};
let cyl = Cylinder::new(Vec3Fix::ZERO, Fix128::ONE, Fix128::ONE);
let sphere = Sphere::new(Vec3Fix::from_int(10, 0, 0), Fix128::ONE);
let result = gjk(&cyl, &sphere);
assert!(
!result.colliding,
"Separated cylinder and sphere should not collide"
);
}
}