use crate::shape::{RayHit, Shape};
use oxiphysics_core::Aabb;
use oxiphysics_core::math::{Mat3, Real, Vec3};
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct Cone {
pub radius: Real,
pub half_height: Real,
}
impl Cone {
pub fn new(radius: Real, half_height: Real) -> Self {
Self {
radius,
half_height,
}
}
#[allow(dead_code)]
pub fn volume_explicit(&self) -> Real {
let h = 2.0 * self.half_height;
PI * self.radius * self.radius * h / 3.0
}
#[allow(dead_code)]
pub fn surface_area(&self) -> Real {
let r = self.radius;
let h = 2.0 * self.half_height;
let slant = (r * r + h * h).sqrt();
PI * r * r + PI * r * slant
}
#[allow(dead_code)]
pub fn inertia_tensor_array(&self, mass: f64) -> [[f64; 3]; 3] {
let r2 = self.radius * self.radius;
let h2 = (2.0 * self.half_height).powi(2);
let iy = 3.0 * mass * r2 / 10.0;
let ixz = mass * (3.0 * r2 + 2.0 * h2) / 20.0;
[[ixz, 0.0, 0.0], [0.0, iy, 0.0], [0.0, 0.0, ixz]]
}
#[allow(dead_code)]
pub fn ray_cast_array(
&self,
origin: [f64; 3],
direction: [f64; 3],
max_toi: f64,
) -> Option<(f64, [f64; 3])> {
let o = Vec3::new(origin[0], origin[1], origin[2]);
let d = Vec3::new(direction[0], direction[1], direction[2]);
let hit = self.ray_cast(&o, &d, max_toi)?;
Some((hit.toi, [hit.normal.x, hit.normal.y, hit.normal.z]))
}
#[allow(dead_code)]
pub fn closest_point(&self, p: [f64; 3]) -> [f64; 3] {
let px = p[0];
let py = p[1];
let pz = p[2];
let cy = py.clamp(-self.half_height, self.half_height);
let h = 2.0 * self.half_height;
let r_at_cy = self.radius * (self.half_height - cy) / h;
let xz_len = (px * px + pz * pz).sqrt();
if xz_len <= r_at_cy {
let dist_to_base = py - (-self.half_height);
let dist_to_side = r_at_cy - xz_len;
if dist_to_base <= dist_to_side {
[px, -self.half_height, pz]
} else {
if xz_len < 1e-12 {
[r_at_cy, cy, 0.0]
} else {
let s = r_at_cy / xz_len;
[px * s, cy, pz * s]
}
}
} else {
if xz_len < 1e-12 {
[r_at_cy, cy, 0.0]
} else {
let s = r_at_cy / xz_len;
[px * s, cy, pz * s]
}
}
}
#[allow(dead_code)]
pub fn support(&self, direction: [f64; 3]) -> [f64; 3] {
let h = 2.0 * self.half_height;
let xz_len = (direction[0] * direction[0] + direction[2] * direction[2]).sqrt();
let apex_dot = direction[1] * self.half_height;
let base_center_dot = -direction[1] * self.half_height;
let base_rim_dot = base_center_dot + self.radius * xz_len;
if apex_dot >= base_rim_dot {
[0.0, self.half_height, 0.0]
} else if xz_len > 1e-10 {
let _sin_angle = self.radius / (self.radius * self.radius + h * h).sqrt();
[
self.radius * direction[0] / xz_len,
-self.half_height,
self.radius * direction[2] / xz_len,
]
} else {
[self.radius, -self.half_height, 0.0]
}
}
#[allow(dead_code)]
pub fn slant_height(&self) -> f64 {
let h = 2.0 * self.half_height;
(self.radius * self.radius + h * h).sqrt()
}
#[allow(dead_code)]
pub fn half_angle(&self) -> f64 {
let h = 2.0 * self.half_height;
(self.radius / h).atan()
}
#[allow(dead_code)]
pub fn lateral_surface_area(&self) -> f64 {
PI * self.radius * self.slant_height()
}
#[allow(dead_code)]
pub fn base_area(&self) -> f64 {
PI * self.radius * self.radius
}
#[allow(dead_code)]
pub fn contains_point(&self, p: [f64; 3]) -> bool {
if p[1] < -self.half_height || p[1] > self.half_height {
return false;
}
let h = 2.0 * self.half_height;
let r_at_y = self.radius * (self.half_height - p[1]) / h;
let xz2 = p[0] * p[0] + p[2] * p[2];
xz2 <= r_at_y * r_at_y
}
#[allow(dead_code)]
pub fn signed_distance(&self, p: [f64; 3]) -> f64 {
let cp = self.closest_point(p);
let dx = p[0] - cp[0];
let dy = p[1] - cp[1];
let dz = p[2] - cp[2];
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
if self.contains_point(p) { -dist } else { dist }
}
#[allow(dead_code)]
pub fn radius_at_y(&self, y: f64) -> f64 {
let clamped = y.clamp(-self.half_height, self.half_height);
let h = 2.0 * self.half_height;
self.radius * (self.half_height - clamped) / h
}
#[allow(dead_code)]
pub fn truncated_cone(&self, cut_ratio: f64) -> (f64, f64, f64) {
let ratio = cut_ratio.clamp(0.0, 1.0);
let top_r = self.radius * (1.0 - ratio);
let bottom_r = self.radius;
let hh = self.half_height * ratio;
(top_r, bottom_r, hh)
}
#[allow(dead_code)]
pub fn truncated_cone_volume(&self, cut_ratio: f64) -> f64 {
let (r1, r2, _) = self.truncated_cone(cut_ratio);
let h = 2.0 * self.half_height * cut_ratio.clamp(0.0, 1.0);
PI * h * (r1 * r1 + r1 * r2 + r2 * r2) / 3.0
}
#[allow(dead_code)]
pub fn ray_cast_base_cap(
&self,
origin: [f64; 3],
direction: [f64; 3],
max_toi: f64,
) -> Option<(f64, [f64; 3])> {
if direction[1].abs() < 1e-12 {
return None;
}
let t = (-self.half_height - origin[1]) / direction[1];
if t < 0.0 || t > max_toi {
return None;
}
let px = origin[0] + t * direction[0];
let pz = origin[2] + t * direction[2];
if px * px + pz * pz <= self.radius * self.radius {
Some((t, [0.0, -1.0, 0.0]))
} else {
None
}
}
#[allow(dead_code)]
pub fn closest_point_on_lateral(&self, p: [f64; 3]) -> [f64; 3] {
let xz_len = (p[0] * p[0] + p[2] * p[2]).sqrt();
let h = 2.0 * self.half_height;
let side_len = self.slant_height();
let side_dx = self.radius / side_len;
let side_dy = -h / side_len;
let vx = xz_len;
let vy = p[1] - self.half_height;
let t = (vx * side_dx + vy * side_dy).clamp(0.0, side_len);
let proj_xz = t * side_dx;
let proj_y = self.half_height + t * side_dy;
if xz_len < 1e-12 {
[proj_xz, proj_y, 0.0]
} else {
let s = proj_xz / xz_len;
[p[0] * s, proj_y, p[2] * s]
}
}
#[allow(dead_code)]
pub fn ray_cone_analytic(
&self,
origin: [f64; 3],
direction: [f64; 3],
max_toi: f64,
) -> Option<(f64, [f64; 3], bool)> {
let o = Vec3::new(origin[0], origin[1], origin[2]);
let d = Vec3::new(direction[0], direction[1], direction[2]);
let h = 2.0 * self.half_height;
let k = self.radius / h;
let k2 = k * k;
let apex_y = self.half_height;
let fy = apex_y - o.y;
let a = d.x * d.x + d.z * d.z - k2 * d.y * d.y;
let b = 2.0 * (o.x * d.x + o.z * d.z + k2 * fy * d.y);
let c_val = o.x * o.x + o.z * o.z - k2 * fy * fy;
let mut best_t = f64::INFINITY;
let mut best_is_lateral = true;
if a.abs() > 1e-12 {
let disc = b * b - 4.0 * a * c_val;
if disc >= 0.0 {
let sqrt_disc = disc.sqrt();
for t in [(-b - sqrt_disc) / (2.0 * a), (-b + sqrt_disc) / (2.0 * a)] {
if t >= 0.0 && t <= max_toi {
let py = o.y + t * d.y;
if py >= -self.half_height && py <= self.half_height && t < best_t {
best_t = t;
best_is_lateral = true;
}
}
}
}
}
if d.y.abs() > 1e-12 {
let t = (-self.half_height - o.y) / d.y;
if t >= 0.0 && t <= max_toi {
let px = o.x + t * d.x;
let pz = o.z + t * d.z;
if px * px + pz * pz <= self.radius * self.radius && t < best_t {
best_t = t;
best_is_lateral = false;
}
}
}
if best_t.is_infinite() {
return None;
}
let hit_pt = [
origin[0] + best_t * direction[0],
origin[1] + best_t * direction[1],
origin[2] + best_t * direction[2],
];
let normal = if best_is_lateral {
let xz_len = (hit_pt[0] * hit_pt[0] + hit_pt[2] * hit_pt[2])
.sqrt()
.max(1e-30);
let slope = self.radius / h;
let nx = hit_pt[0] / xz_len;
let nz = hit_pt[2] / xz_len;
let len = (nx * nx + slope * slope + nz * nz).sqrt();
[nx / len, slope / len, nz / len]
} else {
[0.0, -1.0, 0.0]
};
Some((best_t, normal, best_is_lateral))
}
#[allow(dead_code)]
pub fn intersects_plane(&self, plane_normal: [f64; 3], plane_d: f64) -> bool {
let apex = [0.0_f64, self.half_height, 0.0];
let apex_sd =
plane_normal[0] * apex[0] + plane_normal[1] * apex[1] + plane_normal[2] * apex[2]
- plane_d;
let base_center_sd = -plane_normal[1] * self.half_height - plane_d;
let xz_proj =
(plane_normal[0] * plane_normal[0] + plane_normal[2] * plane_normal[2]).sqrt();
let rim_sd = base_center_sd + self.radius * xz_proj;
let rim_sd_neg = base_center_sd - self.radius * xz_proj;
let signs = [apex_sd, rim_sd, rim_sd_neg];
let any_pos = signs.iter().any(|&s| s >= 0.0);
let any_neg = signs.iter().any(|&s| s <= 0.0);
any_pos && any_neg
}
#[allow(dead_code)]
pub fn frustum_from_cone(&self, y_bottom: f64, y_top: f64) -> (f64, f64, f64) {
let yb = y_bottom.clamp(-self.half_height, self.half_height);
let yt = y_top.clamp(yb, self.half_height);
let r_bottom = self.radius_at_y(yb);
let r_top = self.radius_at_y(yt);
let frustum_hh = (yt - yb) * 0.5;
(r_bottom, r_top, frustum_hh)
}
#[allow(dead_code)]
pub fn sdf(&self, p: [f64; 3]) -> f64 {
self.signed_distance(p)
}
#[allow(dead_code)]
pub fn intersects_sphere(&self, sphere_center: [f64; 3], sphere_radius: f64) -> bool {
let cp = self.closest_point(sphere_center);
let dx = sphere_center[0] - cp[0];
let dy = sphere_center[1] - cp[1];
let dz = sphere_center[2] - cp[2];
let dist_sq = dx * dx + dy * dy + dz * dz;
dist_sq <= sphere_radius * sphere_radius
}
#[allow(dead_code)]
pub fn frustum_lateral_area(&self, y_bottom: f64, y_top: f64) -> f64 {
let (r_bottom, r_top, _hh) = self.frustum_from_cone(y_bottom, y_top);
let dy = (y_top - y_bottom).abs();
let dr = (r_bottom - r_top).abs();
let slant = (dy * dy + dr * dr).sqrt();
PI * (r_bottom + r_top) * slant
}
#[allow(dead_code)]
pub fn apex_angle(&self) -> f64 {
2.0 * self.half_angle()
}
#[allow(dead_code)]
pub fn double_cone_params(&self) -> (f64, f64) {
(self.radius, self.half_height)
}
#[allow(dead_code)]
pub fn contains_point_double_cone(&self, p: [f64; 3]) -> bool {
let h = 2.0 * self.half_height;
let xz2 = p[0] * p[0] + p[2] * p[2];
let y_abs = p[1].abs();
if y_abs > self.half_height {
return false;
}
let r_at = self.radius * y_abs / self.half_height;
xz2 <= r_at * r_at + 1e-14 * h
}
#[allow(dead_code)]
pub fn unfold_lateral(&self, u: f64, t_slant: f64) -> [f64; 2] {
let sin_alpha = self.radius / self.slant_height().max(1e-14);
let phi = u * sin_alpha;
[t_slant * phi.cos(), t_slant * phi.sin()]
}
#[allow(dead_code)]
pub fn bounding_cone_of_points(points: &[[f64; 3]]) -> (f64, f64) {
if points.is_empty() {
return (0.0, 0.0);
}
let mut max_half_angle: f64 = 0.0;
let mut max_elev: f64 = 0.0;
for &p in points {
let len = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
if len < 1e-14 {
continue;
}
let cos_a = (p[1] / len).clamp(-1.0, 1.0);
let half_angle = cos_a.acos();
let elev = p[1] / len;
if half_angle > max_half_angle {
max_half_angle = half_angle;
max_elev = elev;
}
}
(max_half_angle, max_elev)
}
#[allow(dead_code)]
pub fn volume_swept(&self, delta: [f64; 3]) -> f64 {
let dist = (delta[0] * delta[0] + delta[1] * delta[1] + delta[2] * delta[2]).sqrt();
self.volume() + self.base_area() * dist
}
#[allow(dead_code)]
pub fn sdf_gradient(&self, p: [f64; 3]) -> [f64; 3] {
let eps = 1e-5;
let s0 = self.sdf(p);
let gx = (self.sdf([p[0] + eps, p[1], p[2]]) - s0) / eps;
let gy = (self.sdf([p[0], p[1] + eps, p[2]]) - s0) / eps;
let gz = (self.sdf([p[0], p[1], p[2] + eps]) - s0) / eps;
let len = (gx * gx + gy * gy + gz * gz).sqrt().max(1e-14);
[gx / len, gy / len, gz / len]
}
#[allow(dead_code)]
pub fn ray_intersection_count(
&self,
origin: [f64; 3],
direction: [f64; 3],
max_toi: f64,
) -> usize {
let mut count = 0usize;
let h = 2.0 * self.half_height;
let k = self.radius / h;
let k2 = k * k;
let apex_y = self.half_height;
let fy = apex_y - origin[1];
let a = direction[0] * direction[0] + direction[2] * direction[2]
- k2 * direction[1] * direction[1];
let b =
2.0 * (origin[0] * direction[0] + origin[2] * direction[2] + k2 * fy * direction[1]);
let c = origin[0] * origin[0] + origin[2] * origin[2] - k2 * fy * fy;
if a.abs() > 1e-12 {
let disc = b * b - 4.0 * a * c;
if disc >= 0.0 {
let sq = disc.sqrt();
for t in [(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)] {
if t >= 0.0 && t <= max_toi {
let py = origin[1] + t * direction[1];
if py >= -self.half_height && py <= self.half_height {
count += 1;
}
}
}
}
}
if direction[1].abs() > 1e-12 {
let t = (-self.half_height - origin[1]) / direction[1];
if t >= 0.0 && t <= max_toi {
let px = origin[0] + t * direction[0];
let pz = origin[2] + t * direction[2];
if px * px + pz * pz <= self.radius * self.radius {
count += 1;
}
}
}
count
}
#[allow(dead_code)]
pub fn solid_angle(&self) -> f64 {
let alpha = self.half_angle();
2.0 * PI * (1.0 - alpha.cos())
}
#[allow(dead_code)]
pub fn base_rim_points(&self, n: usize) -> Vec<[f64; 3]> {
let n = n.max(3);
(0..n)
.map(|i| {
let t = 2.0 * PI * i as f64 / n as f64;
[
self.radius * t.cos(),
-self.half_height,
self.radius * t.sin(),
]
})
.collect()
}
#[allow(dead_code)]
pub fn sphere_inside_cone(&self, center: [f64; 3], sphere_radius: f64) -> bool {
let sd = self.sdf(center);
sd + sphere_radius <= 0.0
}
#[allow(dead_code)]
pub fn random_lateral_surface_point(&self, seed: u64) -> [f64; 3] {
let mut state = seed;
let xorshift = |s: &mut u64| -> f64 {
*s ^= *s << 13;
*s ^= *s >> 7;
*s ^= *s << 17;
(*s as f64) / (u64::MAX as f64)
};
let u = xorshift(&mut state);
let frac_t = 1.0 - (1.0 - u).sqrt(); let y = self.half_height - frac_t * 2.0 * self.half_height;
let r_at_y = self.radius_at_y(y);
let theta = xorshift(&mut state) * 2.0 * PI;
[r_at_y * theta.cos(), y, r_at_y * theta.sin()]
}
}
impl Shape for Cone {
fn bounding_box(&self) -> Aabb {
Aabb::new(
Vec3::new(-self.radius, -self.half_height, -self.radius),
Vec3::new(self.radius, self.half_height, self.radius),
)
}
fn support_point(&self, direction: &Vec3) -> Vec3 {
let h = 2.0 * self.half_height;
let xz_len = (direction.x * direction.x + direction.z * direction.z).sqrt();
let apex_dot = direction.y * self.half_height;
let base_center_dot = -direction.y * self.half_height;
let base_rim_dot = base_center_dot + self.radius * xz_len;
if apex_dot >= base_rim_dot {
Vec3::new(0.0, self.half_height, 0.0)
} else if xz_len > 1e-10 {
let sin_angle = self.radius / (self.radius * self.radius + h * h).sqrt();
let _ = sin_angle; Vec3::new(
self.radius * direction.x / xz_len,
-self.half_height,
self.radius * direction.z / xz_len,
)
} else {
Vec3::new(self.radius, -self.half_height, 0.0)
}
}
fn volume(&self) -> Real {
let h = 2.0 * self.half_height;
PI * self.radius * self.radius * h / 3.0
}
fn center_of_mass(&self) -> Vec3 {
Vec3::new(0.0, -self.half_height * 0.5, 0.0)
}
fn inertia_tensor(&self, mass: Real) -> Mat3 {
let r2 = self.radius * self.radius;
let h2 = (2.0 * self.half_height).powi(2);
let iy = 3.0 * mass * r2 / 10.0;
let ixz = mass * (3.0 * r2 + 2.0 * h2) / 20.0;
Mat3::new(ixz, 0.0, 0.0, 0.0, iy, 0.0, 0.0, 0.0, ixz)
}
fn ray_cast(&self, ray_origin: &Vec3, ray_direction: &Vec3, max_toi: Real) -> Option<RayHit> {
let h = 2.0 * self.half_height;
let k = self.radius / h;
let k2 = k * k;
let apex_y = self.half_height;
let ox = ray_origin.x;
let oy = ray_origin.y;
let oz = ray_origin.z;
let dx = ray_direction.x;
let dy = ray_direction.y;
let dz = ray_direction.z;
let fy = apex_y - oy;
let a = dx * dx + dz * dz - k2 * dy * dy;
let b = 2.0 * (ox * dx + oz * dz + k2 * fy * dy);
let c_val = ox * ox + oz * oz - k2 * fy * fy;
let mut best: Option<RayHit> = None;
let try_t = |t: Real, best: &mut Option<RayHit>| {
if t < 0.0 || t > max_toi {
return;
}
let p = ray_origin + ray_direction * t;
if p.y < -self.half_height || p.y > self.half_height {
return;
}
let xz_len = (p.x * p.x + p.z * p.z).sqrt();
let normal = if xz_len > 1e-12 {
let slope = self.radius / h;
Vec3::new(p.x / xz_len, slope, p.z / xz_len).normalize()
} else {
Vec3::new(0.0, 1.0, 0.0)
};
if best.as_ref().is_none_or(|prev| t < prev.toi) {
*best = Some(RayHit {
point: p,
normal,
toi: t,
});
}
};
if a.abs() > 1e-12 {
let disc = b * b - 4.0 * a * c_val;
if disc >= 0.0 {
let sqrt_disc = disc.sqrt();
try_t((-b - sqrt_disc) / (2.0 * a), &mut best);
try_t((-b + sqrt_disc) / (2.0 * a), &mut best);
}
} else if b.abs() > 1e-12 {
try_t(-c_val / b, &mut best);
}
if ray_direction.y.abs() > 1e-12 {
let t = (-self.half_height - ray_origin.y) / ray_direction.y;
if t >= 0.0 && t <= max_toi {
let p = ray_origin + ray_direction * t;
if p.x * p.x + p.z * p.z <= self.radius * self.radius {
let normal = Vec3::new(0.0, -1.0, 0.0);
if best.as_ref().is_none_or(|prev| t < prev.toi) {
best = Some(RayHit {
point: p,
normal,
toi: t,
});
}
}
}
}
best
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cone_volume() {
let c = Cone::new(1.0, 1.5);
let expected = PI * 1.0 * 3.0 / 3.0;
assert!((c.volume() - expected).abs() < 1e-10);
}
#[test]
fn test_cone_surface_area() {
let c = Cone::new(1.0, 1.0);
let slant = (1.0_f64 + 4.0_f64).sqrt();
let expected = PI + PI * slant;
assert!((c.surface_area() - expected).abs() < 1e-10);
}
#[test]
fn test_cone_support_apex() {
let c = Cone::new(1.0, 2.0);
let sp = c.support_point(&Vec3::new(0.0, 1.0, 0.0));
assert!((sp.y - 2.0).abs() < 1e-10);
}
#[test]
fn test_cone_inertia_symmetry() {
let c = Cone::new(1.5, 2.0);
let it = c.inertia_tensor(3.0);
let diff = (it[(0, 0)] - it[(2, 2)]).abs();
assert!(
diff < 1e-10,
"I_xx != I_zz: {} vs {}",
it[(0, 0)],
it[(2, 2)]
);
assert!(it[(0, 1)].abs() < 1e-10);
assert!(it[(1, 0)].abs() < 1e-10);
}
#[test]
fn test_cone_inertia_array() {
let c = Cone::new(1.0, 1.0);
let it = c.inertia_tensor_array(1.0);
assert!(it[0][0] > 0.0);
assert!(it[1][1] > 0.0);
assert!((it[0][0] - it[2][2]).abs() < 1e-10);
}
#[test]
fn test_cone_raycast_side() {
let c = Cone::new(1.0, 2.0); let origin = Vec3::new(-5.0, 0.0, 0.0);
let dir = Vec3::new(1.0, 0.0, 0.0);
let hit = c.ray_cast(&origin, &dir, 100.0);
assert!(hit.is_some(), "expected a hit on cone side");
let hit = hit.unwrap();
assert!(hit.toi > 0.0);
assert!(
(hit.point.x + 0.5).abs() < 1e-6,
"expected hit at x=-0.5, got x={}",
hit.point.x
);
}
#[test]
fn test_cone_raycast_base_cap() {
let c = Cone::new(1.0, 1.0); let origin = Vec3::new(0.0, -5.0, 0.0);
let dir = Vec3::new(0.0, 1.0, 0.0);
let hit = c.ray_cast(&origin, &dir, 100.0);
assert!(hit.is_some(), "expected a hit on cone base");
let hit = hit.unwrap();
assert!(
(hit.toi - 4.0).abs() < 1e-6,
"expected toi=4, got {}",
hit.toi
);
assert!((hit.normal.y + 1.0).abs() < 1e-10);
}
#[test]
fn test_cone_closest_point_outside() {
let c = Cone::new(1.0, 1.0); let cp = c.closest_point([5.0, 0.0, 0.0]);
assert!(
(cp[0] - 0.5).abs() < 1e-6,
"expected cp[0]=0.5, got {}",
cp[0]
);
assert!((cp[1]).abs() < 1e-6);
}
#[test]
fn test_cone_support_array_apex() {
let c = Cone::new(1.0, 2.0);
let sp = c.support([0.0, 1.0, 0.0]);
assert!((sp[1] - 2.0).abs() < 1e-10);
}
#[test]
fn test_cone_support_array_base_rim() {
let c = Cone::new(1.0, 2.0);
let sp = c.support([1.0, -1.0, 0.0]);
assert!((sp[0] - 1.0).abs() < 1e-10);
assert!((sp[1] + 2.0).abs() < 1e-10);
}
#[test]
fn test_cone_slant_height() {
let c = Cone::new(3.0, 2.0); let expected = (9.0 + 16.0_f64).sqrt(); assert!((c.slant_height() - expected).abs() < 1e-10);
}
#[test]
fn test_cone_half_angle() {
let c = Cone::new(1.0, 1.0); let expected = (0.5_f64).atan(); assert!((c.half_angle() - expected).abs() < 1e-10);
}
#[test]
fn test_cone_lateral_surface_area() {
let c = Cone::new(1.0, 1.0);
let slant = c.slant_height();
let expected = PI * 1.0 * slant;
assert!((c.lateral_surface_area() - expected).abs() < 1e-10);
}
#[test]
fn test_cone_base_area() {
let c = Cone::new(2.0, 1.0);
assert!((c.base_area() - 4.0 * PI).abs() < 1e-10);
}
#[test]
fn test_cone_surface_area_decomposition() {
let c = Cone::new(1.5, 2.0);
let total = c.surface_area();
let decomposed = c.lateral_surface_area() + c.base_area();
assert!((total - decomposed).abs() < 1e-10);
}
#[test]
fn test_cone_contains_point() {
let c = Cone::new(1.0, 1.0); assert!(c.contains_point([0.0, 0.0, 0.0])); assert!(c.contains_point([0.0, -1.0, 0.0])); assert!(!c.contains_point([0.0, 2.0, 0.0])); assert!(!c.contains_point([1.0, 0.0, 0.0])); }
#[test]
fn test_cone_contains_point_base_rim() {
let c = Cone::new(1.0, 1.0);
assert!(c.contains_point([1.0, -1.0, 0.0]));
assert!(!c.contains_point([1.1, -1.0, 0.0]));
}
#[test]
fn test_cone_signed_distance_inside() {
let c = Cone::new(2.0, 2.0);
let d = c.signed_distance([0.0, -2.0, 0.0]); assert!(d <= 0.0);
}
#[test]
fn test_cone_signed_distance_outside() {
let c = Cone::new(1.0, 1.0);
let d = c.signed_distance([5.0, 0.0, 0.0]);
assert!(d > 0.0);
}
#[test]
fn test_cone_radius_at_y() {
let c = Cone::new(2.0, 2.0); assert!((c.radius_at_y(-2.0) - 2.0).abs() < 1e-10); assert!((c.radius_at_y(2.0)).abs() < 1e-10); assert!((c.radius_at_y(0.0) - 1.0).abs() < 1e-10); }
#[test]
fn test_cone_truncated_cone() {
let c = Cone::new(2.0, 2.0);
let (top_r, bot_r, hh) = c.truncated_cone(0.5);
assert!((bot_r - 2.0).abs() < 1e-10);
assert!((top_r - 1.0).abs() < 1e-10); assert!((hh - 1.0).abs() < 1e-10); }
#[test]
fn test_cone_truncated_cone_full() {
let c = Cone::new(2.0, 2.0);
let (top_r, _, _) = c.truncated_cone(1.0);
assert!(top_r.abs() < 1e-10); }
#[test]
fn test_cone_truncated_cone_volume() {
let c = Cone::new(1.0, 1.0);
let full_vol = c.volume();
let trunc_vol = c.truncated_cone_volume(1.0);
assert!((trunc_vol - full_vol).abs() < 1e-10);
}
#[test]
fn test_cone_ray_cast_base_cap_array() {
let c = Cone::new(1.0, 1.0);
let result = c.ray_cast_base_cap([0.0, -5.0, 0.0], [0.0, 1.0, 0.0], 100.0);
assert!(result.is_some());
let (t, n) = result.unwrap();
assert!((t - 4.0).abs() < 1e-10);
assert!((n[1] + 1.0).abs() < 1e-10);
}
#[test]
fn test_cone_ray_cast_base_cap_miss() {
let c = Cone::new(1.0, 1.0);
let result = c.ray_cast_base_cap([5.0, -5.0, 0.0], [0.0, 1.0, 0.0], 100.0);
assert!(result.is_none());
}
#[test]
fn test_cone_closest_point_on_lateral() {
let c = Cone::new(1.0, 1.0);
let cp = c.closest_point_on_lateral([5.0, 0.0, 0.0]);
assert!(cp[0] > 0.0);
let r = (cp[0] * cp[0] + cp[2] * cp[2]).sqrt();
let expected_r = c.radius_at_y(cp[1]);
assert!((r - expected_r).abs() < 0.1);
}
#[test]
fn test_cone_closest_point_on_lateral_apex() {
let c = Cone::new(1.0, 1.0);
let cp = c.closest_point_on_lateral([0.0, 5.0, 0.0]);
assert!((cp[1] - 1.0).abs() < 1e-10);
assert!(cp[0].abs() < 1e-10);
}
#[test]
fn test_cone_volume_explicit_matches() {
let c = Cone::new(1.5, 2.5);
assert!((c.volume_explicit() - c.volume()).abs() < 1e-10);
}
#[test]
fn test_cone_ray_analytic_lateral() {
let c = Cone::new(1.0, 2.0);
let result = c.ray_cone_analytic([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0);
assert!(result.is_some(), "should hit lateral surface");
let (t, n, is_lateral) = result.unwrap();
assert!(t > 0.0);
assert!(is_lateral, "should be lateral hit");
assert!(
n[1] > 0.0,
"normal should have positive Y component for lateral cone"
);
}
#[test]
fn test_cone_ray_analytic_base() {
let c = Cone::new(1.0, 1.0);
let result = c.ray_cone_analytic([0.0, -5.0, 0.0], [0.0, 1.0, 0.0], 100.0);
assert!(result.is_some());
let (t, n, is_lateral) = result.unwrap();
assert!((t - 4.0).abs() < 1e-6, "toi should be 4, got {t}");
assert!(!is_lateral);
assert!((n[1] + 1.0).abs() < 1e-10);
}
#[test]
fn test_cone_plane_intersection_horizontal_through_base() {
let c = Cone::new(1.0, 1.0);
let hit = c.intersects_plane([0.0, 1.0, 0.0], -1.0);
assert!(hit, "plane through base should intersect cone");
}
#[test]
fn test_cone_plane_intersection_above_apex() {
let c = Cone::new(1.0, 1.0);
let hit = c.intersects_plane([0.0, 1.0, 0.0], 5.0);
assert!(!hit, "plane far above should not intersect cone");
}
#[test]
fn test_cone_frustum_from_cone() {
let c = Cone::new(2.0, 2.0); let (r_bot, r_top, hh) = c.frustum_from_cone(-1.0, 1.0);
assert!((r_bot - 1.5).abs() < 1e-10, "r_bot={r_bot}");
assert!((r_top - 0.5).abs() < 1e-10, "r_top={r_top}");
assert!((hh - 1.0).abs() < 1e-10, "hh={hh}");
}
#[test]
fn test_cone_sdf_matches_signed_distance() {
let c = Cone::new(1.0, 1.0);
let p = [3.0, 0.0, 0.0];
assert!((c.sdf(p) - c.signed_distance(p)).abs() < 1e-12);
}
#[test]
fn test_cone_intersects_sphere_overlap() {
let c = Cone::new(1.0, 1.0);
assert!(c.intersects_sphere([0.0, 0.0, 0.0], 5.0));
}
#[test]
fn test_cone_intersects_sphere_no_overlap() {
let c = Cone::new(1.0, 1.0);
assert!(!c.intersects_sphere([10.0, 0.0, 0.0], 0.1));
}
#[test]
fn test_cone_frustum_lateral_area_full_cone() {
let c = Cone::new(1.0, 1.0); let area = c.frustum_lateral_area(-1.0, 1.0);
let expected = c.lateral_surface_area();
assert!(
(area - expected).abs() < 1e-9,
"frustum area {area} vs lateral {expected}"
);
}
#[test]
fn test_cone_random_lateral_surface_point_on_surface() {
let c = Cone::new(1.0, 2.0);
for seed in [1u64, 42, 99, 1337, 65535] {
let p = c.random_lateral_surface_point(seed);
assert!(
p[1] >= -c.half_height - 1e-9 && p[1] <= c.half_height + 1e-9,
"y={} out of range",
p[1]
);
let xz = (p[0] * p[0] + p[2] * p[2]).sqrt();
let r_at = c.radius_at_y(p[1]);
assert!((xz - r_at).abs() < 1e-9, "xz={xz}, r_at={r_at}");
}
}
#[test]
fn test_cone_ray_analytic_miss() {
let c = Cone::new(1.0, 1.0);
let result = c.ray_cone_analytic([0.0, 10.0, 0.0], [0.0, 1.0, 0.0], 5.0);
assert!(result.is_none(), "ray going away should miss");
}
#[test]
fn test_cone_apex_angle() {
let c = Cone::new(1.0, 1.0); let expected = 2.0 * (0.5_f64).atan();
assert!((c.apex_angle() - expected).abs() < 1e-10);
}
#[test]
fn test_cone_apex_angle_90_degrees() {
let c = Cone::new(1.0, 0.5); let expected = PI / 2.0;
assert!((c.apex_angle() - expected).abs() < 1e-10);
}
#[test]
fn test_cone_double_cone_params() {
let c = Cone::new(1.5, 2.0);
let (r, hh) = c.double_cone_params();
assert!((r - 1.5).abs() < 1e-12);
assert!((hh - 2.0).abs() < 1e-12);
}
#[test]
fn test_cone_contains_point_double_cone_at_origin() {
let c = Cone::new(1.0, 1.0);
assert!(c.contains_point_double_cone([0.0, 0.0, 0.0]));
}
#[test]
fn test_cone_contains_point_double_cone_above_apex() {
let c = Cone::new(1.0, 1.0);
assert!(!c.contains_point_double_cone([0.0, 1.5, 0.0]));
}
#[test]
fn test_cone_contains_point_double_cone_on_surface() {
let c = Cone::new(1.0, 1.0);
assert!(c.contains_point_double_cone([1.0, 1.0, 0.0]));
}
#[test]
fn test_cone_unfold_lateral_origin() {
let c = Cone::new(1.0, 1.0);
let [x, y] = c.unfold_lateral(0.0, 0.0);
assert!(x.abs() < 1e-12);
assert!(y.abs() < 1e-12);
}
#[test]
fn test_cone_unfold_lateral_nonzero() {
let c = Cone::new(1.0, 1.0);
let [x, y] = c.unfold_lateral(0.0, 1.0);
let len = (x * x + y * y).sqrt();
assert!((len - 1.0).abs() < 1e-10, "len={len}");
}
#[test]
fn test_cone_bounding_cone_of_points_empty() {
let (ha, _) = Cone::bounding_cone_of_points(&[]);
assert_eq!(ha, 0.0);
}
#[test]
fn test_cone_bounding_cone_of_points_single() {
let pts = [[1.0, 1.0, 0.0]];
let (ha, _) = Cone::bounding_cone_of_points(&pts);
let expected = PI / 4.0; assert!((ha - expected).abs() < 1e-9, "ha={ha} expected={expected}");
}
#[test]
fn test_cone_bounding_cone_contains_all_points() {
let pts = [[0.1, 1.0, 0.0], [0.0, 1.0, 0.1], [0.0, 1.0, -0.1]];
let (max_ha, _) = Cone::bounding_cone_of_points(&pts);
for &p in &pts {
let len = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
let cos_a = (p[1] / len).clamp(-1.0, 1.0);
let ha = cos_a.acos();
assert!(ha <= max_ha + 1e-12, "ha={ha} > max={max_ha}");
}
}
#[test]
fn test_cone_volume_swept_zero_delta() {
let c = Cone::new(1.0, 1.0);
let swept = c.volume_swept([0.0, 0.0, 0.0]);
assert!((swept - c.volume()).abs() < 1e-10);
}
#[test]
fn test_cone_volume_swept_positive_delta() {
let c = Cone::new(1.0, 1.0);
let swept = c.volume_swept([1.0, 0.0, 0.0]);
assert!(swept > c.volume());
assert!((swept - c.volume() - PI).abs() < 1e-10);
}
#[test]
fn test_cone_sdf_gradient_outside_outward() {
let c = Cone::new(1.0, 1.0);
let g = c.sdf_gradient([5.0, 0.0, 0.0]);
assert!(g[0] > 0.0, "gx={}", g[0]);
}
#[test]
fn test_cone_sdf_gradient_unit_length() {
let c = Cone::new(1.0, 1.0);
let g = c.sdf_gradient([3.0, 0.0, 0.0]);
let len = (g[0] * g[0] + g[1] * g[1] + g[2] * g[2]).sqrt();
assert!((len - 1.0).abs() < 1e-6, "gradient not unit: len={len}");
}
#[test]
fn test_cone_ray_intersection_count_lateral() {
let c = Cone::new(1.0, 2.0);
let count = c.ray_intersection_count([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0);
assert!(count >= 1, "expected >=1 hit, got {count}");
}
#[test]
fn test_cone_ray_intersection_count_miss() {
let c = Cone::new(1.0, 1.0);
let count = c.ray_intersection_count([0.0, 10.0, 0.0], [0.0, 1.0, 0.0], 5.0);
assert_eq!(count, 0);
}
#[test]
fn test_cone_solid_angle_full_sphere() {
let c = Cone::new(1.0, 0.5); let sa = c.solid_angle();
let expected = 2.0 * PI * (1.0 - (PI / 4.0).cos());
assert!((sa - expected).abs() < 1e-10);
}
#[test]
fn test_cone_solid_angle_positive() {
let c = Cone::new(1.0, 1.0);
assert!(c.solid_angle() > 0.0);
}
#[test]
fn test_cone_base_rim_points_count() {
let c = Cone::new(1.0, 2.0);
let pts = c.base_rim_points(12);
assert_eq!(pts.len(), 12);
}
#[test]
fn test_cone_base_rim_points_on_base() {
let c = Cone::new(1.5, 2.0);
let pts = c.base_rim_points(16);
for p in &pts {
assert!((p[1] + c.half_height).abs() < 1e-12);
let xz = (p[0] * p[0] + p[2] * p[2]).sqrt();
assert!((xz - c.radius).abs() < 1e-9);
}
}
#[test]
fn test_cone_sphere_inside_cone_yes() {
let c = Cone::new(2.0, 2.0);
assert!(c.sphere_inside_cone([0.0, -1.9, 0.0], 0.05));
}
#[test]
fn test_cone_sphere_inside_cone_no() {
let c = Cone::new(1.0, 1.0);
assert!(!c.sphere_inside_cone([5.0, 0.0, 0.0], 1.0));
}
#[test]
fn test_cone_unfold_lateral_angle_scaling() {
let c = Cone::new(1.0, 1.0);
let p1 = c.unfold_lateral(0.0, 2.0);
let p2 = c.unfold_lateral(PI / 2.0, 2.0);
let d = ((p1[0] - p2[0]).powi(2) + (p1[1] - p2[1]).powi(2)).sqrt();
assert!(d > 0.1, "unrolled points should differ");
}
#[test]
fn test_cone_double_cone_waist_radius_zero() {
let c = Cone::new(2.0, 1.0);
assert!(c.contains_point_double_cone([0.0, 0.0, 0.0]));
}
}