1use crate::shape::{RayHit, Shape};
7use oxiphysics_core::Aabb;
8use oxiphysics_core::math::{Mat3, Real, Vec3};
9use std::f64::consts::PI;
10
11#[derive(Debug, Clone)]
15pub struct Torus {
16 pub major_radius: Real,
18 pub minor_radius: Real,
20}
21
22impl Torus {
23 pub fn new(major_radius: Real, minor_radius: Real) -> Self {
25 Self {
26 major_radius,
27 minor_radius,
28 }
29 }
30
31 pub fn volume(&self) -> Real {
33 2.0 * PI * PI * self.major_radius * self.minor_radius * self.minor_radius
34 }
35
36 pub fn surface_area(&self) -> Real {
38 4.0 * PI * PI * self.major_radius * self.minor_radius
39 }
40
41 pub fn bounding_box_extents(&self) -> Vec3 {
45 let outer = self.major_radius + self.minor_radius;
46 Vec3::new(outer, self.minor_radius, outer)
47 }
48
49 pub fn inertia_tensor_array(&self, mass: Real) -> [[f64; 3]; 3] {
55 let r = self.major_radius;
56 let a = self.minor_radius;
57 let iy = mass * (r * r + (3.0 / 4.0) * a * a);
58 let ixz = mass * ((5.0 / 8.0) * a * a + r * r / 2.0);
59 [[ixz, 0.0, 0.0], [0.0, iy, 0.0], [0.0, 0.0, ixz]]
60 }
61
62 pub fn ray_cast_array(
70 &self,
71 origin: [f64; 3],
72 direction: [f64; 3],
73 max_toi: f64,
74 ) -> Option<(f64, [f64; 3])> {
75 let o = Vec3::new(origin[0], origin[1], origin[2]);
76 let d = Vec3::new(direction[0], direction[1], direction[2]);
77 let hit = self.ray_cast_impl(&o, &d, max_toi)?;
78 Some((hit.toi, [hit.normal.x, hit.normal.y, hit.normal.z]))
79 }
80
81 pub fn closest_point(&self, p: [f64; 3]) -> [f64; 3] {
87 let px = p[0];
88 let py = p[1];
89 let pz = p[2];
90 let xz_len = (px * px + pz * pz).sqrt();
92 let (ring_x, ring_z) = if xz_len < 1e-12 {
93 (self.major_radius, 0.0)
95 } else {
96 let s = self.major_radius / xz_len;
97 (px * s, pz * s)
98 };
99 let dx = px - ring_x;
101 let dy = py;
102 let dz = pz - ring_z;
103 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
104 if dist < 1e-12 {
105 return [ring_x, self.minor_radius, ring_z];
107 }
108 let s = self.minor_radius / dist;
109 [ring_x + dx * s, dy * s, ring_z + dz * s]
110 }
111
112 pub fn contains_point(&self, p: [f64; 3]) -> bool {
114 let px = p[0];
115 let py = p[1];
116 let pz = p[2];
117 let xz = (px * px + pz * pz).sqrt();
118 let dist_to_ring = ((xz - self.major_radius) * (xz - self.major_radius) + py * py).sqrt();
119 dist_to_ring <= self.minor_radius
120 }
121
122 pub fn sample_surface(&self, u: f64, v: f64) -> [f64; 3] {
129 let r_outer = self.major_radius + self.minor_radius * v.cos();
130 [
131 r_outer * u.cos(),
132 self.minor_radius * v.sin(),
133 r_outer * u.sin(),
134 ]
135 }
136
137 pub fn sdf(&self, p: [f64; 3]) -> f64 {
143 let xz = (p[0] * p[0] + p[2] * p[2]).sqrt();
144 let dist_to_ring = ((xz - self.major_radius).powi(2) + p[1] * p[1]).sqrt();
145 dist_to_ring - self.minor_radius
146 }
147
148 pub fn ray_torus_analytic(
150 &self,
151 origin: [f64; 3],
152 direction: [f64; 3],
153 max_toi: f64,
154 ) -> Option<(f64, [f64; 3])> {
155 self.ray_cast_array(origin, direction, max_toi)
156 }
157
158 pub fn support_array(&self, direction: [f64; 3]) -> [f64; 3] {
160 let d = Vec3::new(direction[0], direction[1], direction[2]);
161 let sp = self.support_point(&d);
162 [sp.x, sp.y, sp.z]
163 }
164
165 pub fn surface_parameters(&self, p: [f64; 3]) -> (f64, f64) {
171 let xz = (p[0] * p[0] + p[2] * p[2]).sqrt();
172 let u = p[2].atan2(p[0]); let rx = self.major_radius * u.cos();
177 let rz = self.major_radius * u.sin();
178
179 let dx = p[0] - rx;
181 let dy = p[1];
182 let dz = p[2] - rz;
183
184 let radial = xz - self.major_radius;
187 let v = dy.atan2(radial);
188
189 let _ = (dx, dz); (u, v)
191 }
192
193 pub fn random_surface_points(&self, n: usize, seed: u64) -> Vec<[f64; 3]> {
198 let mut points = Vec::with_capacity(n);
199 let mut state = seed;
200
201 let next_f64 = |s: &mut u64| -> f64 {
202 *s ^= *s << 13;
203 *s ^= *s >> 7;
204 *s ^= *s << 17;
205 (*s as f64) / (u64::MAX as f64)
206 };
207
208 let max_weight = self.major_radius + self.minor_radius;
211
212 while points.len() < n {
213 let u = next_f64(&mut state) * 2.0 * PI;
214 let v = next_f64(&mut state) * 2.0 * PI;
215 let w = next_f64(&mut state);
216
217 let weight = (self.major_radius + self.minor_radius * v.cos()) / max_weight;
218 if w <= weight {
219 points.push(self.sample_surface(u, v));
220 }
221 }
222 points
223 }
224
225 pub fn inertia_raw(&self, mass: f64) -> [[f64; 3]; 3] {
227 self.inertia_tensor_array(mass)
228 }
229
230 pub fn outer_radius(&self) -> f64 {
232 self.major_radius + self.minor_radius
233 }
234
235 pub fn inner_radius(&self) -> f64 {
237 (self.major_radius - self.minor_radius).max(0.0)
238 }
239
240 pub fn uv_map(&self, p: [f64; 3]) -> [f64; 2] {
248 let (theta, phi) = self.surface_parameters(p);
249 let u = ((theta / (2.0 * PI)) + 1.0) % 1.0;
251 let v = ((phi / (2.0 * PI)) + 1.0) % 1.0;
252 [u, v]
253 }
254
255 pub fn geodesic_distance_flat(&self, a: [f64; 3], b: [f64; 3]) -> f64 {
261 let (ua, va) = self.surface_parameters(a);
262 let (ub, vb) = self.surface_parameters(b);
263 let d_theta = angle_diff(ua, ub);
264 let d_phi = angle_diff(va, vb);
265 let arc_major = self.major_radius * d_theta;
266 let arc_minor = self.minor_radius * d_phi;
267 (arc_major * arc_major + arc_minor * arc_minor).sqrt()
268 }
269
270 pub fn area_element_factor(&self, v: f64) -> f64 {
274 self.major_radius + self.minor_radius * v.cos()
275 }
276
277 pub fn surface_area_numeric(&self, n_steps: usize) -> f64 {
281 let n = n_steps.max(4);
282 let du = 2.0 * PI / n as f64;
283 let dv = 2.0 * PI / n as f64;
284 let mut sum = 0.0;
285 for iv in 0..n {
286 let v = (iv as f64 + 0.5) * dv;
287 let factor = self.area_element_factor(v);
288 sum += factor * n as f64; }
290 sum * self.minor_radius * du * dv
291 }
292
293 pub fn tube_cross_section(&self, u: f64, n: usize) -> Vec<[f64; 3]> {
297 let n = n.max(3);
298 let ring_x = self.major_radius * u.cos();
299 let ring_z = self.major_radius * u.sin();
300 (0..n)
301 .map(|i| {
302 let v = 2.0 * PI * i as f64 / n as f64;
303 let rx = u.cos();
305 let rz = u.sin();
306 let cx = ring_x + self.minor_radius * v.cos() * rx;
307 let cy = self.minor_radius * v.sin();
308 let cz = ring_z + self.minor_radius * v.cos() * rz;
309 [cx, cy, cz]
310 })
311 .collect()
312 }
313
314 pub fn torus_knot_path(&self, p: i32, q: i32, n_pts: usize) -> Vec<[f64; 3]> {
319 let n = n_pts.max(3);
320 (0..n)
321 .map(|i| {
322 let t = 2.0 * PI * i as f64 / n as f64;
323 let u = p as f64 * t;
324 let v = q as f64 * t;
325 self.sample_surface(u, v)
326 })
327 .collect()
328 }
329
330 pub fn winding_number_major(&self, curve: &[[f64; 3]]) -> i32 {
337 if curve.len() < 2 {
338 return 0;
339 }
340 let angles: Vec<f64> = curve
341 .iter()
342 .map(|&p| self.surface_parameters(p).0)
343 .collect();
344 let mut total = 0.0_f64;
345 for i in 0..angles.len() {
346 let next = (i + 1) % angles.len();
347 let diff = angle_diff(angles[i], angles[next]);
348 total += diff;
349 }
350 (total / (2.0 * PI)).round() as i32
351 }
352
353 pub fn tangent_u(&self, u: f64, v: f64) -> [f64; 3] {
357 let r_outer = self.major_radius + self.minor_radius * v.cos();
358 [-r_outer * u.sin(), 0.0, r_outer * u.cos()]
359 }
360
361 pub fn tangent_v(&self, u: f64, v: f64) -> [f64; 3] {
365 let r = self.minor_radius;
366 [-r * v.sin() * u.cos(), r * v.cos(), -r * v.sin() * u.sin()]
367 }
368
369 pub fn normal_from_tangents(&self, u: f64, v: f64) -> [f64; 3] {
373 let tu = self.tangent_u(u, v);
374 let tv = self.tangent_v(u, v);
375 let n = cross3([tu[0], tu[1], tu[2]], [tv[0], tv[1], tv[2]]);
376 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
377 if len < 1e-14 {
378 [0.0, 1.0, 0.0]
379 } else {
380 [n[0] / len, n[1] / len, n[2] / len]
381 }
382 }
383
384 pub fn ray_intersection_count(
390 &self,
391 origin: [f64; 3],
392 direction: [f64; 3],
393 max_toi: f64,
394 ) -> usize {
395 let o = Vec3::new(origin[0], origin[1], origin[2]);
396 let d = Vec3::new(direction[0], direction[1], direction[2]);
397 let dir_len = d.norm();
398 if dir_len < 1e-12 {
399 return 0;
400 }
401 let dn = d / dir_len;
402
403 let oo = o.dot(&o);
404 let od = o.dot(&dn);
405 let rr = self.major_radius * self.major_radius;
406 let aa = self.minor_radius * self.minor_radius;
407 let c_alpha = oo + rr - aa;
408 let beta = 2.0 * od;
409 let dxz2 = dn.x * dn.x + dn.z * dn.z;
410 let od_xz = o.x * dn.x + o.z * dn.z;
411 let oxz2 = o.x * o.x + o.z * o.z;
412 let c4 = 1.0;
413 let c3 = 2.0 * beta;
414 let c2 = beta * beta + 2.0 * c_alpha - 4.0 * rr * dxz2;
415 let c1 = 2.0 * beta * c_alpha - 8.0 * rr * od_xz;
416 let c0 = c_alpha * c_alpha - 4.0 * rr * oxz2;
417 let roots = solve_quartic(c4, c3, c2, c1, c0);
418
419 roots
420 .iter()
421 .filter(|&&t| {
422 !t.is_nan() && !t.is_infinite() && {
423 let t_actual = t / dir_len;
424 t_actual >= 1e-6 && t_actual <= max_toi
425 }
426 })
427 .count()
428 }
429
430 pub fn intersects_aabb(&self, aabb_min: [f64; 3], aabb_max: [f64; 3]) -> bool {
435 let corners = [
436 [aabb_min[0], aabb_min[1], aabb_min[2]],
437 [aabb_max[0], aabb_min[1], aabb_min[2]],
438 [aabb_min[0], aabb_max[1], aabb_min[2]],
439 [aabb_max[0], aabb_max[1], aabb_min[2]],
440 [aabb_min[0], aabb_min[1], aabb_max[2]],
441 [aabb_max[0], aabb_min[1], aabb_max[2]],
442 [aabb_min[0], aabb_max[1], aabb_max[2]],
443 [aabb_max[0], aabb_max[1], aabb_max[2]],
444 ];
445 let sdfs: Vec<f64> = corners.iter().map(|&c| self.sdf(c)).collect();
447 let min_sdf = sdfs.iter().cloned().fold(f64::INFINITY, f64::min);
448 min_sdf <= 0.0
449 }
450
451 pub fn aspect_ratio(&self) -> f64 {
456 self.major_radius / self.minor_radius.max(1e-30)
457 }
458
459 pub fn major_circle_points(&self, n: usize) -> Vec<[f64; 3]> {
464 let n = n.max(2);
465 (0..n)
466 .map(|i| {
467 let u = 2.0 * PI * i as f64 / n as f64;
468 [
469 self.major_radius * u.cos(),
470 0.0,
471 self.major_radius * u.sin(),
472 ]
473 })
474 .collect()
475 }
476
477 fn ray_cast_impl(&self, origin: &Vec3, dir: &Vec3, max_toi: Real) -> Option<RayHit> {
481 let r_big = self.major_radius;
482 let r_small = self.minor_radius;
483
484 let dir_len = dir.norm();
486 if dir_len < 1e-12 {
487 return None;
488 }
489 let d = dir / dir_len;
490 let o = *origin;
491
492 let oo = o.dot(&o);
495 let od = o.dot(&d);
496 let _dd = d.dot(&d); let rr = r_big * r_big;
498 let aa = r_small * r_small;
499 let alpha = oo - rr - aa; let beta = 2.0 * od;
501 let dxz2 = d.x * d.x + d.z * d.z;
509 let od_xz = o.x * d.x + o.z * d.z;
510 let oxz2 = o.x * o.x + o.z * o.z;
511
512 let c_alpha = oo + rr - aa; let c4 = 1.0;
516 let c3 = 2.0 * beta;
517 let c2 = beta * beta + 2.0 * c_alpha - 4.0 * rr * dxz2;
518 let c1 = 2.0 * beta * c_alpha - 8.0 * rr * od_xz;
519 let c0 = c_alpha * c_alpha - 4.0 * rr * oxz2;
520 let _ = alpha; let roots = solve_quartic(c4, c3, c2, c1, c0);
526
527 let mut best_t = Real::INFINITY;
528 for t_raw in roots {
529 if t_raw.is_nan() || t_raw.is_infinite() {
530 continue;
531 }
532 let t = t_raw / dir_len;
534 if t >= 1e-6 && t <= max_toi && t < best_t {
535 best_t = t;
536 }
537 }
538 if best_t.is_infinite() {
539 return None;
540 }
541
542 let point = o + d * (best_t * dir_len);
543 let normal = torus_normal(&point, r_big);
544 Some(RayHit {
545 point,
546 normal,
547 toi: best_t,
548 })
549 }
550}
551
552fn angle_diff(a: f64, b: f64) -> f64 {
554 let d = b - a;
555 d - (2.0 * PI) * ((d + PI) / (2.0 * PI)).floor()
558}
559
560fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
562 [
563 a[1] * b[2] - a[2] * b[1],
564 a[2] * b[0] - a[0] * b[2],
565 a[0] * b[1] - a[1] * b[0],
566 ]
567}
568
569fn torus_normal(p: &Vec3, big_r: Real) -> Vec3 {
571 let xz = (p.x * p.x + p.z * p.z).sqrt();
572 if xz < 1e-12 {
573 return Vec3::new(0.0, 1.0, 0.0);
574 }
575 let s = big_r / xz;
577 let ring = Vec3::new(p.x * s, 0.0, p.z * s);
578 let n = p - ring;
579 let len = n.norm();
580 if len < 1e-12 {
581 Vec3::new(p.x / xz, 0.0, p.z / xz)
582 } else {
583 n / len
584 }
585}
586
587fn solve_quartic(c4: f64, c3: f64, c2: f64, c1: f64, c0: f64) -> [f64; 4] {
590 let a = c3 / c4;
592 let b = c2 / c4;
593 let c = c1 / c4;
594 let d = c0 / c4;
595
596 let p_depr = b - (3.0 * a * a) / 8.0;
598 let q_depr = (a * a * a) / 8.0 - (a * b) / 2.0 + c;
599 let r_depr = -(3.0 * a * a * a * a) / 256.0 + (a * a * b) / 16.0 - (a * c) / 4.0 + d;
600
601 let shift = a / 4.0;
602
603 let (m1, m2, m3) = solve_cubic_3roots(
607 8.0,
608 8.0 * p_depr,
609 2.0 * p_depr * p_depr - 8.0 * r_depr,
610 -(q_depr * q_depr),
611 );
612
613 let mut roots = [f64::NAN; 4];
614 let mut ri = 0usize;
615
616 for m in [m1, m2, m3] {
617 if m.is_nan() {
618 continue;
619 }
620 let two_m_p = 2.0 * m + p_depr;
621 if two_m_p < 0.0 {
622 continue;
623 }
624 let sqrt_2mp = two_m_p.sqrt();
625 if sqrt_2mp < 1e-14 {
626 continue;
627 }
628
629 let half_q_over = q_depr / (2.0 * sqrt_2mp);
633
634 for &(s, c_q) in &[(1.0_f64, m + half_q_over), (-1.0_f64, m - half_q_over)] {
635 let disc = (s * sqrt_2mp) * (s * sqrt_2mp) / 4.0 - c_q;
636 let disc2 = sqrt_2mp * sqrt_2mp / 4.0 - c_q;
638 if disc2 >= -1e-9 {
639 let sd = disc2.max(0.0).sqrt();
640 let half_b = s * sqrt_2mp / 2.0;
641 let r0 = -half_b + sd - shift;
642 let r1 = -half_b - sd - shift;
643 if ri < 4 {
644 roots[ri] = r0;
645 ri += 1;
646 }
647 if ri < 4 {
648 roots[ri] = r1;
649 ri += 1;
650 }
651 }
652 let _ = disc; }
654 if ri >= 4 {
655 break;
656 }
657 }
658 roots
659}
660
661fn solve_cubic_3roots(a: f64, b: f64, c: f64, d: f64) -> (f64, f64, f64) {
664 let b = b / a;
666 let c = c / a;
667 let d = d / a;
668
669 let p = c - b * b / 3.0;
671 let q = 2.0 * b * b * b / 27.0 - b * c / 3.0 + d;
672 let shift = b / 3.0;
673
674 let discriminant = -(4.0 * p * p * p + 27.0 * q * q);
675
676 if discriminant >= 0.0 {
677 let m = 2.0 * (-p / 3.0).sqrt();
679 let theta = ((3.0 * q) / (2.0 * p) * ((-3.0) / p).sqrt())
680 .clamp(-1.0, 1.0)
681 .acos();
682 let r0 = m * (theta / 3.0).cos() - shift;
683 let r1 = m * ((theta - 2.0 * PI) / 3.0).cos() - shift;
684 let r2 = m * ((theta - 4.0 * PI) / 3.0).cos() - shift;
685 (r0, r1, r2)
686 } else {
687 let d_val = -(4.0 * p * p * p + 27.0 * q * q);
689 let inner = -q / 2.0;
690 let outer = (q * q / 4.0 + p * p * p / 27.0).sqrt();
691 let u = cbrt_real(inner + outer);
692 let v = cbrt_real(inner - outer);
693 let r0 = u + v - shift;
694 let _ = d_val; (r0, f64::NAN, f64::NAN)
696 }
697}
698
699fn cbrt_real(x: f64) -> f64 {
700 if x >= 0.0 { x.cbrt() } else { -((-x).cbrt()) }
701}
702
703impl Shape for Torus {
704 fn bounding_box(&self) -> Aabb {
705 let half = self.bounding_box_extents();
706 Aabb::new(-half, half)
707 }
708
709 fn support_point(&self, direction: &Vec3) -> Vec3 {
710 let eps = 1e-10;
711 let dir_xz = Vec3::new(direction.x, 0.0, direction.z);
713 let xz_len = dir_xz.norm();
714 let xz_norm = if xz_len < eps {
715 Vec3::new(1.0, 0.0, 0.0)
716 } else {
717 dir_xz / xz_len
718 };
719 let tube_center = xz_norm * self.major_radius;
721 let dir_len = direction.norm();
723 let dir_norm = if dir_len < eps {
724 Vec3::new(1.0, 0.0, 0.0)
725 } else {
726 direction / dir_len
727 };
728 tube_center + dir_norm * self.minor_radius
729 }
730
731 fn volume(&self) -> Real {
732 self.volume()
733 }
734
735 fn center_of_mass(&self) -> Vec3 {
736 Vec3::zeros()
737 }
738
739 fn inertia_tensor(&self, mass: Real) -> Mat3 {
740 let r = self.major_radius;
741 let a = self.minor_radius;
742 let iy = mass * (r * r + (3.0 / 4.0) * a * a);
747 let ixz = mass * ((5.0 / 8.0) * a * a + r * r / 2.0);
748 Mat3::new(ixz, 0.0, 0.0, 0.0, iy, 0.0, 0.0, 0.0, ixz)
749 }
750
751 fn mass_properties(&self, density: Real) -> oxiphysics_core::MassProperties {
752 let mass = density * self.volume();
753 oxiphysics_core::MassProperties::new(mass, self.center_of_mass(), self.inertia_tensor(mass))
754 }
755
756 fn ray_cast(&self, ray_origin: &Vec3, ray_direction: &Vec3, max_toi: Real) -> Option<RayHit> {
757 self.ray_cast_impl(ray_origin, ray_direction, max_toi)
758 }
759}
760
761#[cfg(test)]
762mod tests {
763 use super::*;
764
765 #[test]
766 fn test_torus_volume() {
767 let t = Torus::new(1.0, 0.5);
768 let expected = 2.0 * PI * PI * 1.0 * 0.25;
770 assert!(
771 (t.volume() - expected).abs() < 1e-6,
772 "volume={}, expected={}",
773 t.volume(),
774 expected
775 );
776 assert!(
778 (t.volume() - 4.935).abs() < 0.01,
779 "volume ≈ 4.935, got {}",
780 t.volume()
781 );
782 }
783
784 #[test]
785 fn test_torus_surface_area() {
786 let t = Torus::new(1.0, 0.5);
787 let expected = 4.0 * PI * PI * 1.0 * 0.5;
789 assert!(
790 (t.surface_area() - expected).abs() < 1e-6,
791 "surface_area={}, expected={}",
792 t.surface_area(),
793 expected
794 );
795 assert!(
796 (t.surface_area() - 19.74).abs() < 0.01,
797 "surface_area ≈ 19.74, got {}",
798 t.surface_area()
799 );
800 }
801
802 #[test]
803 fn test_torus_bounding_box() {
804 let t = Torus::new(2.0, 0.5);
805 let extents = t.bounding_box_extents();
807 assert!((extents.x - 2.5).abs() < 1e-10, "extents.x={}", extents.x);
808 assert!((extents.y - 0.5).abs() < 1e-10, "extents.y={}", extents.y);
809 assert!((extents.z - 2.5).abs() < 1e-10, "extents.z={}", extents.z);
810 let bb = t.bounding_box();
811 assert!((bb.min.x + 2.5).abs() < 1e-10);
812 assert!((bb.max.x - 2.5).abs() < 1e-10);
813 assert!((bb.min.y + 0.5).abs() < 1e-10);
814 assert!((bb.max.y - 0.5).abs() < 1e-10);
815 }
816
817 #[test]
818 fn test_torus_support_xz() {
819 let t = Torus::new(1.0, 0.5);
820 let dir = Vec3::new(1.0, 0.0, 0.0);
822 let sp = t.support_point(&dir);
823 let expected = 1.0 + 0.5; assert!(
825 (sp.x - expected).abs() < 1e-10,
826 "support.x={}, expected={}",
827 sp.x,
828 expected
829 );
830 assert!(sp.y.abs() < 1e-10, "support.y={}", sp.y);
831 assert!(sp.z.abs() < 1e-10, "support.z={}", sp.z);
832 }
833
834 #[test]
835 fn test_torus_support_y() {
836 let t = Torus::new(1.0, 0.5);
837 let dir = Vec3::new(0.0, 1.0, 0.0);
839 let sp = t.support_point(&dir);
840 assert!(
843 (sp.y - 0.5).abs() < 1e-10,
844 "support.y={}, expected minor_radius=0.5",
845 sp.y
846 );
847 }
848
849 #[test]
850 fn test_torus_mass_properties() {
851 let t = Torus::new(1.0, 0.5);
852 let density = 1000.0;
853 let mp = t.mass_properties(density);
854 let expected_mass = density * t.volume();
855 assert!(
856 (mp.mass - expected_mass).abs() < 1e-6,
857 "mass={}, expected={}",
858 mp.mass,
859 expected_mass
860 );
861 assert!(mp.mass > 0.0, "mass should be positive");
862 assert!(
863 mp.local_inertia[(0, 0)] > 0.0,
864 "I_xx should be positive: {}",
865 mp.local_inertia[(0, 0)]
866 );
867 assert!(
868 mp.local_inertia[(1, 1)] > 0.0,
869 "I_yy should be positive: {}",
870 mp.local_inertia[(1, 1)]
871 );
872 assert!(
873 mp.local_inertia[(2, 2)] > 0.0,
874 "I_zz should be positive: {}",
875 mp.local_inertia[(2, 2)]
876 );
877 }
878
879 #[test]
880 fn test_torus_contains_point_inside() {
881 let t = Torus::new(3.0, 1.0);
882 assert!(
884 t.contains_point([3.0, 0.0, 0.0]),
885 "ring centre should be inside"
886 );
887 assert!(
889 !t.contains_point([0.0, 0.0, 0.0]),
890 "origin should be outside"
891 );
892 assert!(
894 t.contains_point([3.0, 1.0, 0.0]),
895 "top of tube should be inside/on"
896 );
897 assert!(
899 !t.contains_point([3.0, 1.1, 0.0]),
900 "beyond tube should be outside"
901 );
902 }
903
904 #[test]
905 fn test_torus_closest_point_on_surface() {
906 let t = Torus::new(3.0, 1.0);
907 let cp = t.closest_point([10.0, 0.0, 0.0]);
909 assert!((cp[0] - 4.0).abs() < 1e-6, "cp.x={}, expected 4.0", cp[0]);
911 assert!(cp[1].abs() < 1e-6, "cp.y={}", cp[1]);
912 assert!(cp[2].abs() < 1e-6, "cp.z={}", cp[2]);
913 }
914
915 #[test]
916 fn test_torus_sample_surface_on_torus() {
917 let t = Torus::new(3.0, 1.0);
918 let p = t.sample_surface(0.0, 0.0);
920 assert!((p[0] - 4.0).abs() < 1e-10, "p.x={}", p[0]);
921 assert!(p[1].abs() < 1e-10, "p.y={}", p[1]);
922 assert!(
924 t.contains_point(p),
925 "sampled point {:?} should be on torus surface",
926 p
927 );
928 let p2 = t.sample_surface(PI / 2.0, PI / 2.0);
930 assert!(p2[0].abs() < 1e-10, "p2.x={}", p2[0]);
932 assert!((p2[1] - 1.0).abs() < 1e-10, "p2.y={}", p2[1]);
933 assert!((p2[2] - 3.0).abs() < 1e-10, "p2.z={}", p2[2]);
934 }
935
936 #[test]
937 fn test_torus_inertia_tensor_array() {
938 let t = Torus::new(2.0, 0.5);
939 let it = t.inertia_tensor_array(10.0);
940 assert!(it[0][0] > 0.0, "Ixx={}", it[0][0]);
942 assert!(it[1][1] > 0.0, "Iyy={}", it[1][1]);
943 assert!(it[2][2] > 0.0, "Izz={}", it[2][2]);
944 assert!(it[0][1].abs() < 1e-12);
946 assert!(it[0][2].abs() < 1e-12);
947 assert!((it[0][0] - it[2][2]).abs() < 1e-10);
949 }
950
951 #[test]
952 fn test_torus_ray_cast_hit() {
953 let t = Torus::new(3.0, 1.0);
954 let origin = Vec3::new(3.0, -10.0, 0.0);
956 let dir = Vec3::new(0.0, 1.0, 0.0);
957 let hit = t.ray_cast(&origin, &dir, 100.0);
958 assert!(hit.is_some(), "ray through tube should hit");
959 let hit = hit.unwrap();
960 assert!(
962 hit.toi > 0.0 && hit.toi < 20.0,
963 "toi should be reasonable, got {}",
964 hit.toi
965 );
966 }
967
968 #[test]
969 fn test_torus_ray_cast_miss() {
970 let t = Torus::new(3.0, 1.0);
971 let origin = Vec3::new(0.0, 10.0, 0.0);
973 let dir = Vec3::new(0.0, 1.0, 0.0);
974 let hit = t.ray_cast(&origin, &dir, 5.0);
975 assert!(hit.is_none(), "ray going away should miss");
976 }
977
978 #[test]
979 fn test_torus_ray_cast_array() {
980 let t = Torus::new(3.0, 1.0);
981 let result = t.ray_cast_array([3.0, -10.0, 0.0], [0.0, 1.0, 0.0], 100.0);
982 assert!(result.is_some(), "ray should hit");
983 let (toi, _n) = result.unwrap();
984 assert!(
985 toi > 0.0 && toi < 20.0,
986 "toi should be reasonable, got {}",
987 toi
988 );
989 }
990
991 #[test]
994 fn test_torus_sdf_inside_tube() {
995 let t = Torus::new(3.0, 1.0);
996 let p = [3.0, 0.0, 0.0];
998 assert!(
999 (t.sdf(p) + 1.0).abs() < 1e-10,
1000 "sdf at ring center should be -1, got {}",
1001 t.sdf(p)
1002 );
1003 }
1004
1005 #[test]
1006 fn test_torus_sdf_outside() {
1007 let t = Torus::new(3.0, 1.0);
1008 let p = [10.0, 0.0, 0.0];
1010 assert!(t.sdf(p) > 0.0, "sdf outside should be positive");
1011 }
1012
1013 #[test]
1014 fn test_torus_sdf_on_surface() {
1015 let t = Torus::new(3.0, 1.0);
1016 let p = [4.0, 0.0, 0.0];
1018 assert!(
1019 t.sdf(p).abs() < 1e-10,
1020 "sdf at surface should be ~0, got {}",
1021 t.sdf(p)
1022 );
1023 }
1024
1025 #[test]
1026 fn test_torus_ray_torus_analytic() {
1027 let t = Torus::new(3.0, 1.0);
1028 let result = t.ray_torus_analytic([3.0, -10.0, 0.0], [0.0, 1.0, 0.0], 100.0);
1029 assert!(result.is_some());
1030 }
1031
1032 #[test]
1033 fn test_torus_support_array_xplus() {
1034 let t = Torus::new(2.0, 0.5);
1035 let sp = t.support_array([1.0, 0.0, 0.0]);
1036 assert!((sp[0] - 2.5).abs() < 1e-10, "sp.x={}", sp[0]);
1038 assert!(sp[1].abs() < 1e-10);
1039 }
1040
1041 #[test]
1042 fn test_torus_surface_parameters_roundtrip() {
1043 let t = Torus::new(3.0, 1.0);
1044 let u_in = 1.0_f64;
1045 let v_in = 0.5_f64;
1046 let p = t.sample_surface(u_in, v_in);
1047 let (u_out, v_out) = t.surface_parameters(p);
1048 let du = (u_out - u_in).abs();
1050 let du_mod = du.min((du - 2.0 * PI).abs());
1051 assert!(du_mod < 1e-9, "u mismatch: in={u_in}, out={u_out}");
1052 assert!(
1053 (v_out - v_in).abs() < 1e-9,
1054 "v mismatch: in={v_in}, out={v_out}"
1055 );
1056 }
1057
1058 #[test]
1059 fn test_torus_random_surface_points_count() {
1060 let t = Torus::new(2.0, 0.5);
1061 let pts = t.random_surface_points(40, 777);
1062 assert_eq!(pts.len(), 40);
1063 }
1064
1065 #[test]
1066 fn test_torus_random_surface_points_on_surface() {
1067 let t = Torus::new(3.0, 0.5);
1068 let pts = t.random_surface_points(60, 42);
1069 for p in &pts {
1070 let sdf = t.sdf(*p);
1071 assert!(sdf.abs() < 1e-6, "point {:?} sdf={sdf}", p);
1072 }
1073 }
1074
1075 #[test]
1076 fn test_torus_outer_inner_radius() {
1077 let t = Torus::new(3.0, 1.0);
1078 assert!((t.outer_radius() - 4.0).abs() < 1e-12);
1079 assert!((t.inner_radius() - 2.0).abs() < 1e-12);
1080 }
1081
1082 #[test]
1083 fn test_torus_inner_radius_clamped() {
1084 let t = Torus::new(0.5, 1.0); assert_eq!(t.inner_radius(), 0.0);
1086 }
1087
1088 #[test]
1089 fn test_torus_inertia_raw_matches_array() {
1090 let t = Torus::new(2.0, 0.5);
1091 let raw = t.inertia_raw(5.0);
1092 let arr = t.inertia_tensor_array(5.0);
1093 for i in 0..3 {
1094 for j in 0..3 {
1095 assert!((raw[i][j] - arr[i][j]).abs() < 1e-12);
1096 }
1097 }
1098 }
1099
1100 #[test]
1103 fn test_torus_uv_map_roundtrip() {
1104 let t = Torus::new(3.0, 1.0);
1105 let p = t.sample_surface(1.2, 0.8);
1106 let [u, v] = t.uv_map(p);
1107 assert!((0.0..1.0).contains(&u), "u={u}");
1109 assert!((0.0..1.0).contains(&v), "v={v}");
1110 let p2 = t.sample_surface(u * 2.0 * PI, v * 2.0 * PI);
1112 let d = ((p[0] - p2[0]).powi(2) + (p[1] - p2[1]).powi(2) + (p[2] - p2[2]).powi(2)).sqrt();
1113 assert!(d < 1e-9, "roundtrip error={d}");
1114 }
1115
1116 #[test]
1117 fn test_torus_uv_map_zero() {
1118 let t = Torus::new(2.0, 0.5);
1119 let p = [t.major_radius + t.minor_radius, 0.0, 0.0];
1121 let [u, v] = t.uv_map(p);
1122 assert!(u.abs() < 1e-9 || (u - 1.0).abs() < 1e-9, "u={u}");
1123 assert!(v.abs() < 1e-9 || (v - 1.0).abs() < 1e-9, "v={v}");
1124 }
1125
1126 #[test]
1127 fn test_torus_geodesic_distance_flat_same_point() {
1128 let t = Torus::new(3.0, 1.0);
1129 let p = t.sample_surface(0.5, 0.3);
1130 let d = t.geodesic_distance_flat(p, p);
1131 assert!(d.abs() < 1e-9, "distance to self should be 0, got {d}");
1132 }
1133
1134 #[test]
1135 fn test_torus_geodesic_distance_flat_opposite_minor() {
1136 let t = Torus::new(3.0, 1.0);
1137 let p1 = t.sample_surface(0.0, 0.0);
1138 let p2 = t.sample_surface(0.0, PI);
1139 let d = t.geodesic_distance_flat(p1, p2);
1141 let expected = PI * t.minor_radius;
1142 assert!((d - expected).abs() < 1e-9, "d={d} expected={expected}");
1143 }
1144
1145 #[test]
1146 fn test_torus_area_element_factor_at_v0() {
1147 let t = Torus::new(3.0, 1.0);
1148 assert!((t.area_element_factor(0.0) - 4.0).abs() < 1e-12);
1150 }
1151
1152 #[test]
1153 fn test_torus_area_element_factor_at_vpi() {
1154 let t = Torus::new(3.0, 1.0);
1155 assert!((t.area_element_factor(PI) - 2.0).abs() < 1e-12);
1157 }
1158
1159 #[test]
1160 fn test_torus_surface_area_numeric_approximation() {
1161 let t = Torus::new(3.0, 1.0);
1162 let analytic = t.surface_area();
1163 let numeric = t.surface_area_numeric(256);
1164 let rel_err = (numeric - analytic).abs() / analytic;
1166 assert!(
1167 rel_err < 0.001,
1168 "numeric={numeric} analytic={analytic} rel_err={rel_err}"
1169 );
1170 }
1171
1172 #[test]
1173 fn test_torus_tube_cross_section_count() {
1174 let t = Torus::new(2.0, 0.5);
1175 let pts = t.tube_cross_section(0.0, 16);
1176 assert_eq!(pts.len(), 16);
1177 }
1178
1179 #[test]
1180 fn test_torus_tube_cross_section_on_torus() {
1181 let t = Torus::new(2.0, 0.5);
1182 let pts = t.tube_cross_section(0.0, 20);
1183 for p in &pts {
1184 let sdf = t.sdf(*p);
1185 assert!(sdf.abs() < 1e-9, "cross-section pt not on torus sdf={sdf}");
1186 }
1187 }
1188
1189 #[test]
1190 fn test_torus_knot_path_count() {
1191 let t = Torus::new(3.0, 1.0);
1192 let pts = t.torus_knot_path(2, 3, 60);
1193 assert_eq!(pts.len(), 60);
1194 }
1195
1196 #[test]
1197 fn test_torus_knot_path_on_surface() {
1198 let t = Torus::new(3.0, 1.0);
1199 let pts = t.torus_knot_path(2, 3, 48);
1200 for p in &pts {
1201 let sdf = t.sdf(*p);
1202 assert!(sdf.abs() < 1e-9, "knot pt sdf={sdf}");
1203 }
1204 }
1205
1206 #[test]
1207 fn test_torus_knot_path_trefoil() {
1208 let t = Torus::new(3.0, 1.0);
1210 let pts = t.torus_knot_path(2, 3, 60);
1211 assert_eq!(pts.len(), 60);
1214 }
1215
1216 #[test]
1217 fn test_torus_winding_number_major_circle() {
1218 let t = Torus::new(3.0, 1.0);
1220 let n = 64;
1221 let curve: Vec<[f64; 3]> = (0..n)
1222 .map(|i| {
1223 let u = 2.0 * PI * i as f64 / n as f64;
1224 t.sample_surface(u, 0.0) })
1226 .collect();
1227 let winding = t.winding_number_major(&curve);
1228 assert_eq!(winding, 1, "should wind once, got {winding}");
1229 }
1230
1231 #[test]
1232 fn test_torus_tangent_u_perpendicular_to_normal() {
1233 let t = Torus::new(3.0, 1.0);
1234 let u = 0.5;
1235 let v = 0.3;
1236 let tu = t.tangent_u(u, v);
1237 let n = t.normal_from_tangents(u, v);
1238 let dot = tu[0] * n[0] + tu[1] * n[1] + tu[2] * n[2];
1239 assert!(dot.abs() < 1e-9, "tangent_u · normal = {dot}");
1240 }
1241
1242 #[test]
1243 fn test_torus_tangent_v_perpendicular_to_normal() {
1244 let t = Torus::new(3.0, 1.0);
1245 let u = 0.5;
1246 let v = 0.3;
1247 let tv = t.tangent_v(u, v);
1248 let n = t.normal_from_tangents(u, v);
1249 let dot = tv[0] * n[0] + tv[1] * n[1] + tv[2] * n[2];
1250 assert!(dot.abs() < 1e-9, "tangent_v · normal = {dot}");
1251 }
1252
1253 #[test]
1254 fn test_torus_normal_from_tangents_unit() {
1255 let t = Torus::new(3.0, 1.0);
1256 let n = t.normal_from_tangents(0.5, 0.3);
1257 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
1258 assert!((len - 1.0).abs() < 1e-9, "normal not unit length={len}");
1259 }
1260
1261 #[test]
1262 fn test_torus_ray_intersection_count_through() {
1263 let t = Torus::new(3.0, 1.0);
1264 let count = t.ray_intersection_count([3.0, -10.0, 0.0], [0.0, 1.0, 0.0], 100.0);
1267 assert!(count >= 2, "expected >=2 intersections, got {count}");
1268 }
1269
1270 #[test]
1271 fn test_torus_ray_intersection_count_miss() {
1272 let t = Torus::new(3.0, 1.0);
1273 let count = t.ray_intersection_count([0.0, 20.0, 0.0], [0.0, 1.0, 0.0], 5.0);
1275 assert_eq!(count, 0, "expected 0 intersections");
1276 }
1277
1278 #[test]
1279 fn test_torus_intersects_aabb_inside() {
1280 let t = Torus::new(3.0, 1.0);
1281 assert!(t.intersects_aabb([2.5, -0.5, -0.5], [3.5, 0.5, 0.5]));
1283 }
1284
1285 #[test]
1286 fn test_torus_intersects_aabb_outside() {
1287 let t = Torus::new(3.0, 1.0);
1288 assert!(!t.intersects_aabb([20.0, 20.0, 20.0], [30.0, 30.0, 30.0]));
1290 }
1291
1292 #[test]
1293 fn test_torus_aspect_ratio() {
1294 let t = Torus::new(4.0, 1.0);
1295 assert!((t.aspect_ratio() - 4.0).abs() < 1e-12);
1296 }
1297
1298 #[test]
1299 fn test_torus_major_circle_points_count() {
1300 let t = Torus::new(3.0, 1.0);
1301 let pts = t.major_circle_points(36);
1302 assert_eq!(pts.len(), 36);
1303 }
1304
1305 #[test]
1306 fn test_torus_major_circle_points_on_ring() {
1307 let t = Torus::new(3.0, 1.0);
1308 let pts = t.major_circle_points(24);
1309 for p in &pts {
1310 let xz = (p[0] * p[0] + p[2] * p[2]).sqrt();
1311 assert!((xz - t.major_radius).abs() < 1e-9, "xz={xz}");
1312 assert!(p[1].abs() < 1e-12, "y={}", p[1]);
1313 }
1314 }
1315
1316 #[test]
1317 fn test_torus_winding_number_single_point() {
1318 let t = Torus::new(3.0, 1.0);
1319 let curve = vec![[3.0, 0.0, 0.0]];
1320 assert_eq!(t.winding_number_major(&curve), 0);
1321 }
1322
1323 #[test]
1324 fn test_torus_winding_number_empty() {
1325 let t = Torus::new(3.0, 1.0);
1326 assert_eq!(t.winding_number_major(&[]), 0);
1327 }
1328
1329 #[test]
1330 fn test_torus_surface_area_numeric_32() {
1331 let t = Torus::new(2.0, 0.5);
1332 let num = t.surface_area_numeric(32);
1333 let ana = t.surface_area();
1334 let err = (num - ana).abs() / ana;
1335 assert!(err < 0.01, "err={err}");
1336 }
1337
1338 #[test]
1339 fn test_torus_tangents_nonzero() {
1340 let t = Torus::new(3.0, 1.0);
1341 let tu = t.tangent_u(0.1, 0.2);
1342 let tv = t.tangent_v(0.1, 0.2);
1343 let len_u = (tu[0] * tu[0] + tu[1] * tu[1] + tu[2] * tu[2]).sqrt();
1344 let len_v = (tv[0] * tv[0] + tv[1] * tv[1] + tv[2] * tv[2]).sqrt();
1345 assert!(len_u > 1e-6);
1346 assert!(len_v > 1e-6);
1347 }
1348
1349 #[test]
1350 fn test_torus_tube_cross_section_minimum_3() {
1351 let t = Torus::new(2.0, 0.5);
1352 let pts = t.tube_cross_section(0.0, 1); assert_eq!(pts.len(), 3);
1354 }
1355
1356 #[test]
1357 fn test_torus_knot_path_trivial_11() {
1358 let t = Torus::new(3.0, 1.0);
1360 let pts = t.torus_knot_path(1, 1, 32);
1361 for p in &pts {
1362 assert!(t.sdf(*p).abs() < 1e-8);
1363 }
1364 }
1365
1366 #[test]
1367 fn test_torus_area_element_factor_positive() {
1368 let t = Torus::new(3.0, 1.0);
1369 for i in 0..32 {
1371 let v = 2.0 * PI * i as f64 / 32.0;
1372 assert!(t.area_element_factor(v) > 0.0, "factor at v={v}");
1373 }
1374 }
1375}