Skip to main content

oxiphysics_geometry/
torus.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Torus shape.
5
6use crate::shape::{RayHit, Shape};
7use oxiphysics_core::Aabb;
8use oxiphysics_core::math::{Mat3, Real, Vec3};
9use std::f64::consts::PI;
10
11/// A torus defined by major radius R (center to tube center) and minor radius r (tube radius).
12///
13/// The torus lies in the XZ plane (Y is the axis of symmetry).
14#[derive(Debug, Clone)]
15pub struct Torus {
16    /// Major radius: distance from the center of the torus to the center of the tube.
17    pub major_radius: Real,
18    /// Minor radius: radius of the tube.
19    pub minor_radius: Real,
20}
21
22impl Torus {
23    /// Create a new torus with the given major and minor radii.
24    pub fn new(major_radius: Real, minor_radius: Real) -> Self {
25        Self {
26            major_radius,
27            minor_radius,
28        }
29    }
30
31    /// Volume = 2π²Rr²
32    pub fn volume(&self) -> Real {
33        2.0 * PI * PI * self.major_radius * self.minor_radius * self.minor_radius
34    }
35
36    /// Surface area = 4π²Rr
37    pub fn surface_area(&self) -> Real {
38        4.0 * PI * PI * self.major_radius * self.minor_radius
39    }
40
41    /// Approximate bounding box half-extents: outer radius = R+r, height = r.
42    ///
43    /// Returns half-extents as `[R+r, r, R+r]` (torus in XZ plane, Y is up).
44    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    /// Inertia tensor for a solid torus of given mass.
50    ///
51    /// Axis of symmetry is Y. Standard formulas:
52    ///   I_y  = m*(R² + (3/4)*r²)  (but commonly written as m*(3R²+4r²)/4 – equivalent)
53    ///   I_xz = m*((5/8)*r² + R²/2) (= m*(5r²+4R²)/8)
54    #[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    /// Cast a ray against the torus using an iterative quartic solver approach.
64    ///
65    /// The torus implicit equation (centered at origin, axis = Y):
66    ///   (x²+y²+z² + R²−r²)² − 4R²(x²+z²) = 0
67    ///
68    /// Substituting P+t*D gives a quartic in t which is solved by Ferrari's method.
69    /// Returns `Some((t, normal))` or `None`.
70    #[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    /// Closest point on the torus surface to point `p`.
84    ///
85    /// Projects `p` onto the nearest point on the ring circle (in XZ), then
86    /// moves from that circle point toward `p` (or outward if p is inside tube)
87    /// by the minor radius.
88    #[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        // Project onto XZ plane to find the nearest point on the ring circle
94        let xz_len = (px * px + pz * pz).sqrt();
95        let (ring_x, ring_z) = if xz_len < 1e-12 {
96            // p is on Y axis; ring point is arbitrary, pick +X
97            (self.major_radius, 0.0)
98        } else {
99            let s = self.major_radius / xz_len;
100            (px * s, pz * s)
101        };
102        // Vector from ring point to p
103        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            // p is exactly on the ring circle – push out along +Y
109            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    /// Returns `true` if point `p` is inside (or on the surface of) the torus tube.
116    #[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    /// Parametric surface sample: `u ∈ [0,2π)`, `v ∈ [0,2π)`.
127    ///
128    /// Returns a point `(x,y,z)` on the torus surface.
129    ///   x = (R + r*cos(v)) * cos(u)
130    ///   y = r * sin(v)
131    ///   z = (R + r*cos(v)) * sin(u)
132    #[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    // ── New expanded methods ──
143
144    /// Torus SDF (signed distance to torus surface).
145    ///
146    /// Negative inside the tube, positive outside.
147    #[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    /// Torus-ray analytic intersection (same as `ray_cast_array` but explicit name).
155    #[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    /// Torus support function (plain array).
166    #[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    /// Surface parameterization: returns the `(u, v)` angles for the point on
174    /// the torus surface closest to `p`.
175    ///
176    /// `u` is the angle around the major circle (XZ plane), `v` is the angle
177    /// around the tube (in the plane through the Y axis and the ring point).
178    #[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        // u: angle in XZ plane
182        let u = p[2].atan2(p[0]); // atan2(z, x)
183
184        // Ring point at angle u
185        let rx = self.major_radius * u.cos();
186        let rz = self.major_radius * u.sin();
187
188        // Vector from ring point to p
189        let dx = p[0] - rx;
190        let dy = p[1];
191        let dz = p[2] - rz;
192
193        // v: angle in the plane of the tube (radial outward from ring center)
194        // Radial direction: (cos u, 0, sin u); Y direction: (0, 1, 0)
195        let radial = xz - self.major_radius;
196        let v = dy.atan2(radial);
197
198        let _ = (dx, dz); // suppress unused warnings
199        (u, v)
200    }
201
202    /// Generate `n` random points uniformly distributed on the torus surface.
203    ///
204    /// Uses a deterministic xorshift64 PRNG seeded with `seed`.
205    /// Sampling: uniform in `u` and `v`, weighted by area element `(R + r cos v)`.
206    #[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        // Rejection sampling: sample (u, v) uniform in [0,2π)²,
219        // accept with probability proportional to (R + r*cos(v)) / (R + r).
220        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    /// Approximate solid torus inertia tensor from `inertia_tensor_array`.
236    #[allow(dead_code)]
237    pub fn inertia_raw(&self, mass: f64) -> [[f64; 3]; 3] {
238        self.inertia_tensor_array(mass)
239    }
240
241    /// Outer radius of the torus (major + minor).
242    #[allow(dead_code)]
243    pub fn outer_radius(&self) -> f64 {
244        self.major_radius + self.minor_radius
245    }
246
247    /// Inner radius of the torus (major - minor, clamped to 0).
248    #[allow(dead_code)]
249    pub fn inner_radius(&self) -> f64 {
250        (self.major_radius - self.minor_radius).max(0.0)
251    }
252
253    // ── Extended geometry: UV mapping, geodesics, tube cross-sections, knot paths, winding ──
254
255    /// UV mapping for the torus surface.
256    ///
257    /// Maps a surface point `p` to texture coordinates `(u, v)` in `[0, 1)²`.
258    /// `u` corresponds to the major (longitudinal) angle and `v` to the minor
259    /// (latitudinal) angle, both normalised to the range `[0, 1)`.
260    #[allow(dead_code)]
261    pub fn uv_map(&self, p: [f64; 3]) -> [f64; 2] {
262        let (theta, phi) = self.surface_parameters(p);
263        // Normalise from (-π, π] to [0, 1)
264        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    /// Approximate geodesic distance between two surface points `a` and `b`
270    /// via the flat-torus metric.
271    ///
272    /// The flat-torus metric: `d = sqrt((R Δθ)² + (r Δφ)²)` where `Δθ` and
273    /// `Δφ` are the wrapped angular differences on the major and minor circles.
274    #[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    /// Area element at angle `v` on the tube: `(R + r cos v) r dv du`.
286    ///
287    /// Returns the area-weighted factor `R + r*cos(v)` for the given tube angle `v`.
288    #[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    /// Approximate surface area by numerical integration (cross-check of closed form).
294    ///
295    /// Uses `n_steps` Gauss-Legendre-like sampling along each angular dimension.
296    #[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; // summing over n u-samples
306        }
307        sum * self.minor_radius * du * dv
308    }
309
310    /// Tube cross-section: return a list of `n` points on the tube circle at angle `u`.
311    ///
312    /// The circle lies in the plane through the ring point at angle `u` and the Y axis.
313    #[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                // Radial direction in XZ plane at angle u
322                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    /// Generate a torus knot path with winding numbers `(p, q)`.
333    ///
334    /// A `(p, q)`-torus knot winds around the major circle `p` times while
335    /// winding around the tube `q` times.  Returns `n_pts` sampled points.
336    #[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    /// Winding number of a closed curve projected onto the torus major circle.
350    ///
351    /// Counts how many times the sequence of `(u, _)` angles (extracted from the
352    /// points via `surface_parameters`) winds around the major circle.
353    ///
354    /// Returns an integer winding count (positive = counter-clockwise).
355    #[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    /// Parametric tangent vector on the torus surface in the `u` direction.
374    ///
375    /// `dP/du` at `(u, v)`: partial derivative with respect to the major angle.
376    #[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    /// Parametric tangent vector on the torus surface in the `v` direction.
383    ///
384    /// `dP/dv` at `(u, v)`: partial derivative with respect to the tube angle.
385    #[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    /// Surface normal via cross product of tangents `dP/du × dP/dv`.
392    ///
393    /// Should agree with `torus_normal` up to sign conventions.
394    #[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    /// Test whether a ray intersects the torus and return the hit count.
408    ///
409    /// A finite ray with origin `o`, direction `d`, and parameter `max_toi` can
410    /// intersect the torus at 0, 2, or 4 points.  This counts all positive-t
411    /// intersections within `[0, max_toi]`.
412    #[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    /// Approximate torus-AABB overlap test via SDF.
455    ///
456    /// Returns `true` if the AABB `[min, max]` overlaps the torus.
457    /// Uses the 8 corners of the AABB as sample points.
458    #[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        // Check if any corner is inside the torus, or if the SDF changes sign
471        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    /// Aspect ratio of the torus: major_radius / minor_radius.
477    ///
478    /// Returns the ratio R/r.  A value close to 1 means the torus is nearly
479    /// self-intersecting; large values mean a thin tube.
480    #[allow(dead_code)]
481    pub fn aspect_ratio(&self) -> f64 {
482        self.major_radius / self.minor_radius.max(1e-30)
483    }
484
485    /// Generate `n` equidistant points along the major circle (the ring itself).
486    ///
487    /// Points lie on the spine of the torus (on the ring circle in the XZ plane,
488    /// at `y = 0`).
489    #[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    // -----------------------------------------------------------------------
505    // Internal ray-cast implementation (quartic via Ferrari / companion matrix).
506    // -----------------------------------------------------------------------
507    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        // Normalize direction for parameter computation; scale toi back after.
512        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        // Coefficients of the quartic  c4*t^4 + c3*t^3 + c2*t^2 + c1*t + c0 = 0
520        // derived from ((ox+t*dx)²+(oy+t*dy)²+(oz+t*dz)² + R²-r²)² = 4R²((ox+t*dx)²+(oz+t*dz)²)
521        let oo = o.dot(&o);
522        let od = o.dot(&d);
523        let _dd = d.dot(&d); // = 1 since d is normalised
524        let rr = r_big * r_big;
525        let aa = r_small * r_small;
526        let alpha = oo - rr - aa; // constant term inside big parens (before squaring)
527        let beta = 2.0 * od;
528        // Let f(t) = oo + 2*od*t + dd*t² + R²-r² -4R²*(... ) is expanded below.
529
530        // Using the parametric form:
531        //   Let S(t) = |o + t*d|² + R² - r² = (dd)*t² + 2*od*t + (oo + R² - r²)
532        //            = t² + beta*t + (oo + rr - aa)   [since dd=1]
533        // Let T(t) = (ox+t*dx)² + (oz+t*dz)²  (XZ only)
534        //          = (dx²+dz²)*t² + 2*(ox*dx+oz*dz)*t + (ox²+oz²)
535        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        // Quartic: (t² + beta*t + (oo+rr-aa))² - 4rr*(dxz2*t² + 2*od_xz*t + oxz2) = 0
540        let c_alpha = oo + rr - aa; // = oo + R² - r²
541        // Expand (t² + beta*t + c_alpha)² = t^4 + 2beta t^3 + (beta²+2c_alpha) t² + 2beta*c_alpha t + c_alpha²
542        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; // suppress unused warning
548
549        // Solve quartic numerically – we use a simple companion-matrix eigenvalue
550        // approach via Durand-Kerner / iterative refinement, or fall back to
551        // a straightforward bisection in likely intervals.
552        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            // Scale back from normalised-direction t to actual t along original dir
560            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
579/// Wrapped angular difference (shortest path), result in `(-π, π]`.
580fn angle_diff(a: f64, b: f64) -> f64 {
581    let d = b - a;
582    // wrap into (-π, π]
583
584    d - (2.0 * PI) * ((d + PI) / (2.0 * PI)).floor()
585}
586
587/// 3-component cross product.
588fn 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
596/// Compute the outward normal on the torus surface at `p`.
597fn 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    // Nearest point on the ring circle
603    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
614/// Solve a monic quartic t^4 + (c3/c4)*t^3 + ... analytically via Ferrari's method
615/// or by numerical polishing.  Returns up to 4 real roots (may include duplicates).
616fn solve_quartic(c4: f64, c3: f64, c2: f64, c1: f64, c0: f64) -> [f64; 4] {
617    // Normalise to monic
618    let a = c3 / c4;
619    let b = c2 / c4;
620    let c = c1 / c4;
621    let d = c0 / c4;
622
623    // Depress the quartic: substitute t = u - a/4
624    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    // Solve the depressed quartic u^4 + p*u^2 + q*u + r = 0 via Ferrari
631    // Resolvent cubic: m^3 + (5p/2)*m^2 + (2p²-r)*m + (p³/2-p*r/2-q²/8-... )
632    // Using standard Ferrari resolvent: 8m³ + 8p*m² + (2p²-8r)*m - q² = 0
633    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        // Two quadratics:
657        // u² + sqrt(2m+p)*u + (m + q/(2*sqrt(2m+p))) = 0
658        // u² - sqrt(2m+p)*u + (m - q/(2*sqrt(2m+p))) = 0
659        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            // discriminant of u² ∓ sqrt(2m+p)*u + c_q = 0 is (sqrt_2mp/2)² - c_q
664            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; // suppress warning
680        }
681        if ri >= 4 {
682            break;
683        }
684    }
685    roots
686}
687
688/// Returns one real root of the cubic a*t³ + b*t² + c*t + d = 0,
689/// plus two more (may be complex → NAN).
690fn solve_cubic_3roots(a: f64, b: f64, c: f64, d: f64) -> (f64, f64, f64) {
691    // Monic: t³ + (b/a)*t² + (c/a)*t + (d/a) = 0
692    let b = b / a;
693    let c = c / a;
694    let d = d / a;
695
696    // Depress: t = u - b/3
697    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        // Three real roots using trigonometric method
705        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        // One real root via Cardano
715        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; // suppress warning
722        (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        // Project direction onto XZ plane
739        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        // Tube center on the ring
747        let tube_center = xz_norm * self.major_radius;
748        // Add minor radius in full direction
749        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        // Solid torus inertia tensors (axis of symmetry = Y):
770        //   I_y = mass * (R² + (3/4)*r²)
771        //   I_x = I_z = mass * (5/8*r² + R²/2)
772        // (from standard solid torus formulas)
773        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        // V = 2π²*1*0.25 ≈ 4.935
796        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        // Approximately 4.935
804        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        // SA = 4π²*1*0.5 ≈ 19.74
815        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        // half_extents = [2.5, 0.5, 2.5]
833        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        // dir = (1,0,0) → support should be at (R+r, 0, 0)
848        let dir = Vec3::new(1.0, 0.0, 0.0);
849        let sp = t.support_point(&dir);
850        let expected = 1.0 + 0.5; // R + r
851        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        // dir = (0,1,0) → y component = r (minor radius)
865        let dir = Vec3::new(0.0, 1.0, 0.0);
866        let sp = t.support_point(&dir);
867        // tube center at (R, 0, 0) + r*(0,1,0)/|(1,1,0)| -- actually dir_xz is zero so
868        // tube_center = (R, 0, 0), dir_norm = (0,1,0), so result = (R, r, 0)
869        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        // Point on the tube center circle in XZ plane → inside
910        assert!(
911            t.contains_point([3.0, 0.0, 0.0]),
912            "ring centre should be inside"
913        );
914        // Point clearly outside
915        assert!(
916            !t.contains_point([0.0, 0.0, 0.0]),
917            "origin should be outside"
918        );
919        // Point on tube surface
920        assert!(
921            t.contains_point([3.0, 1.0, 0.0]),
922            "top of tube should be inside/on"
923        );
924        // Point just beyond tube
925        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        // A point far outside in +X direction
935        let cp = t.closest_point([10.0, 0.0, 0.0]);
936        // Should be at (R+r, 0, 0) = (4, 0, 0)
937        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        // Sample at u=0, v=0 → (R+r, 0, 0) = (4, 0, 0)
946        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        // Verify point is actually on the torus surface
950        assert!(
951            t.contains_point(p),
952            "sampled point {:?} should be on torus surface",
953            p
954        );
955        // Sample at u=PI/2, v=PI/2 → top of tube at 90°
956        let p2 = t.sample_surface(PI / 2.0, PI / 2.0);
957        // (R + r*cos(π/2)) * cos(π/2) = R*0 = 0, y = r*sin(π/2) = r, z = R*sin(π/2) = R
958        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        // All diagonal entries should be positive
968        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        // Off-diagonal should be zero
972        assert!(it[0][1].abs() < 1e-12);
973        assert!(it[0][2].abs() < 1e-12);
974        // Ixx == Izz by symmetry
975        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        // Ray along +Y from below, aimed at (3,0,0) which is on the tube
982        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        // Should hit somewhere between origin and tube; toi must be positive
988        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        // Ray from far outside the torus going away from it
999        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    // ── Expanded tests ──
1019
1020    #[test]
1021    fn test_torus_sdf_inside_tube() {
1022        let t = Torus::new(3.0, 1.0);
1023        // Point on the tube ring center: sdf should be -minor_radius
1024        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        // Point far outside
1036        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        // Point on the outer surface: (R+r, 0, 0) = (4, 0, 0)
1044        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        // Should be at (R+r, 0, 0)
1064        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        // u should match (mod 2π)
1076        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); // minor > major
1112        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    // ── Extended tests for new methods ──────────────────────────────────────
1128
1129    #[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        // u and v should be in [0, 1)
1135        assert!((0.0..1.0).contains(&u), "u={u}");
1136        assert!((0.0..1.0).contains(&v), "v={v}");
1137        // Reconstruct point
1138        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        // (R+r, 0, 0) → u=0, v=0
1147        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        // Half tube circumference = π * r
1167        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        // At v=0 (outer equator): factor = R + r
1176        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        // At v=π (inner equator): factor = R - r = 2
1183        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        // Should agree within 0.1%
1192        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        // (2,3) torus knot should be a closed curve after 1 full parameter cycle
1236        let t = Torus::new(3.0, 1.0);
1237        let pts = t.torus_knot_path(2, 3, 60);
1238        // First and "last+1" point should wrap (they're not the same since we
1239        // sample [0, 2π) exclusive, but the path is topologically closed)
1240        assert_eq!(pts.len(), 60);
1241    }
1242
1243    #[test]
1244    fn test_torus_winding_number_major_circle() {
1245        // A curve following the major circle once
1246        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) // v=0 fixed, u winds around once
1252            })
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        // Ray along +Y through a tube: enters and exits the tube = 2 intersections,
1292        // but the quartic may produce up to 4 roots (2 real, 2 near-degenerate).
1293        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        // Ray going away from torus
1301        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        // AABB that has a corner at (3,0,0) which is inside the tube (sdf < 0)
1309        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        // AABB far from torus
1316        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); // should clamp to 3
1380        assert_eq!(pts.len(), 3);
1381    }
1382
1383    #[test]
1384    fn test_torus_knot_path_trivial_11() {
1385        // A (1,1) torus knot is just a circle on the surface
1386        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 major > minor, factor is always positive
1397        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}