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 #[allow(dead_code)]
55 pub fn inertia_tensor_array(&self, mass: Real) -> [[f64; 3]; 3] {
56 let r = self.major_radius;
57 let a = self.minor_radius;
58 let iy = mass * (r * r + (3.0 / 4.0) * a * a);
59 let ixz = mass * ((5.0 / 8.0) * a * a + r * r / 2.0);
60 [[ixz, 0.0, 0.0], [0.0, iy, 0.0], [0.0, 0.0, ixz]]
61 }
62
63 #[allow(dead_code)]
71 pub fn ray_cast_array(
72 &self,
73 origin: [f64; 3],
74 direction: [f64; 3],
75 max_toi: f64,
76 ) -> Option<(f64, [f64; 3])> {
77 let o = Vec3::new(origin[0], origin[1], origin[2]);
78 let d = Vec3::new(direction[0], direction[1], direction[2]);
79 let hit = self.ray_cast_impl(&o, &d, max_toi)?;
80 Some((hit.toi, [hit.normal.x, hit.normal.y, hit.normal.z]))
81 }
82
83 #[allow(dead_code)]
89 pub fn closest_point(&self, p: [f64; 3]) -> [f64; 3] {
90 let px = p[0];
91 let py = p[1];
92 let pz = p[2];
93 let xz_len = (px * px + pz * pz).sqrt();
95 let (ring_x, ring_z) = if xz_len < 1e-12 {
96 (self.major_radius, 0.0)
98 } else {
99 let s = self.major_radius / xz_len;
100 (px * s, pz * s)
101 };
102 let dx = px - ring_x;
104 let dy = py;
105 let dz = pz - ring_z;
106 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
107 if dist < 1e-12 {
108 return [ring_x, self.minor_radius, ring_z];
110 }
111 let s = self.minor_radius / dist;
112 [ring_x + dx * s, dy * s, ring_z + dz * s]
113 }
114
115 #[allow(dead_code)]
117 pub fn contains_point(&self, p: [f64; 3]) -> bool {
118 let px = p[0];
119 let py = p[1];
120 let pz = p[2];
121 let xz = (px * px + pz * pz).sqrt();
122 let dist_to_ring = ((xz - self.major_radius) * (xz - self.major_radius) + py * py).sqrt();
123 dist_to_ring <= self.minor_radius
124 }
125
126 #[allow(dead_code)]
133 pub fn sample_surface(&self, u: f64, v: f64) -> [f64; 3] {
134 let r_outer = self.major_radius + self.minor_radius * v.cos();
135 [
136 r_outer * u.cos(),
137 self.minor_radius * v.sin(),
138 r_outer * u.sin(),
139 ]
140 }
141
142 #[allow(dead_code)]
148 pub fn sdf(&self, p: [f64; 3]) -> f64 {
149 let xz = (p[0] * p[0] + p[2] * p[2]).sqrt();
150 let dist_to_ring = ((xz - self.major_radius).powi(2) + p[1] * p[1]).sqrt();
151 dist_to_ring - self.minor_radius
152 }
153
154 #[allow(dead_code)]
156 pub fn ray_torus_analytic(
157 &self,
158 origin: [f64; 3],
159 direction: [f64; 3],
160 max_toi: f64,
161 ) -> Option<(f64, [f64; 3])> {
162 self.ray_cast_array(origin, direction, max_toi)
163 }
164
165 #[allow(dead_code)]
167 pub fn support_array(&self, direction: [f64; 3]) -> [f64; 3] {
168 let d = Vec3::new(direction[0], direction[1], direction[2]);
169 let sp = self.support_point(&d);
170 [sp.x, sp.y, sp.z]
171 }
172
173 #[allow(dead_code)]
179 pub fn surface_parameters(&self, p: [f64; 3]) -> (f64, f64) {
180 let xz = (p[0] * p[0] + p[2] * p[2]).sqrt();
181 let u = p[2].atan2(p[0]); let rx = self.major_radius * u.cos();
186 let rz = self.major_radius * u.sin();
187
188 let dx = p[0] - rx;
190 let dy = p[1];
191 let dz = p[2] - rz;
192
193 let radial = xz - self.major_radius;
196 let v = dy.atan2(radial);
197
198 let _ = (dx, dz); (u, v)
200 }
201
202 #[allow(dead_code)]
207 pub fn random_surface_points(&self, n: usize, seed: u64) -> Vec<[f64; 3]> {
208 let mut points = Vec::with_capacity(n);
209 let mut state = seed;
210
211 let next_f64 = |s: &mut u64| -> f64 {
212 *s ^= *s << 13;
213 *s ^= *s >> 7;
214 *s ^= *s << 17;
215 (*s as f64) / (u64::MAX as f64)
216 };
217
218 let max_weight = self.major_radius + self.minor_radius;
221
222 while points.len() < n {
223 let u = next_f64(&mut state) * 2.0 * PI;
224 let v = next_f64(&mut state) * 2.0 * PI;
225 let w = next_f64(&mut state);
226
227 let weight = (self.major_radius + self.minor_radius * v.cos()) / max_weight;
228 if w <= weight {
229 points.push(self.sample_surface(u, v));
230 }
231 }
232 points
233 }
234
235 #[allow(dead_code)]
237 pub fn inertia_raw(&self, mass: f64) -> [[f64; 3]; 3] {
238 self.inertia_tensor_array(mass)
239 }
240
241 #[allow(dead_code)]
243 pub fn outer_radius(&self) -> f64 {
244 self.major_radius + self.minor_radius
245 }
246
247 #[allow(dead_code)]
249 pub fn inner_radius(&self) -> f64 {
250 (self.major_radius - self.minor_radius).max(0.0)
251 }
252
253 #[allow(dead_code)]
261 pub fn uv_map(&self, p: [f64; 3]) -> [f64; 2] {
262 let (theta, phi) = self.surface_parameters(p);
263 let u = ((theta / (2.0 * PI)) + 1.0) % 1.0;
265 let v = ((phi / (2.0 * PI)) + 1.0) % 1.0;
266 [u, v]
267 }
268
269 #[allow(dead_code)]
275 pub fn geodesic_distance_flat(&self, a: [f64; 3], b: [f64; 3]) -> f64 {
276 let (ua, va) = self.surface_parameters(a);
277 let (ub, vb) = self.surface_parameters(b);
278 let d_theta = angle_diff(ua, ub);
279 let d_phi = angle_diff(va, vb);
280 let arc_major = self.major_radius * d_theta;
281 let arc_minor = self.minor_radius * d_phi;
282 (arc_major * arc_major + arc_minor * arc_minor).sqrt()
283 }
284
285 #[allow(dead_code)]
289 pub fn area_element_factor(&self, v: f64) -> f64 {
290 self.major_radius + self.minor_radius * v.cos()
291 }
292
293 #[allow(dead_code)]
297 pub fn surface_area_numeric(&self, n_steps: usize) -> f64 {
298 let n = n_steps.max(4);
299 let du = 2.0 * PI / n as f64;
300 let dv = 2.0 * PI / n as f64;
301 let mut sum = 0.0;
302 for iv in 0..n {
303 let v = (iv as f64 + 0.5) * dv;
304 let factor = self.area_element_factor(v);
305 sum += factor * n as f64; }
307 sum * self.minor_radius * du * dv
308 }
309
310 #[allow(dead_code)]
314 pub fn tube_cross_section(&self, u: f64, n: usize) -> Vec<[f64; 3]> {
315 let n = n.max(3);
316 let ring_x = self.major_radius * u.cos();
317 let ring_z = self.major_radius * u.sin();
318 (0..n)
319 .map(|i| {
320 let v = 2.0 * PI * i as f64 / n as f64;
321 let rx = u.cos();
323 let rz = u.sin();
324 let cx = ring_x + self.minor_radius * v.cos() * rx;
325 let cy = self.minor_radius * v.sin();
326 let cz = ring_z + self.minor_radius * v.cos() * rz;
327 [cx, cy, cz]
328 })
329 .collect()
330 }
331
332 #[allow(dead_code)]
337 pub fn torus_knot_path(&self, p: i32, q: i32, n_pts: usize) -> Vec<[f64; 3]> {
338 let n = n_pts.max(3);
339 (0..n)
340 .map(|i| {
341 let t = 2.0 * PI * i as f64 / n as f64;
342 let u = p as f64 * t;
343 let v = q as f64 * t;
344 self.sample_surface(u, v)
345 })
346 .collect()
347 }
348
349 #[allow(dead_code)]
356 pub fn winding_number_major(&self, curve: &[[f64; 3]]) -> i32 {
357 if curve.len() < 2 {
358 return 0;
359 }
360 let angles: Vec<f64> = curve
361 .iter()
362 .map(|&p| self.surface_parameters(p).0)
363 .collect();
364 let mut total = 0.0_f64;
365 for i in 0..angles.len() {
366 let next = (i + 1) % angles.len();
367 let diff = angle_diff(angles[i], angles[next]);
368 total += diff;
369 }
370 (total / (2.0 * PI)).round() as i32
371 }
372
373 #[allow(dead_code)]
377 pub fn tangent_u(&self, u: f64, v: f64) -> [f64; 3] {
378 let r_outer = self.major_radius + self.minor_radius * v.cos();
379 [-r_outer * u.sin(), 0.0, r_outer * u.cos()]
380 }
381
382 #[allow(dead_code)]
386 pub fn tangent_v(&self, u: f64, v: f64) -> [f64; 3] {
387 let r = self.minor_radius;
388 [-r * v.sin() * u.cos(), r * v.cos(), -r * v.sin() * u.sin()]
389 }
390
391 #[allow(dead_code)]
395 pub fn normal_from_tangents(&self, u: f64, v: f64) -> [f64; 3] {
396 let tu = self.tangent_u(u, v);
397 let tv = self.tangent_v(u, v);
398 let n = cross3([tu[0], tu[1], tu[2]], [tv[0], tv[1], tv[2]]);
399 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
400 if len < 1e-14 {
401 [0.0, 1.0, 0.0]
402 } else {
403 [n[0] / len, n[1] / len, n[2] / len]
404 }
405 }
406
407 #[allow(dead_code)]
413 pub fn ray_intersection_count(
414 &self,
415 origin: [f64; 3],
416 direction: [f64; 3],
417 max_toi: f64,
418 ) -> usize {
419 let o = Vec3::new(origin[0], origin[1], origin[2]);
420 let d = Vec3::new(direction[0], direction[1], direction[2]);
421 let dir_len = d.norm();
422 if dir_len < 1e-12 {
423 return 0;
424 }
425 let dn = d / dir_len;
426
427 let oo = o.dot(&o);
428 let od = o.dot(&dn);
429 let rr = self.major_radius * self.major_radius;
430 let aa = self.minor_radius * self.minor_radius;
431 let c_alpha = oo + rr - aa;
432 let beta = 2.0 * od;
433 let dxz2 = dn.x * dn.x + dn.z * dn.z;
434 let od_xz = o.x * dn.x + o.z * dn.z;
435 let oxz2 = o.x * o.x + o.z * o.z;
436 let c4 = 1.0;
437 let c3 = 2.0 * beta;
438 let c2 = beta * beta + 2.0 * c_alpha - 4.0 * rr * dxz2;
439 let c1 = 2.0 * beta * c_alpha - 8.0 * rr * od_xz;
440 let c0 = c_alpha * c_alpha - 4.0 * rr * oxz2;
441 let roots = solve_quartic(c4, c3, c2, c1, c0);
442
443 roots
444 .iter()
445 .filter(|&&t| {
446 !t.is_nan() && !t.is_infinite() && {
447 let t_actual = t / dir_len;
448 t_actual >= 1e-6 && t_actual <= max_toi
449 }
450 })
451 .count()
452 }
453
454 #[allow(dead_code)]
459 pub fn intersects_aabb(&self, aabb_min: [f64; 3], aabb_max: [f64; 3]) -> bool {
460 let corners = [
461 [aabb_min[0], aabb_min[1], aabb_min[2]],
462 [aabb_max[0], aabb_min[1], aabb_min[2]],
463 [aabb_min[0], aabb_max[1], aabb_min[2]],
464 [aabb_max[0], aabb_max[1], aabb_min[2]],
465 [aabb_min[0], aabb_min[1], aabb_max[2]],
466 [aabb_max[0], aabb_min[1], aabb_max[2]],
467 [aabb_min[0], aabb_max[1], aabb_max[2]],
468 [aabb_max[0], aabb_max[1], aabb_max[2]],
469 ];
470 let sdfs: Vec<f64> = corners.iter().map(|&c| self.sdf(c)).collect();
472 let min_sdf = sdfs.iter().cloned().fold(f64::INFINITY, f64::min);
473 min_sdf <= 0.0
474 }
475
476 #[allow(dead_code)]
481 pub fn aspect_ratio(&self) -> f64 {
482 self.major_radius / self.minor_radius.max(1e-30)
483 }
484
485 #[allow(dead_code)]
490 pub fn major_circle_points(&self, n: usize) -> Vec<[f64; 3]> {
491 let n = n.max(2);
492 (0..n)
493 .map(|i| {
494 let u = 2.0 * PI * i as f64 / n as f64;
495 [
496 self.major_radius * u.cos(),
497 0.0,
498 self.major_radius * u.sin(),
499 ]
500 })
501 .collect()
502 }
503
504 fn ray_cast_impl(&self, origin: &Vec3, dir: &Vec3, max_toi: Real) -> Option<RayHit> {
508 let r_big = self.major_radius;
509 let r_small = self.minor_radius;
510
511 let dir_len = dir.norm();
513 if dir_len < 1e-12 {
514 return None;
515 }
516 let d = dir / dir_len;
517 let o = *origin;
518
519 let oo = o.dot(&o);
522 let od = o.dot(&d);
523 let _dd = d.dot(&d); let rr = r_big * r_big;
525 let aa = r_small * r_small;
526 let alpha = oo - rr - aa; let beta = 2.0 * od;
528 let dxz2 = d.x * d.x + d.z * d.z;
536 let od_xz = o.x * d.x + o.z * d.z;
537 let oxz2 = o.x * o.x + o.z * o.z;
538
539 let c_alpha = oo + rr - aa; let c4 = 1.0;
543 let c3 = 2.0 * beta;
544 let c2 = beta * beta + 2.0 * c_alpha - 4.0 * rr * dxz2;
545 let c1 = 2.0 * beta * c_alpha - 8.0 * rr * od_xz;
546 let c0 = c_alpha * c_alpha - 4.0 * rr * oxz2;
547 let _ = alpha; let roots = solve_quartic(c4, c3, c2, c1, c0);
553
554 let mut best_t = Real::INFINITY;
555 for t_raw in roots {
556 if t_raw.is_nan() || t_raw.is_infinite() {
557 continue;
558 }
559 let t = t_raw / dir_len;
561 if t >= 1e-6 && t <= max_toi && t < best_t {
562 best_t = t;
563 }
564 }
565 if best_t.is_infinite() {
566 return None;
567 }
568
569 let point = o + d * (best_t * dir_len);
570 let normal = torus_normal(&point, r_big);
571 Some(RayHit {
572 point,
573 normal,
574 toi: best_t,
575 })
576 }
577}
578
579fn angle_diff(a: f64, b: f64) -> f64 {
581 let d = b - a;
582 d - (2.0 * PI) * ((d + PI) / (2.0 * PI)).floor()
585}
586
587fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
589 [
590 a[1] * b[2] - a[2] * b[1],
591 a[2] * b[0] - a[0] * b[2],
592 a[0] * b[1] - a[1] * b[0],
593 ]
594}
595
596fn torus_normal(p: &Vec3, big_r: Real) -> Vec3 {
598 let xz = (p.x * p.x + p.z * p.z).sqrt();
599 if xz < 1e-12 {
600 return Vec3::new(0.0, 1.0, 0.0);
601 }
602 let s = big_r / xz;
604 let ring = Vec3::new(p.x * s, 0.0, p.z * s);
605 let n = p - ring;
606 let len = n.norm();
607 if len < 1e-12 {
608 Vec3::new(p.x / xz, 0.0, p.z / xz)
609 } else {
610 n / len
611 }
612}
613
614fn solve_quartic(c4: f64, c3: f64, c2: f64, c1: f64, c0: f64) -> [f64; 4] {
617 let a = c3 / c4;
619 let b = c2 / c4;
620 let c = c1 / c4;
621 let d = c0 / c4;
622
623 let p_depr = b - (3.0 * a * a) / 8.0;
625 let q_depr = (a * a * a) / 8.0 - (a * b) / 2.0 + c;
626 let r_depr = -(3.0 * a * a * a * a) / 256.0 + (a * a * b) / 16.0 - (a * c) / 4.0 + d;
627
628 let shift = a / 4.0;
629
630 let (m1, m2, m3) = solve_cubic_3roots(
634 8.0,
635 8.0 * p_depr,
636 2.0 * p_depr * p_depr - 8.0 * r_depr,
637 -(q_depr * q_depr),
638 );
639
640 let mut roots = [f64::NAN; 4];
641 let mut ri = 0usize;
642
643 for m in [m1, m2, m3] {
644 if m.is_nan() {
645 continue;
646 }
647 let two_m_p = 2.0 * m + p_depr;
648 if two_m_p < 0.0 {
649 continue;
650 }
651 let sqrt_2mp = two_m_p.sqrt();
652 if sqrt_2mp < 1e-14 {
653 continue;
654 }
655
656 let half_q_over = q_depr / (2.0 * sqrt_2mp);
660
661 for &(s, c_q) in &[(1.0_f64, m + half_q_over), (-1.0_f64, m - half_q_over)] {
662 let disc = (s * sqrt_2mp) * (s * sqrt_2mp) / 4.0 - c_q;
663 let disc2 = sqrt_2mp * sqrt_2mp / 4.0 - c_q;
665 if disc2 >= -1e-9 {
666 let sd = disc2.max(0.0).sqrt();
667 let half_b = s * sqrt_2mp / 2.0;
668 let r0 = -half_b + sd - shift;
669 let r1 = -half_b - sd - shift;
670 if ri < 4 {
671 roots[ri] = r0;
672 ri += 1;
673 }
674 if ri < 4 {
675 roots[ri] = r1;
676 ri += 1;
677 }
678 }
679 let _ = disc; }
681 if ri >= 4 {
682 break;
683 }
684 }
685 roots
686}
687
688fn solve_cubic_3roots(a: f64, b: f64, c: f64, d: f64) -> (f64, f64, f64) {
691 let b = b / a;
693 let c = c / a;
694 let d = d / a;
695
696 let p = c - b * b / 3.0;
698 let q = 2.0 * b * b * b / 27.0 - b * c / 3.0 + d;
699 let shift = b / 3.0;
700
701 let discriminant = -(4.0 * p * p * p + 27.0 * q * q);
702
703 if discriminant >= 0.0 {
704 let m = 2.0 * (-p / 3.0).sqrt();
706 let theta = ((3.0 * q) / (2.0 * p) * ((-3.0) / p).sqrt())
707 .clamp(-1.0, 1.0)
708 .acos();
709 let r0 = m * (theta / 3.0).cos() - shift;
710 let r1 = m * ((theta - 2.0 * PI) / 3.0).cos() - shift;
711 let r2 = m * ((theta - 4.0 * PI) / 3.0).cos() - shift;
712 (r0, r1, r2)
713 } else {
714 let d_val = -(4.0 * p * p * p + 27.0 * q * q);
716 let inner = -q / 2.0;
717 let outer = (q * q / 4.0 + p * p * p / 27.0).sqrt();
718 let u = cbrt_real(inner + outer);
719 let v = cbrt_real(inner - outer);
720 let r0 = u + v - shift;
721 let _ = d_val; (r0, f64::NAN, f64::NAN)
723 }
724}
725
726fn cbrt_real(x: f64) -> f64 {
727 if x >= 0.0 { x.cbrt() } else { -((-x).cbrt()) }
728}
729
730impl Shape for Torus {
731 fn bounding_box(&self) -> Aabb {
732 let half = self.bounding_box_extents();
733 Aabb::new(-half, half)
734 }
735
736 fn support_point(&self, direction: &Vec3) -> Vec3 {
737 let eps = 1e-10;
738 let dir_xz = Vec3::new(direction.x, 0.0, direction.z);
740 let xz_len = dir_xz.norm();
741 let xz_norm = if xz_len < eps {
742 Vec3::new(1.0, 0.0, 0.0)
743 } else {
744 dir_xz / xz_len
745 };
746 let tube_center = xz_norm * self.major_radius;
748 let dir_len = direction.norm();
750 let dir_norm = if dir_len < eps {
751 Vec3::new(1.0, 0.0, 0.0)
752 } else {
753 direction / dir_len
754 };
755 tube_center + dir_norm * self.minor_radius
756 }
757
758 fn volume(&self) -> Real {
759 self.volume()
760 }
761
762 fn center_of_mass(&self) -> Vec3 {
763 Vec3::zeros()
764 }
765
766 fn inertia_tensor(&self, mass: Real) -> Mat3 {
767 let r = self.major_radius;
768 let a = self.minor_radius;
769 let iy = mass * (r * r + (3.0 / 4.0) * a * a);
774 let ixz = mass * ((5.0 / 8.0) * a * a + r * r / 2.0);
775 Mat3::new(ixz, 0.0, 0.0, 0.0, iy, 0.0, 0.0, 0.0, ixz)
776 }
777
778 fn mass_properties(&self, density: Real) -> oxiphysics_core::MassProperties {
779 let mass = density * self.volume();
780 oxiphysics_core::MassProperties::new(mass, self.center_of_mass(), self.inertia_tensor(mass))
781 }
782
783 fn ray_cast(&self, ray_origin: &Vec3, ray_direction: &Vec3, max_toi: Real) -> Option<RayHit> {
784 self.ray_cast_impl(ray_origin, ray_direction, max_toi)
785 }
786}
787
788#[cfg(test)]
789mod tests {
790 use super::*;
791
792 #[test]
793 fn test_torus_volume() {
794 let t = Torus::new(1.0, 0.5);
795 let expected = 2.0 * PI * PI * 1.0 * 0.25;
797 assert!(
798 (t.volume() - expected).abs() < 1e-6,
799 "volume={}, expected={}",
800 t.volume(),
801 expected
802 );
803 assert!(
805 (t.volume() - 4.935).abs() < 0.01,
806 "volume ≈ 4.935, got {}",
807 t.volume()
808 );
809 }
810
811 #[test]
812 fn test_torus_surface_area() {
813 let t = Torus::new(1.0, 0.5);
814 let expected = 4.0 * PI * PI * 1.0 * 0.5;
816 assert!(
817 (t.surface_area() - expected).abs() < 1e-6,
818 "surface_area={}, expected={}",
819 t.surface_area(),
820 expected
821 );
822 assert!(
823 (t.surface_area() - 19.74).abs() < 0.01,
824 "surface_area ≈ 19.74, got {}",
825 t.surface_area()
826 );
827 }
828
829 #[test]
830 fn test_torus_bounding_box() {
831 let t = Torus::new(2.0, 0.5);
832 let extents = t.bounding_box_extents();
834 assert!((extents.x - 2.5).abs() < 1e-10, "extents.x={}", extents.x);
835 assert!((extents.y - 0.5).abs() < 1e-10, "extents.y={}", extents.y);
836 assert!((extents.z - 2.5).abs() < 1e-10, "extents.z={}", extents.z);
837 let bb = t.bounding_box();
838 assert!((bb.min.x + 2.5).abs() < 1e-10);
839 assert!((bb.max.x - 2.5).abs() < 1e-10);
840 assert!((bb.min.y + 0.5).abs() < 1e-10);
841 assert!((bb.max.y - 0.5).abs() < 1e-10);
842 }
843
844 #[test]
845 fn test_torus_support_xz() {
846 let t = Torus::new(1.0, 0.5);
847 let dir = Vec3::new(1.0, 0.0, 0.0);
849 let sp = t.support_point(&dir);
850 let expected = 1.0 + 0.5; assert!(
852 (sp.x - expected).abs() < 1e-10,
853 "support.x={}, expected={}",
854 sp.x,
855 expected
856 );
857 assert!(sp.y.abs() < 1e-10, "support.y={}", sp.y);
858 assert!(sp.z.abs() < 1e-10, "support.z={}", sp.z);
859 }
860
861 #[test]
862 fn test_torus_support_y() {
863 let t = Torus::new(1.0, 0.5);
864 let dir = Vec3::new(0.0, 1.0, 0.0);
866 let sp = t.support_point(&dir);
867 assert!(
870 (sp.y - 0.5).abs() < 1e-10,
871 "support.y={}, expected minor_radius=0.5",
872 sp.y
873 );
874 }
875
876 #[test]
877 fn test_torus_mass_properties() {
878 let t = Torus::new(1.0, 0.5);
879 let density = 1000.0;
880 let mp = t.mass_properties(density);
881 let expected_mass = density * t.volume();
882 assert!(
883 (mp.mass - expected_mass).abs() < 1e-6,
884 "mass={}, expected={}",
885 mp.mass,
886 expected_mass
887 );
888 assert!(mp.mass > 0.0, "mass should be positive");
889 assert!(
890 mp.local_inertia[(0, 0)] > 0.0,
891 "I_xx should be positive: {}",
892 mp.local_inertia[(0, 0)]
893 );
894 assert!(
895 mp.local_inertia[(1, 1)] > 0.0,
896 "I_yy should be positive: {}",
897 mp.local_inertia[(1, 1)]
898 );
899 assert!(
900 mp.local_inertia[(2, 2)] > 0.0,
901 "I_zz should be positive: {}",
902 mp.local_inertia[(2, 2)]
903 );
904 }
905
906 #[test]
907 fn test_torus_contains_point_inside() {
908 let t = Torus::new(3.0, 1.0);
909 assert!(
911 t.contains_point([3.0, 0.0, 0.0]),
912 "ring centre should be inside"
913 );
914 assert!(
916 !t.contains_point([0.0, 0.0, 0.0]),
917 "origin should be outside"
918 );
919 assert!(
921 t.contains_point([3.0, 1.0, 0.0]),
922 "top of tube should be inside/on"
923 );
924 assert!(
926 !t.contains_point([3.0, 1.1, 0.0]),
927 "beyond tube should be outside"
928 );
929 }
930
931 #[test]
932 fn test_torus_closest_point_on_surface() {
933 let t = Torus::new(3.0, 1.0);
934 let cp = t.closest_point([10.0, 0.0, 0.0]);
936 assert!((cp[0] - 4.0).abs() < 1e-6, "cp.x={}, expected 4.0", cp[0]);
938 assert!(cp[1].abs() < 1e-6, "cp.y={}", cp[1]);
939 assert!(cp[2].abs() < 1e-6, "cp.z={}", cp[2]);
940 }
941
942 #[test]
943 fn test_torus_sample_surface_on_torus() {
944 let t = Torus::new(3.0, 1.0);
945 let p = t.sample_surface(0.0, 0.0);
947 assert!((p[0] - 4.0).abs() < 1e-10, "p.x={}", p[0]);
948 assert!(p[1].abs() < 1e-10, "p.y={}", p[1]);
949 assert!(
951 t.contains_point(p),
952 "sampled point {:?} should be on torus surface",
953 p
954 );
955 let p2 = t.sample_surface(PI / 2.0, PI / 2.0);
957 assert!(p2[0].abs() < 1e-10, "p2.x={}", p2[0]);
959 assert!((p2[1] - 1.0).abs() < 1e-10, "p2.y={}", p2[1]);
960 assert!((p2[2] - 3.0).abs() < 1e-10, "p2.z={}", p2[2]);
961 }
962
963 #[test]
964 fn test_torus_inertia_tensor_array() {
965 let t = Torus::new(2.0, 0.5);
966 let it = t.inertia_tensor_array(10.0);
967 assert!(it[0][0] > 0.0, "Ixx={}", it[0][0]);
969 assert!(it[1][1] > 0.0, "Iyy={}", it[1][1]);
970 assert!(it[2][2] > 0.0, "Izz={}", it[2][2]);
971 assert!(it[0][1].abs() < 1e-12);
973 assert!(it[0][2].abs() < 1e-12);
974 assert!((it[0][0] - it[2][2]).abs() < 1e-10);
976 }
977
978 #[test]
979 fn test_torus_ray_cast_hit() {
980 let t = Torus::new(3.0, 1.0);
981 let origin = Vec3::new(3.0, -10.0, 0.0);
983 let dir = Vec3::new(0.0, 1.0, 0.0);
984 let hit = t.ray_cast(&origin, &dir, 100.0);
985 assert!(hit.is_some(), "ray through tube should hit");
986 let hit = hit.unwrap();
987 assert!(
989 hit.toi > 0.0 && hit.toi < 20.0,
990 "toi should be reasonable, got {}",
991 hit.toi
992 );
993 }
994
995 #[test]
996 fn test_torus_ray_cast_miss() {
997 let t = Torus::new(3.0, 1.0);
998 let origin = Vec3::new(0.0, 10.0, 0.0);
1000 let dir = Vec3::new(0.0, 1.0, 0.0);
1001 let hit = t.ray_cast(&origin, &dir, 5.0);
1002 assert!(hit.is_none(), "ray going away should miss");
1003 }
1004
1005 #[test]
1006 fn test_torus_ray_cast_array() {
1007 let t = Torus::new(3.0, 1.0);
1008 let result = t.ray_cast_array([3.0, -10.0, 0.0], [0.0, 1.0, 0.0], 100.0);
1009 assert!(result.is_some(), "ray should hit");
1010 let (toi, _n) = result.unwrap();
1011 assert!(
1012 toi > 0.0 && toi < 20.0,
1013 "toi should be reasonable, got {}",
1014 toi
1015 );
1016 }
1017
1018 #[test]
1021 fn test_torus_sdf_inside_tube() {
1022 let t = Torus::new(3.0, 1.0);
1023 let p = [3.0, 0.0, 0.0];
1025 assert!(
1026 (t.sdf(p) + 1.0).abs() < 1e-10,
1027 "sdf at ring center should be -1, got {}",
1028 t.sdf(p)
1029 );
1030 }
1031
1032 #[test]
1033 fn test_torus_sdf_outside() {
1034 let t = Torus::new(3.0, 1.0);
1035 let p = [10.0, 0.0, 0.0];
1037 assert!(t.sdf(p) > 0.0, "sdf outside should be positive");
1038 }
1039
1040 #[test]
1041 fn test_torus_sdf_on_surface() {
1042 let t = Torus::new(3.0, 1.0);
1043 let p = [4.0, 0.0, 0.0];
1045 assert!(
1046 t.sdf(p).abs() < 1e-10,
1047 "sdf at surface should be ~0, got {}",
1048 t.sdf(p)
1049 );
1050 }
1051
1052 #[test]
1053 fn test_torus_ray_torus_analytic() {
1054 let t = Torus::new(3.0, 1.0);
1055 let result = t.ray_torus_analytic([3.0, -10.0, 0.0], [0.0, 1.0, 0.0], 100.0);
1056 assert!(result.is_some());
1057 }
1058
1059 #[test]
1060 fn test_torus_support_array_xplus() {
1061 let t = Torus::new(2.0, 0.5);
1062 let sp = t.support_array([1.0, 0.0, 0.0]);
1063 assert!((sp[0] - 2.5).abs() < 1e-10, "sp.x={}", sp[0]);
1065 assert!(sp[1].abs() < 1e-10);
1066 }
1067
1068 #[test]
1069 fn test_torus_surface_parameters_roundtrip() {
1070 let t = Torus::new(3.0, 1.0);
1071 let u_in = 1.0_f64;
1072 let v_in = 0.5_f64;
1073 let p = t.sample_surface(u_in, v_in);
1074 let (u_out, v_out) = t.surface_parameters(p);
1075 let du = (u_out - u_in).abs();
1077 let du_mod = du.min((du - 2.0 * PI).abs());
1078 assert!(du_mod < 1e-9, "u mismatch: in={u_in}, out={u_out}");
1079 assert!(
1080 (v_out - v_in).abs() < 1e-9,
1081 "v mismatch: in={v_in}, out={v_out}"
1082 );
1083 }
1084
1085 #[test]
1086 fn test_torus_random_surface_points_count() {
1087 let t = Torus::new(2.0, 0.5);
1088 let pts = t.random_surface_points(40, 777);
1089 assert_eq!(pts.len(), 40);
1090 }
1091
1092 #[test]
1093 fn test_torus_random_surface_points_on_surface() {
1094 let t = Torus::new(3.0, 0.5);
1095 let pts = t.random_surface_points(60, 42);
1096 for p in &pts {
1097 let sdf = t.sdf(*p);
1098 assert!(sdf.abs() < 1e-6, "point {:?} sdf={sdf}", p);
1099 }
1100 }
1101
1102 #[test]
1103 fn test_torus_outer_inner_radius() {
1104 let t = Torus::new(3.0, 1.0);
1105 assert!((t.outer_radius() - 4.0).abs() < 1e-12);
1106 assert!((t.inner_radius() - 2.0).abs() < 1e-12);
1107 }
1108
1109 #[test]
1110 fn test_torus_inner_radius_clamped() {
1111 let t = Torus::new(0.5, 1.0); assert_eq!(t.inner_radius(), 0.0);
1113 }
1114
1115 #[test]
1116 fn test_torus_inertia_raw_matches_array() {
1117 let t = Torus::new(2.0, 0.5);
1118 let raw = t.inertia_raw(5.0);
1119 let arr = t.inertia_tensor_array(5.0);
1120 for i in 0..3 {
1121 for j in 0..3 {
1122 assert!((raw[i][j] - arr[i][j]).abs() < 1e-12);
1123 }
1124 }
1125 }
1126
1127 #[test]
1130 fn test_torus_uv_map_roundtrip() {
1131 let t = Torus::new(3.0, 1.0);
1132 let p = t.sample_surface(1.2, 0.8);
1133 let [u, v] = t.uv_map(p);
1134 assert!((0.0..1.0).contains(&u), "u={u}");
1136 assert!((0.0..1.0).contains(&v), "v={v}");
1137 let p2 = t.sample_surface(u * 2.0 * PI, v * 2.0 * PI);
1139 let d = ((p[0] - p2[0]).powi(2) + (p[1] - p2[1]).powi(2) + (p[2] - p2[2]).powi(2)).sqrt();
1140 assert!(d < 1e-9, "roundtrip error={d}");
1141 }
1142
1143 #[test]
1144 fn test_torus_uv_map_zero() {
1145 let t = Torus::new(2.0, 0.5);
1146 let p = [t.major_radius + t.minor_radius, 0.0, 0.0];
1148 let [u, v] = t.uv_map(p);
1149 assert!(u.abs() < 1e-9 || (u - 1.0).abs() < 1e-9, "u={u}");
1150 assert!(v.abs() < 1e-9 || (v - 1.0).abs() < 1e-9, "v={v}");
1151 }
1152
1153 #[test]
1154 fn test_torus_geodesic_distance_flat_same_point() {
1155 let t = Torus::new(3.0, 1.0);
1156 let p = t.sample_surface(0.5, 0.3);
1157 let d = t.geodesic_distance_flat(p, p);
1158 assert!(d.abs() < 1e-9, "distance to self should be 0, got {d}");
1159 }
1160
1161 #[test]
1162 fn test_torus_geodesic_distance_flat_opposite_minor() {
1163 let t = Torus::new(3.0, 1.0);
1164 let p1 = t.sample_surface(0.0, 0.0);
1165 let p2 = t.sample_surface(0.0, PI);
1166 let d = t.geodesic_distance_flat(p1, p2);
1168 let expected = PI * t.minor_radius;
1169 assert!((d - expected).abs() < 1e-9, "d={d} expected={expected}");
1170 }
1171
1172 #[test]
1173 fn test_torus_area_element_factor_at_v0() {
1174 let t = Torus::new(3.0, 1.0);
1175 assert!((t.area_element_factor(0.0) - 4.0).abs() < 1e-12);
1177 }
1178
1179 #[test]
1180 fn test_torus_area_element_factor_at_vpi() {
1181 let t = Torus::new(3.0, 1.0);
1182 assert!((t.area_element_factor(PI) - 2.0).abs() < 1e-12);
1184 }
1185
1186 #[test]
1187 fn test_torus_surface_area_numeric_approximation() {
1188 let t = Torus::new(3.0, 1.0);
1189 let analytic = t.surface_area();
1190 let numeric = t.surface_area_numeric(256);
1191 let rel_err = (numeric - analytic).abs() / analytic;
1193 assert!(
1194 rel_err < 0.001,
1195 "numeric={numeric} analytic={analytic} rel_err={rel_err}"
1196 );
1197 }
1198
1199 #[test]
1200 fn test_torus_tube_cross_section_count() {
1201 let t = Torus::new(2.0, 0.5);
1202 let pts = t.tube_cross_section(0.0, 16);
1203 assert_eq!(pts.len(), 16);
1204 }
1205
1206 #[test]
1207 fn test_torus_tube_cross_section_on_torus() {
1208 let t = Torus::new(2.0, 0.5);
1209 let pts = t.tube_cross_section(0.0, 20);
1210 for p in &pts {
1211 let sdf = t.sdf(*p);
1212 assert!(sdf.abs() < 1e-9, "cross-section pt not on torus sdf={sdf}");
1213 }
1214 }
1215
1216 #[test]
1217 fn test_torus_knot_path_count() {
1218 let t = Torus::new(3.0, 1.0);
1219 let pts = t.torus_knot_path(2, 3, 60);
1220 assert_eq!(pts.len(), 60);
1221 }
1222
1223 #[test]
1224 fn test_torus_knot_path_on_surface() {
1225 let t = Torus::new(3.0, 1.0);
1226 let pts = t.torus_knot_path(2, 3, 48);
1227 for p in &pts {
1228 let sdf = t.sdf(*p);
1229 assert!(sdf.abs() < 1e-9, "knot pt sdf={sdf}");
1230 }
1231 }
1232
1233 #[test]
1234 fn test_torus_knot_path_trefoil() {
1235 let t = Torus::new(3.0, 1.0);
1237 let pts = t.torus_knot_path(2, 3, 60);
1238 assert_eq!(pts.len(), 60);
1241 }
1242
1243 #[test]
1244 fn test_torus_winding_number_major_circle() {
1245 let t = Torus::new(3.0, 1.0);
1247 let n = 64;
1248 let curve: Vec<[f64; 3]> = (0..n)
1249 .map(|i| {
1250 let u = 2.0 * PI * i as f64 / n as f64;
1251 t.sample_surface(u, 0.0) })
1253 .collect();
1254 let winding = t.winding_number_major(&curve);
1255 assert_eq!(winding, 1, "should wind once, got {winding}");
1256 }
1257
1258 #[test]
1259 fn test_torus_tangent_u_perpendicular_to_normal() {
1260 let t = Torus::new(3.0, 1.0);
1261 let u = 0.5;
1262 let v = 0.3;
1263 let tu = t.tangent_u(u, v);
1264 let n = t.normal_from_tangents(u, v);
1265 let dot = tu[0] * n[0] + tu[1] * n[1] + tu[2] * n[2];
1266 assert!(dot.abs() < 1e-9, "tangent_u · normal = {dot}");
1267 }
1268
1269 #[test]
1270 fn test_torus_tangent_v_perpendicular_to_normal() {
1271 let t = Torus::new(3.0, 1.0);
1272 let u = 0.5;
1273 let v = 0.3;
1274 let tv = t.tangent_v(u, v);
1275 let n = t.normal_from_tangents(u, v);
1276 let dot = tv[0] * n[0] + tv[1] * n[1] + tv[2] * n[2];
1277 assert!(dot.abs() < 1e-9, "tangent_v · normal = {dot}");
1278 }
1279
1280 #[test]
1281 fn test_torus_normal_from_tangents_unit() {
1282 let t = Torus::new(3.0, 1.0);
1283 let n = t.normal_from_tangents(0.5, 0.3);
1284 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
1285 assert!((len - 1.0).abs() < 1e-9, "normal not unit length={len}");
1286 }
1287
1288 #[test]
1289 fn test_torus_ray_intersection_count_through() {
1290 let t = Torus::new(3.0, 1.0);
1291 let count = t.ray_intersection_count([3.0, -10.0, 0.0], [0.0, 1.0, 0.0], 100.0);
1294 assert!(count >= 2, "expected >=2 intersections, got {count}");
1295 }
1296
1297 #[test]
1298 fn test_torus_ray_intersection_count_miss() {
1299 let t = Torus::new(3.0, 1.0);
1300 let count = t.ray_intersection_count([0.0, 20.0, 0.0], [0.0, 1.0, 0.0], 5.0);
1302 assert_eq!(count, 0, "expected 0 intersections");
1303 }
1304
1305 #[test]
1306 fn test_torus_intersects_aabb_inside() {
1307 let t = Torus::new(3.0, 1.0);
1308 assert!(t.intersects_aabb([2.5, -0.5, -0.5], [3.5, 0.5, 0.5]));
1310 }
1311
1312 #[test]
1313 fn test_torus_intersects_aabb_outside() {
1314 let t = Torus::new(3.0, 1.0);
1315 assert!(!t.intersects_aabb([20.0, 20.0, 20.0], [30.0, 30.0, 30.0]));
1317 }
1318
1319 #[test]
1320 fn test_torus_aspect_ratio() {
1321 let t = Torus::new(4.0, 1.0);
1322 assert!((t.aspect_ratio() - 4.0).abs() < 1e-12);
1323 }
1324
1325 #[test]
1326 fn test_torus_major_circle_points_count() {
1327 let t = Torus::new(3.0, 1.0);
1328 let pts = t.major_circle_points(36);
1329 assert_eq!(pts.len(), 36);
1330 }
1331
1332 #[test]
1333 fn test_torus_major_circle_points_on_ring() {
1334 let t = Torus::new(3.0, 1.0);
1335 let pts = t.major_circle_points(24);
1336 for p in &pts {
1337 let xz = (p[0] * p[0] + p[2] * p[2]).sqrt();
1338 assert!((xz - t.major_radius).abs() < 1e-9, "xz={xz}");
1339 assert!(p[1].abs() < 1e-12, "y={}", p[1]);
1340 }
1341 }
1342
1343 #[test]
1344 fn test_torus_winding_number_single_point() {
1345 let t = Torus::new(3.0, 1.0);
1346 let curve = vec![[3.0, 0.0, 0.0]];
1347 assert_eq!(t.winding_number_major(&curve), 0);
1348 }
1349
1350 #[test]
1351 fn test_torus_winding_number_empty() {
1352 let t = Torus::new(3.0, 1.0);
1353 assert_eq!(t.winding_number_major(&[]), 0);
1354 }
1355
1356 #[test]
1357 fn test_torus_surface_area_numeric_32() {
1358 let t = Torus::new(2.0, 0.5);
1359 let num = t.surface_area_numeric(32);
1360 let ana = t.surface_area();
1361 let err = (num - ana).abs() / ana;
1362 assert!(err < 0.01, "err={err}");
1363 }
1364
1365 #[test]
1366 fn test_torus_tangents_nonzero() {
1367 let t = Torus::new(3.0, 1.0);
1368 let tu = t.tangent_u(0.1, 0.2);
1369 let tv = t.tangent_v(0.1, 0.2);
1370 let len_u = (tu[0] * tu[0] + tu[1] * tu[1] + tu[2] * tu[2]).sqrt();
1371 let len_v = (tv[0] * tv[0] + tv[1] * tv[1] + tv[2] * tv[2]).sqrt();
1372 assert!(len_u > 1e-6);
1373 assert!(len_v > 1e-6);
1374 }
1375
1376 #[test]
1377 fn test_torus_tube_cross_section_minimum_3() {
1378 let t = Torus::new(2.0, 0.5);
1379 let pts = t.tube_cross_section(0.0, 1); assert_eq!(pts.len(), 3);
1381 }
1382
1383 #[test]
1384 fn test_torus_knot_path_trivial_11() {
1385 let t = Torus::new(3.0, 1.0);
1387 let pts = t.torus_knot_path(1, 1, 32);
1388 for p in &pts {
1389 assert!(t.sdf(*p).abs() < 1e-8);
1390 }
1391 }
1392
1393 #[test]
1394 fn test_torus_area_element_factor_positive() {
1395 let t = Torus::new(3.0, 1.0);
1396 for i in 0..32 {
1398 let v = 2.0 * PI * i as f64 / 32.0;
1399 assert!(t.area_element_factor(v) > 0.0, "factor at v={v}");
1400 }
1401 }
1402}