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 Torus {
pub major_radius: Real,
pub minor_radius: Real,
}
impl Torus {
pub fn new(major_radius: Real, minor_radius: Real) -> Self {
Self {
major_radius,
minor_radius,
}
}
pub fn volume(&self) -> Real {
2.0 * PI * PI * self.major_radius * self.minor_radius * self.minor_radius
}
pub fn surface_area(&self) -> Real {
4.0 * PI * PI * self.major_radius * self.minor_radius
}
pub fn bounding_box_extents(&self) -> Vec3 {
let outer = self.major_radius + self.minor_radius;
Vec3::new(outer, self.minor_radius, outer)
}
pub fn inertia_tensor_array(&self, mass: Real) -> [[f64; 3]; 3] {
let r = self.major_radius;
let a = self.minor_radius;
let iy = mass * (r * r + (3.0 / 4.0) * a * a);
let ixz = mass * ((5.0 / 8.0) * a * a + r * r / 2.0);
[[ixz, 0.0, 0.0], [0.0, iy, 0.0], [0.0, 0.0, ixz]]
}
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_impl(&o, &d, max_toi)?;
Some((hit.toi, [hit.normal.x, hit.normal.y, hit.normal.z]))
}
pub fn closest_point(&self, p: [f64; 3]) -> [f64; 3] {
let px = p[0];
let py = p[1];
let pz = p[2];
let xz_len = (px * px + pz * pz).sqrt();
let (ring_x, ring_z) = if xz_len < 1e-12 {
(self.major_radius, 0.0)
} else {
let s = self.major_radius / xz_len;
(px * s, pz * s)
};
let dx = px - ring_x;
let dy = py;
let dz = pz - ring_z;
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
if dist < 1e-12 {
return [ring_x, self.minor_radius, ring_z];
}
let s = self.minor_radius / dist;
[ring_x + dx * s, dy * s, ring_z + dz * s]
}
pub fn contains_point(&self, p: [f64; 3]) -> bool {
let px = p[0];
let py = p[1];
let pz = p[2];
let xz = (px * px + pz * pz).sqrt();
let dist_to_ring = ((xz - self.major_radius) * (xz - self.major_radius) + py * py).sqrt();
dist_to_ring <= self.minor_radius
}
pub fn sample_surface(&self, u: f64, v: f64) -> [f64; 3] {
let r_outer = self.major_radius + self.minor_radius * v.cos();
[
r_outer * u.cos(),
self.minor_radius * v.sin(),
r_outer * u.sin(),
]
}
pub fn sdf(&self, p: [f64; 3]) -> f64 {
let xz = (p[0] * p[0] + p[2] * p[2]).sqrt();
let dist_to_ring = ((xz - self.major_radius).powi(2) + p[1] * p[1]).sqrt();
dist_to_ring - self.minor_radius
}
pub fn ray_torus_analytic(
&self,
origin: [f64; 3],
direction: [f64; 3],
max_toi: f64,
) -> Option<(f64, [f64; 3])> {
self.ray_cast_array(origin, direction, max_toi)
}
pub fn support_array(&self, direction: [f64; 3]) -> [f64; 3] {
let d = Vec3::new(direction[0], direction[1], direction[2]);
let sp = self.support_point(&d);
[sp.x, sp.y, sp.z]
}
pub fn surface_parameters(&self, p: [f64; 3]) -> (f64, f64) {
let xz = (p[0] * p[0] + p[2] * p[2]).sqrt();
let u = p[2].atan2(p[0]);
let rx = self.major_radius * u.cos();
let rz = self.major_radius * u.sin();
let dx = p[0] - rx;
let dy = p[1];
let dz = p[2] - rz;
let radial = xz - self.major_radius;
let v = dy.atan2(radial);
let _ = (dx, dz); (u, v)
}
pub fn random_surface_points(&self, n: usize, seed: u64) -> Vec<[f64; 3]> {
let mut points = Vec::with_capacity(n);
let mut state = seed;
let next_f64 = |s: &mut u64| -> f64 {
*s ^= *s << 13;
*s ^= *s >> 7;
*s ^= *s << 17;
(*s as f64) / (u64::MAX as f64)
};
let max_weight = self.major_radius + self.minor_radius;
while points.len() < n {
let u = next_f64(&mut state) * 2.0 * PI;
let v = next_f64(&mut state) * 2.0 * PI;
let w = next_f64(&mut state);
let weight = (self.major_radius + self.minor_radius * v.cos()) / max_weight;
if w <= weight {
points.push(self.sample_surface(u, v));
}
}
points
}
pub fn inertia_raw(&self, mass: f64) -> [[f64; 3]; 3] {
self.inertia_tensor_array(mass)
}
pub fn outer_radius(&self) -> f64 {
self.major_radius + self.minor_radius
}
pub fn inner_radius(&self) -> f64 {
(self.major_radius - self.minor_radius).max(0.0)
}
pub fn uv_map(&self, p: [f64; 3]) -> [f64; 2] {
let (theta, phi) = self.surface_parameters(p);
let u = ((theta / (2.0 * PI)) + 1.0) % 1.0;
let v = ((phi / (2.0 * PI)) + 1.0) % 1.0;
[u, v]
}
pub fn geodesic_distance_flat(&self, a: [f64; 3], b: [f64; 3]) -> f64 {
let (ua, va) = self.surface_parameters(a);
let (ub, vb) = self.surface_parameters(b);
let d_theta = angle_diff(ua, ub);
let d_phi = angle_diff(va, vb);
let arc_major = self.major_radius * d_theta;
let arc_minor = self.minor_radius * d_phi;
(arc_major * arc_major + arc_minor * arc_minor).sqrt()
}
pub fn area_element_factor(&self, v: f64) -> f64 {
self.major_radius + self.minor_radius * v.cos()
}
pub fn surface_area_numeric(&self, n_steps: usize) -> f64 {
let n = n_steps.max(4);
let du = 2.0 * PI / n as f64;
let dv = 2.0 * PI / n as f64;
let mut sum = 0.0;
for iv in 0..n {
let v = (iv as f64 + 0.5) * dv;
let factor = self.area_element_factor(v);
sum += factor * n as f64; }
sum * self.minor_radius * du * dv
}
pub fn tube_cross_section(&self, u: f64, n: usize) -> Vec<[f64; 3]> {
let n = n.max(3);
let ring_x = self.major_radius * u.cos();
let ring_z = self.major_radius * u.sin();
(0..n)
.map(|i| {
let v = 2.0 * PI * i as f64 / n as f64;
let rx = u.cos();
let rz = u.sin();
let cx = ring_x + self.minor_radius * v.cos() * rx;
let cy = self.minor_radius * v.sin();
let cz = ring_z + self.minor_radius * v.cos() * rz;
[cx, cy, cz]
})
.collect()
}
pub fn torus_knot_path(&self, p: i32, q: i32, n_pts: usize) -> Vec<[f64; 3]> {
let n = n_pts.max(3);
(0..n)
.map(|i| {
let t = 2.0 * PI * i as f64 / n as f64;
let u = p as f64 * t;
let v = q as f64 * t;
self.sample_surface(u, v)
})
.collect()
}
pub fn winding_number_major(&self, curve: &[[f64; 3]]) -> i32 {
if curve.len() < 2 {
return 0;
}
let angles: Vec<f64> = curve
.iter()
.map(|&p| self.surface_parameters(p).0)
.collect();
let mut total = 0.0_f64;
for i in 0..angles.len() {
let next = (i + 1) % angles.len();
let diff = angle_diff(angles[i], angles[next]);
total += diff;
}
(total / (2.0 * PI)).round() as i32
}
pub fn tangent_u(&self, u: f64, v: f64) -> [f64; 3] {
let r_outer = self.major_radius + self.minor_radius * v.cos();
[-r_outer * u.sin(), 0.0, r_outer * u.cos()]
}
pub fn tangent_v(&self, u: f64, v: f64) -> [f64; 3] {
let r = self.minor_radius;
[-r * v.sin() * u.cos(), r * v.cos(), -r * v.sin() * u.sin()]
}
pub fn normal_from_tangents(&self, u: f64, v: f64) -> [f64; 3] {
let tu = self.tangent_u(u, v);
let tv = self.tangent_v(u, v);
let n = cross3([tu[0], tu[1], tu[2]], [tv[0], tv[1], tv[2]]);
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
if len < 1e-14 {
[0.0, 1.0, 0.0]
} else {
[n[0] / len, n[1] / len, n[2] / len]
}
}
pub fn ray_intersection_count(
&self,
origin: [f64; 3],
direction: [f64; 3],
max_toi: f64,
) -> usize {
let o = Vec3::new(origin[0], origin[1], origin[2]);
let d = Vec3::new(direction[0], direction[1], direction[2]);
let dir_len = d.norm();
if dir_len < 1e-12 {
return 0;
}
let dn = d / dir_len;
let oo = o.dot(&o);
let od = o.dot(&dn);
let rr = self.major_radius * self.major_radius;
let aa = self.minor_radius * self.minor_radius;
let c_alpha = oo + rr - aa;
let beta = 2.0 * od;
let dxz2 = dn.x * dn.x + dn.z * dn.z;
let od_xz = o.x * dn.x + o.z * dn.z;
let oxz2 = o.x * o.x + o.z * o.z;
let c4 = 1.0;
let c3 = 2.0 * beta;
let c2 = beta * beta + 2.0 * c_alpha - 4.0 * rr * dxz2;
let c1 = 2.0 * beta * c_alpha - 8.0 * rr * od_xz;
let c0 = c_alpha * c_alpha - 4.0 * rr * oxz2;
let roots = solve_quartic(c4, c3, c2, c1, c0);
roots
.iter()
.filter(|&&t| {
!t.is_nan() && !t.is_infinite() && {
let t_actual = t / dir_len;
t_actual >= 1e-6 && t_actual <= max_toi
}
})
.count()
}
pub fn intersects_aabb(&self, aabb_min: [f64; 3], aabb_max: [f64; 3]) -> bool {
let corners = [
[aabb_min[0], aabb_min[1], aabb_min[2]],
[aabb_max[0], aabb_min[1], aabb_min[2]],
[aabb_min[0], aabb_max[1], aabb_min[2]],
[aabb_max[0], aabb_max[1], aabb_min[2]],
[aabb_min[0], aabb_min[1], aabb_max[2]],
[aabb_max[0], aabb_min[1], aabb_max[2]],
[aabb_min[0], aabb_max[1], aabb_max[2]],
[aabb_max[0], aabb_max[1], aabb_max[2]],
];
let sdfs: Vec<f64> = corners.iter().map(|&c| self.sdf(c)).collect();
let min_sdf = sdfs.iter().cloned().fold(f64::INFINITY, f64::min);
min_sdf <= 0.0
}
pub fn aspect_ratio(&self) -> f64 {
self.major_radius / self.minor_radius.max(1e-30)
}
pub fn major_circle_points(&self, n: usize) -> Vec<[f64; 3]> {
let n = n.max(2);
(0..n)
.map(|i| {
let u = 2.0 * PI * i as f64 / n as f64;
[
self.major_radius * u.cos(),
0.0,
self.major_radius * u.sin(),
]
})
.collect()
}
fn ray_cast_impl(&self, origin: &Vec3, dir: &Vec3, max_toi: Real) -> Option<RayHit> {
let r_big = self.major_radius;
let r_small = self.minor_radius;
let dir_len = dir.norm();
if dir_len < 1e-12 {
return None;
}
let d = dir / dir_len;
let o = *origin;
let oo = o.dot(&o);
let od = o.dot(&d);
let _dd = d.dot(&d); let rr = r_big * r_big;
let aa = r_small * r_small;
let alpha = oo - rr - aa; let beta = 2.0 * od;
let dxz2 = d.x * d.x + d.z * d.z;
let od_xz = o.x * d.x + o.z * d.z;
let oxz2 = o.x * o.x + o.z * o.z;
let c_alpha = oo + rr - aa; let c4 = 1.0;
let c3 = 2.0 * beta;
let c2 = beta * beta + 2.0 * c_alpha - 4.0 * rr * dxz2;
let c1 = 2.0 * beta * c_alpha - 8.0 * rr * od_xz;
let c0 = c_alpha * c_alpha - 4.0 * rr * oxz2;
let _ = alpha;
let roots = solve_quartic(c4, c3, c2, c1, c0);
let mut best_t = Real::INFINITY;
for t_raw in roots {
if t_raw.is_nan() || t_raw.is_infinite() {
continue;
}
let t = t_raw / dir_len;
if t >= 1e-6 && t <= max_toi && t < best_t {
best_t = t;
}
}
if best_t.is_infinite() {
return None;
}
let point = o + d * (best_t * dir_len);
let normal = torus_normal(&point, r_big);
Some(RayHit {
point,
normal,
toi: best_t,
})
}
}
fn angle_diff(a: f64, b: f64) -> f64 {
let d = b - a;
d - (2.0 * PI) * ((d + PI) / (2.0 * PI)).floor()
}
fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn torus_normal(p: &Vec3, big_r: Real) -> Vec3 {
let xz = (p.x * p.x + p.z * p.z).sqrt();
if xz < 1e-12 {
return Vec3::new(0.0, 1.0, 0.0);
}
let s = big_r / xz;
let ring = Vec3::new(p.x * s, 0.0, p.z * s);
let n = p - ring;
let len = n.norm();
if len < 1e-12 {
Vec3::new(p.x / xz, 0.0, p.z / xz)
} else {
n / len
}
}
fn solve_quartic(c4: f64, c3: f64, c2: f64, c1: f64, c0: f64) -> [f64; 4] {
let a = c3 / c4;
let b = c2 / c4;
let c = c1 / c4;
let d = c0 / c4;
let p_depr = b - (3.0 * a * a) / 8.0;
let q_depr = (a * a * a) / 8.0 - (a * b) / 2.0 + c;
let r_depr = -(3.0 * a * a * a * a) / 256.0 + (a * a * b) / 16.0 - (a * c) / 4.0 + d;
let shift = a / 4.0;
let (m1, m2, m3) = solve_cubic_3roots(
8.0,
8.0 * p_depr,
2.0 * p_depr * p_depr - 8.0 * r_depr,
-(q_depr * q_depr),
);
let mut roots = [f64::NAN; 4];
let mut ri = 0usize;
for m in [m1, m2, m3] {
if m.is_nan() {
continue;
}
let two_m_p = 2.0 * m + p_depr;
if two_m_p < 0.0 {
continue;
}
let sqrt_2mp = two_m_p.sqrt();
if sqrt_2mp < 1e-14 {
continue;
}
let half_q_over = q_depr / (2.0 * sqrt_2mp);
for &(s, c_q) in &[(1.0_f64, m + half_q_over), (-1.0_f64, m - half_q_over)] {
let disc = (s * sqrt_2mp) * (s * sqrt_2mp) / 4.0 - c_q;
let disc2 = sqrt_2mp * sqrt_2mp / 4.0 - c_q;
if disc2 >= -1e-9 {
let sd = disc2.max(0.0).sqrt();
let half_b = s * sqrt_2mp / 2.0;
let r0 = -half_b + sd - shift;
let r1 = -half_b - sd - shift;
if ri < 4 {
roots[ri] = r0;
ri += 1;
}
if ri < 4 {
roots[ri] = r1;
ri += 1;
}
}
let _ = disc; }
if ri >= 4 {
break;
}
}
roots
}
fn solve_cubic_3roots(a: f64, b: f64, c: f64, d: f64) -> (f64, f64, f64) {
let b = b / a;
let c = c / a;
let d = d / a;
let p = c - b * b / 3.0;
let q = 2.0 * b * b * b / 27.0 - b * c / 3.0 + d;
let shift = b / 3.0;
let discriminant = -(4.0 * p * p * p + 27.0 * q * q);
if discriminant >= 0.0 {
let m = 2.0 * (-p / 3.0).sqrt();
let theta = ((3.0 * q) / (2.0 * p) * ((-3.0) / p).sqrt())
.clamp(-1.0, 1.0)
.acos();
let r0 = m * (theta / 3.0).cos() - shift;
let r1 = m * ((theta - 2.0 * PI) / 3.0).cos() - shift;
let r2 = m * ((theta - 4.0 * PI) / 3.0).cos() - shift;
(r0, r1, r2)
} else {
let d_val = -(4.0 * p * p * p + 27.0 * q * q);
let inner = -q / 2.0;
let outer = (q * q / 4.0 + p * p * p / 27.0).sqrt();
let u = cbrt_real(inner + outer);
let v = cbrt_real(inner - outer);
let r0 = u + v - shift;
let _ = d_val; (r0, f64::NAN, f64::NAN)
}
}
fn cbrt_real(x: f64) -> f64 {
if x >= 0.0 { x.cbrt() } else { -((-x).cbrt()) }
}
impl Shape for Torus {
fn bounding_box(&self) -> Aabb {
let half = self.bounding_box_extents();
Aabb::new(-half, half)
}
fn support_point(&self, direction: &Vec3) -> Vec3 {
let eps = 1e-10;
let dir_xz = Vec3::new(direction.x, 0.0, direction.z);
let xz_len = dir_xz.norm();
let xz_norm = if xz_len < eps {
Vec3::new(1.0, 0.0, 0.0)
} else {
dir_xz / xz_len
};
let tube_center = xz_norm * self.major_radius;
let dir_len = direction.norm();
let dir_norm = if dir_len < eps {
Vec3::new(1.0, 0.0, 0.0)
} else {
direction / dir_len
};
tube_center + dir_norm * self.minor_radius
}
fn volume(&self) -> Real {
self.volume()
}
fn center_of_mass(&self) -> Vec3 {
Vec3::zeros()
}
fn inertia_tensor(&self, mass: Real) -> Mat3 {
let r = self.major_radius;
let a = self.minor_radius;
let iy = mass * (r * r + (3.0 / 4.0) * a * a);
let ixz = mass * ((5.0 / 8.0) * a * a + r * r / 2.0);
Mat3::new(ixz, 0.0, 0.0, 0.0, iy, 0.0, 0.0, 0.0, ixz)
}
fn mass_properties(&self, density: Real) -> oxiphysics_core::MassProperties {
let mass = density * self.volume();
oxiphysics_core::MassProperties::new(mass, self.center_of_mass(), self.inertia_tensor(mass))
}
fn ray_cast(&self, ray_origin: &Vec3, ray_direction: &Vec3, max_toi: Real) -> Option<RayHit> {
self.ray_cast_impl(ray_origin, ray_direction, max_toi)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_torus_volume() {
let t = Torus::new(1.0, 0.5);
let expected = 2.0 * PI * PI * 1.0 * 0.25;
assert!(
(t.volume() - expected).abs() < 1e-6,
"volume={}, expected={}",
t.volume(),
expected
);
assert!(
(t.volume() - 4.935).abs() < 0.01,
"volume ≈ 4.935, got {}",
t.volume()
);
}
#[test]
fn test_torus_surface_area() {
let t = Torus::new(1.0, 0.5);
let expected = 4.0 * PI * PI * 1.0 * 0.5;
assert!(
(t.surface_area() - expected).abs() < 1e-6,
"surface_area={}, expected={}",
t.surface_area(),
expected
);
assert!(
(t.surface_area() - 19.74).abs() < 0.01,
"surface_area ≈ 19.74, got {}",
t.surface_area()
);
}
#[test]
fn test_torus_bounding_box() {
let t = Torus::new(2.0, 0.5);
let extents = t.bounding_box_extents();
assert!((extents.x - 2.5).abs() < 1e-10, "extents.x={}", extents.x);
assert!((extents.y - 0.5).abs() < 1e-10, "extents.y={}", extents.y);
assert!((extents.z - 2.5).abs() < 1e-10, "extents.z={}", extents.z);
let bb = t.bounding_box();
assert!((bb.min.x + 2.5).abs() < 1e-10);
assert!((bb.max.x - 2.5).abs() < 1e-10);
assert!((bb.min.y + 0.5).abs() < 1e-10);
assert!((bb.max.y - 0.5).abs() < 1e-10);
}
#[test]
fn test_torus_support_xz() {
let t = Torus::new(1.0, 0.5);
let dir = Vec3::new(1.0, 0.0, 0.0);
let sp = t.support_point(&dir);
let expected = 1.0 + 0.5; assert!(
(sp.x - expected).abs() < 1e-10,
"support.x={}, expected={}",
sp.x,
expected
);
assert!(sp.y.abs() < 1e-10, "support.y={}", sp.y);
assert!(sp.z.abs() < 1e-10, "support.z={}", sp.z);
}
#[test]
fn test_torus_support_y() {
let t = Torus::new(1.0, 0.5);
let dir = Vec3::new(0.0, 1.0, 0.0);
let sp = t.support_point(&dir);
assert!(
(sp.y - 0.5).abs() < 1e-10,
"support.y={}, expected minor_radius=0.5",
sp.y
);
}
#[test]
fn test_torus_mass_properties() {
let t = Torus::new(1.0, 0.5);
let density = 1000.0;
let mp = t.mass_properties(density);
let expected_mass = density * t.volume();
assert!(
(mp.mass - expected_mass).abs() < 1e-6,
"mass={}, expected={}",
mp.mass,
expected_mass
);
assert!(mp.mass > 0.0, "mass should be positive");
assert!(
mp.local_inertia[(0, 0)] > 0.0,
"I_xx should be positive: {}",
mp.local_inertia[(0, 0)]
);
assert!(
mp.local_inertia[(1, 1)] > 0.0,
"I_yy should be positive: {}",
mp.local_inertia[(1, 1)]
);
assert!(
mp.local_inertia[(2, 2)] > 0.0,
"I_zz should be positive: {}",
mp.local_inertia[(2, 2)]
);
}
#[test]
fn test_torus_contains_point_inside() {
let t = Torus::new(3.0, 1.0);
assert!(
t.contains_point([3.0, 0.0, 0.0]),
"ring centre should be inside"
);
assert!(
!t.contains_point([0.0, 0.0, 0.0]),
"origin should be outside"
);
assert!(
t.contains_point([3.0, 1.0, 0.0]),
"top of tube should be inside/on"
);
assert!(
!t.contains_point([3.0, 1.1, 0.0]),
"beyond tube should be outside"
);
}
#[test]
fn test_torus_closest_point_on_surface() {
let t = Torus::new(3.0, 1.0);
let cp = t.closest_point([10.0, 0.0, 0.0]);
assert!((cp[0] - 4.0).abs() < 1e-6, "cp.x={}, expected 4.0", cp[0]);
assert!(cp[1].abs() < 1e-6, "cp.y={}", cp[1]);
assert!(cp[2].abs() < 1e-6, "cp.z={}", cp[2]);
}
#[test]
fn test_torus_sample_surface_on_torus() {
let t = Torus::new(3.0, 1.0);
let p = t.sample_surface(0.0, 0.0);
assert!((p[0] - 4.0).abs() < 1e-10, "p.x={}", p[0]);
assert!(p[1].abs() < 1e-10, "p.y={}", p[1]);
assert!(
t.contains_point(p),
"sampled point {:?} should be on torus surface",
p
);
let p2 = t.sample_surface(PI / 2.0, PI / 2.0);
assert!(p2[0].abs() < 1e-10, "p2.x={}", p2[0]);
assert!((p2[1] - 1.0).abs() < 1e-10, "p2.y={}", p2[1]);
assert!((p2[2] - 3.0).abs() < 1e-10, "p2.z={}", p2[2]);
}
#[test]
fn test_torus_inertia_tensor_array() {
let t = Torus::new(2.0, 0.5);
let it = t.inertia_tensor_array(10.0);
assert!(it[0][0] > 0.0, "Ixx={}", it[0][0]);
assert!(it[1][1] > 0.0, "Iyy={}", it[1][1]);
assert!(it[2][2] > 0.0, "Izz={}", it[2][2]);
assert!(it[0][1].abs() < 1e-12);
assert!(it[0][2].abs() < 1e-12);
assert!((it[0][0] - it[2][2]).abs() < 1e-10);
}
#[test]
fn test_torus_ray_cast_hit() {
let t = Torus::new(3.0, 1.0);
let origin = Vec3::new(3.0, -10.0, 0.0);
let dir = Vec3::new(0.0, 1.0, 0.0);
let hit = t.ray_cast(&origin, &dir, 100.0);
assert!(hit.is_some(), "ray through tube should hit");
let hit = hit.unwrap();
assert!(
hit.toi > 0.0 && hit.toi < 20.0,
"toi should be reasonable, got {}",
hit.toi
);
}
#[test]
fn test_torus_ray_cast_miss() {
let t = Torus::new(3.0, 1.0);
let origin = Vec3::new(0.0, 10.0, 0.0);
let dir = Vec3::new(0.0, 1.0, 0.0);
let hit = t.ray_cast(&origin, &dir, 5.0);
assert!(hit.is_none(), "ray going away should miss");
}
#[test]
fn test_torus_ray_cast_array() {
let t = Torus::new(3.0, 1.0);
let result = t.ray_cast_array([3.0, -10.0, 0.0], [0.0, 1.0, 0.0], 100.0);
assert!(result.is_some(), "ray should hit");
let (toi, _n) = result.unwrap();
assert!(
toi > 0.0 && toi < 20.0,
"toi should be reasonable, got {}",
toi
);
}
#[test]
fn test_torus_sdf_inside_tube() {
let t = Torus::new(3.0, 1.0);
let p = [3.0, 0.0, 0.0];
assert!(
(t.sdf(p) + 1.0).abs() < 1e-10,
"sdf at ring center should be -1, got {}",
t.sdf(p)
);
}
#[test]
fn test_torus_sdf_outside() {
let t = Torus::new(3.0, 1.0);
let p = [10.0, 0.0, 0.0];
assert!(t.sdf(p) > 0.0, "sdf outside should be positive");
}
#[test]
fn test_torus_sdf_on_surface() {
let t = Torus::new(3.0, 1.0);
let p = [4.0, 0.0, 0.0];
assert!(
t.sdf(p).abs() < 1e-10,
"sdf at surface should be ~0, got {}",
t.sdf(p)
);
}
#[test]
fn test_torus_ray_torus_analytic() {
let t = Torus::new(3.0, 1.0);
let result = t.ray_torus_analytic([3.0, -10.0, 0.0], [0.0, 1.0, 0.0], 100.0);
assert!(result.is_some());
}
#[test]
fn test_torus_support_array_xplus() {
let t = Torus::new(2.0, 0.5);
let sp = t.support_array([1.0, 0.0, 0.0]);
assert!((sp[0] - 2.5).abs() < 1e-10, "sp.x={}", sp[0]);
assert!(sp[1].abs() < 1e-10);
}
#[test]
fn test_torus_surface_parameters_roundtrip() {
let t = Torus::new(3.0, 1.0);
let u_in = 1.0_f64;
let v_in = 0.5_f64;
let p = t.sample_surface(u_in, v_in);
let (u_out, v_out) = t.surface_parameters(p);
let du = (u_out - u_in).abs();
let du_mod = du.min((du - 2.0 * PI).abs());
assert!(du_mod < 1e-9, "u mismatch: in={u_in}, out={u_out}");
assert!(
(v_out - v_in).abs() < 1e-9,
"v mismatch: in={v_in}, out={v_out}"
);
}
#[test]
fn test_torus_random_surface_points_count() {
let t = Torus::new(2.0, 0.5);
let pts = t.random_surface_points(40, 777);
assert_eq!(pts.len(), 40);
}
#[test]
fn test_torus_random_surface_points_on_surface() {
let t = Torus::new(3.0, 0.5);
let pts = t.random_surface_points(60, 42);
for p in &pts {
let sdf = t.sdf(*p);
assert!(sdf.abs() < 1e-6, "point {:?} sdf={sdf}", p);
}
}
#[test]
fn test_torus_outer_inner_radius() {
let t = Torus::new(3.0, 1.0);
assert!((t.outer_radius() - 4.0).abs() < 1e-12);
assert!((t.inner_radius() - 2.0).abs() < 1e-12);
}
#[test]
fn test_torus_inner_radius_clamped() {
let t = Torus::new(0.5, 1.0); assert_eq!(t.inner_radius(), 0.0);
}
#[test]
fn test_torus_inertia_raw_matches_array() {
let t = Torus::new(2.0, 0.5);
let raw = t.inertia_raw(5.0);
let arr = t.inertia_tensor_array(5.0);
for i in 0..3 {
for j in 0..3 {
assert!((raw[i][j] - arr[i][j]).abs() < 1e-12);
}
}
}
#[test]
fn test_torus_uv_map_roundtrip() {
let t = Torus::new(3.0, 1.0);
let p = t.sample_surface(1.2, 0.8);
let [u, v] = t.uv_map(p);
assert!((0.0..1.0).contains(&u), "u={u}");
assert!((0.0..1.0).contains(&v), "v={v}");
let p2 = t.sample_surface(u * 2.0 * PI, v * 2.0 * PI);
let d = ((p[0] - p2[0]).powi(2) + (p[1] - p2[1]).powi(2) + (p[2] - p2[2]).powi(2)).sqrt();
assert!(d < 1e-9, "roundtrip error={d}");
}
#[test]
fn test_torus_uv_map_zero() {
let t = Torus::new(2.0, 0.5);
let p = [t.major_radius + t.minor_radius, 0.0, 0.0];
let [u, v] = t.uv_map(p);
assert!(u.abs() < 1e-9 || (u - 1.0).abs() < 1e-9, "u={u}");
assert!(v.abs() < 1e-9 || (v - 1.0).abs() < 1e-9, "v={v}");
}
#[test]
fn test_torus_geodesic_distance_flat_same_point() {
let t = Torus::new(3.0, 1.0);
let p = t.sample_surface(0.5, 0.3);
let d = t.geodesic_distance_flat(p, p);
assert!(d.abs() < 1e-9, "distance to self should be 0, got {d}");
}
#[test]
fn test_torus_geodesic_distance_flat_opposite_minor() {
let t = Torus::new(3.0, 1.0);
let p1 = t.sample_surface(0.0, 0.0);
let p2 = t.sample_surface(0.0, PI);
let d = t.geodesic_distance_flat(p1, p2);
let expected = PI * t.minor_radius;
assert!((d - expected).abs() < 1e-9, "d={d} expected={expected}");
}
#[test]
fn test_torus_area_element_factor_at_v0() {
let t = Torus::new(3.0, 1.0);
assert!((t.area_element_factor(0.0) - 4.0).abs() < 1e-12);
}
#[test]
fn test_torus_area_element_factor_at_vpi() {
let t = Torus::new(3.0, 1.0);
assert!((t.area_element_factor(PI) - 2.0).abs() < 1e-12);
}
#[test]
fn test_torus_surface_area_numeric_approximation() {
let t = Torus::new(3.0, 1.0);
let analytic = t.surface_area();
let numeric = t.surface_area_numeric(256);
let rel_err = (numeric - analytic).abs() / analytic;
assert!(
rel_err < 0.001,
"numeric={numeric} analytic={analytic} rel_err={rel_err}"
);
}
#[test]
fn test_torus_tube_cross_section_count() {
let t = Torus::new(2.0, 0.5);
let pts = t.tube_cross_section(0.0, 16);
assert_eq!(pts.len(), 16);
}
#[test]
fn test_torus_tube_cross_section_on_torus() {
let t = Torus::new(2.0, 0.5);
let pts = t.tube_cross_section(0.0, 20);
for p in &pts {
let sdf = t.sdf(*p);
assert!(sdf.abs() < 1e-9, "cross-section pt not on torus sdf={sdf}");
}
}
#[test]
fn test_torus_knot_path_count() {
let t = Torus::new(3.0, 1.0);
let pts = t.torus_knot_path(2, 3, 60);
assert_eq!(pts.len(), 60);
}
#[test]
fn test_torus_knot_path_on_surface() {
let t = Torus::new(3.0, 1.0);
let pts = t.torus_knot_path(2, 3, 48);
for p in &pts {
let sdf = t.sdf(*p);
assert!(sdf.abs() < 1e-9, "knot pt sdf={sdf}");
}
}
#[test]
fn test_torus_knot_path_trefoil() {
let t = Torus::new(3.0, 1.0);
let pts = t.torus_knot_path(2, 3, 60);
assert_eq!(pts.len(), 60);
}
#[test]
fn test_torus_winding_number_major_circle() {
let t = Torus::new(3.0, 1.0);
let n = 64;
let curve: Vec<[f64; 3]> = (0..n)
.map(|i| {
let u = 2.0 * PI * i as f64 / n as f64;
t.sample_surface(u, 0.0) })
.collect();
let winding = t.winding_number_major(&curve);
assert_eq!(winding, 1, "should wind once, got {winding}");
}
#[test]
fn test_torus_tangent_u_perpendicular_to_normal() {
let t = Torus::new(3.0, 1.0);
let u = 0.5;
let v = 0.3;
let tu = t.tangent_u(u, v);
let n = t.normal_from_tangents(u, v);
let dot = tu[0] * n[0] + tu[1] * n[1] + tu[2] * n[2];
assert!(dot.abs() < 1e-9, "tangent_u · normal = {dot}");
}
#[test]
fn test_torus_tangent_v_perpendicular_to_normal() {
let t = Torus::new(3.0, 1.0);
let u = 0.5;
let v = 0.3;
let tv = t.tangent_v(u, v);
let n = t.normal_from_tangents(u, v);
let dot = tv[0] * n[0] + tv[1] * n[1] + tv[2] * n[2];
assert!(dot.abs() < 1e-9, "tangent_v · normal = {dot}");
}
#[test]
fn test_torus_normal_from_tangents_unit() {
let t = Torus::new(3.0, 1.0);
let n = t.normal_from_tangents(0.5, 0.3);
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
assert!((len - 1.0).abs() < 1e-9, "normal not unit length={len}");
}
#[test]
fn test_torus_ray_intersection_count_through() {
let t = Torus::new(3.0, 1.0);
let count = t.ray_intersection_count([3.0, -10.0, 0.0], [0.0, 1.0, 0.0], 100.0);
assert!(count >= 2, "expected >=2 intersections, got {count}");
}
#[test]
fn test_torus_ray_intersection_count_miss() {
let t = Torus::new(3.0, 1.0);
let count = t.ray_intersection_count([0.0, 20.0, 0.0], [0.0, 1.0, 0.0], 5.0);
assert_eq!(count, 0, "expected 0 intersections");
}
#[test]
fn test_torus_intersects_aabb_inside() {
let t = Torus::new(3.0, 1.0);
assert!(t.intersects_aabb([2.5, -0.5, -0.5], [3.5, 0.5, 0.5]));
}
#[test]
fn test_torus_intersects_aabb_outside() {
let t = Torus::new(3.0, 1.0);
assert!(!t.intersects_aabb([20.0, 20.0, 20.0], [30.0, 30.0, 30.0]));
}
#[test]
fn test_torus_aspect_ratio() {
let t = Torus::new(4.0, 1.0);
assert!((t.aspect_ratio() - 4.0).abs() < 1e-12);
}
#[test]
fn test_torus_major_circle_points_count() {
let t = Torus::new(3.0, 1.0);
let pts = t.major_circle_points(36);
assert_eq!(pts.len(), 36);
}
#[test]
fn test_torus_major_circle_points_on_ring() {
let t = Torus::new(3.0, 1.0);
let pts = t.major_circle_points(24);
for p in &pts {
let xz = (p[0] * p[0] + p[2] * p[2]).sqrt();
assert!((xz - t.major_radius).abs() < 1e-9, "xz={xz}");
assert!(p[1].abs() < 1e-12, "y={}", p[1]);
}
}
#[test]
fn test_torus_winding_number_single_point() {
let t = Torus::new(3.0, 1.0);
let curve = vec![[3.0, 0.0, 0.0]];
assert_eq!(t.winding_number_major(&curve), 0);
}
#[test]
fn test_torus_winding_number_empty() {
let t = Torus::new(3.0, 1.0);
assert_eq!(t.winding_number_major(&[]), 0);
}
#[test]
fn test_torus_surface_area_numeric_32() {
let t = Torus::new(2.0, 0.5);
let num = t.surface_area_numeric(32);
let ana = t.surface_area();
let err = (num - ana).abs() / ana;
assert!(err < 0.01, "err={err}");
}
#[test]
fn test_torus_tangents_nonzero() {
let t = Torus::new(3.0, 1.0);
let tu = t.tangent_u(0.1, 0.2);
let tv = t.tangent_v(0.1, 0.2);
let len_u = (tu[0] * tu[0] + tu[1] * tu[1] + tu[2] * tu[2]).sqrt();
let len_v = (tv[0] * tv[0] + tv[1] * tv[1] + tv[2] * tv[2]).sqrt();
assert!(len_u > 1e-6);
assert!(len_v > 1e-6);
}
#[test]
fn test_torus_tube_cross_section_minimum_3() {
let t = Torus::new(2.0, 0.5);
let pts = t.tube_cross_section(0.0, 1); assert_eq!(pts.len(), 3);
}
#[test]
fn test_torus_knot_path_trivial_11() {
let t = Torus::new(3.0, 1.0);
let pts = t.torus_knot_path(1, 1, 32);
for p in &pts {
assert!(t.sdf(*p).abs() < 1e-8);
}
}
#[test]
fn test_torus_area_element_factor_positive() {
let t = Torus::new(3.0, 1.0);
for i in 0..32 {
let v = 2.0 * PI * i as f64 / 32.0;
assert!(t.area_element_factor(v) > 0.0, "factor at v={v}");
}
}
}