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    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    /// Cast a ray against the torus using an iterative quartic solver approach.
63    ///
64    /// The torus implicit equation (centered at origin, axis = Y):
65    ///   (x²+y²+z² + R²−r²)² − 4R²(x²+z²) = 0
66    ///
67    /// Substituting P+t*D gives a quartic in t which is solved by Ferrari's method.
68    /// Returns `Some((t, normal))` or `None`.
69    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    /// Closest point on the torus surface to point `p`.
82    ///
83    /// Projects `p` onto the nearest point on the ring circle (in XZ), then
84    /// moves from that circle point toward `p` (or outward if p is inside tube)
85    /// by the minor radius.
86    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        // Project onto XZ plane to find the nearest point on the ring circle
91        let xz_len = (px * px + pz * pz).sqrt();
92        let (ring_x, ring_z) = if xz_len < 1e-12 {
93            // p is on Y axis; ring point is arbitrary, pick +X
94            (self.major_radius, 0.0)
95        } else {
96            let s = self.major_radius / xz_len;
97            (px * s, pz * s)
98        };
99        // Vector from ring point to p
100        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            // p is exactly on the ring circle – push out along +Y
106            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    /// Returns `true` if point `p` is inside (or on the surface of) the torus tube.
113    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    /// Parametric surface sample: `u ∈ [0,2π)`, `v ∈ [0,2π)`.
123    ///
124    /// Returns a point `(x,y,z)` on the torus surface.
125    ///   x = (R + r*cos(v)) * cos(u)
126    ///   y = r * sin(v)
127    ///   z = (R + r*cos(v)) * sin(u)
128    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    // ── New expanded methods ──
138
139    /// Torus SDF (signed distance to torus surface).
140    ///
141    /// Negative inside the tube, positive outside.
142    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    /// Torus-ray analytic intersection (same as `ray_cast_array` but explicit name).
149    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    /// Torus support function (plain array).
159    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    /// Surface parameterization: returns the `(u, v)` angles for the point on
166    /// the torus surface closest to `p`.
167    ///
168    /// `u` is the angle around the major circle (XZ plane), `v` is the angle
169    /// around the tube (in the plane through the Y axis and the ring point).
170    pub fn surface_parameters(&self, p: [f64; 3]) -> (f64, f64) {
171        let xz = (p[0] * p[0] + p[2] * p[2]).sqrt();
172        // u: angle in XZ plane
173        let u = p[2].atan2(p[0]); // atan2(z, x)
174
175        // Ring point at angle u
176        let rx = self.major_radius * u.cos();
177        let rz = self.major_radius * u.sin();
178
179        // Vector from ring point to p
180        let dx = p[0] - rx;
181        let dy = p[1];
182        let dz = p[2] - rz;
183
184        // v: angle in the plane of the tube (radial outward from ring center)
185        // Radial direction: (cos u, 0, sin u); Y direction: (0, 1, 0)
186        let radial = xz - self.major_radius;
187        let v = dy.atan2(radial);
188
189        let _ = (dx, dz); // suppress unused warnings
190        (u, v)
191    }
192
193    /// Generate `n` random points uniformly distributed on the torus surface.
194    ///
195    /// Uses a deterministic xorshift64 PRNG seeded with `seed`.
196    /// Sampling: uniform in `u` and `v`, weighted by area element `(R + r cos v)`.
197    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        // Rejection sampling: sample (u, v) uniform in [0,2π)²,
209        // accept with probability proportional to (R + r*cos(v)) / (R + r).
210        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    /// Approximate solid torus inertia tensor from `inertia_tensor_array`.
226    pub fn inertia_raw(&self, mass: f64) -> [[f64; 3]; 3] {
227        self.inertia_tensor_array(mass)
228    }
229
230    /// Outer radius of the torus (major + minor).
231    pub fn outer_radius(&self) -> f64 {
232        self.major_radius + self.minor_radius
233    }
234
235    /// Inner radius of the torus (major - minor, clamped to 0).
236    pub fn inner_radius(&self) -> f64 {
237        (self.major_radius - self.minor_radius).max(0.0)
238    }
239
240    // ── Extended geometry: UV mapping, geodesics, tube cross-sections, knot paths, winding ──
241
242    /// UV mapping for the torus surface.
243    ///
244    /// Maps a surface point `p` to texture coordinates `(u, v)` in `[0, 1)²`.
245    /// `u` corresponds to the major (longitudinal) angle and `v` to the minor
246    /// (latitudinal) angle, both normalised to the range `[0, 1)`.
247    pub fn uv_map(&self, p: [f64; 3]) -> [f64; 2] {
248        let (theta, phi) = self.surface_parameters(p);
249        // Normalise from (-π, π] to [0, 1)
250        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    /// Approximate geodesic distance between two surface points `a` and `b`
256    /// via the flat-torus metric.
257    ///
258    /// The flat-torus metric: `d = sqrt((R Δθ)² + (r Δφ)²)` where `Δθ` and
259    /// `Δφ` are the wrapped angular differences on the major and minor circles.
260    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    /// Area element at angle `v` on the tube: `(R + r cos v) r dv du`.
271    ///
272    /// Returns the area-weighted factor `R + r*cos(v)` for the given tube angle `v`.
273    pub fn area_element_factor(&self, v: f64) -> f64 {
274        self.major_radius + self.minor_radius * v.cos()
275    }
276
277    /// Approximate surface area by numerical integration (cross-check of closed form).
278    ///
279    /// Uses `n_steps` Gauss-Legendre-like sampling along each angular dimension.
280    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; // summing over n u-samples
289        }
290        sum * self.minor_radius * du * dv
291    }
292
293    /// Tube cross-section: return a list of `n` points on the tube circle at angle `u`.
294    ///
295    /// The circle lies in the plane through the ring point at angle `u` and the Y axis.
296    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                // Radial direction in XZ plane at angle u
304                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    /// Generate a torus knot path with winding numbers `(p, q)`.
315    ///
316    /// A `(p, q)`-torus knot winds around the major circle `p` times while
317    /// winding around the tube `q` times.  Returns `n_pts` sampled points.
318    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    /// Winding number of a closed curve projected onto the torus major circle.
331    ///
332    /// Counts how many times the sequence of `(u, _)` angles (extracted from the
333    /// points via `surface_parameters`) winds around the major circle.
334    ///
335    /// Returns an integer winding count (positive = counter-clockwise).
336    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    /// Parametric tangent vector on the torus surface in the `u` direction.
354    ///
355    /// `dP/du` at `(u, v)`: partial derivative with respect to the major angle.
356    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    /// Parametric tangent vector on the torus surface in the `v` direction.
362    ///
363    /// `dP/dv` at `(u, v)`: partial derivative with respect to the tube angle.
364    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    /// Surface normal via cross product of tangents `dP/du × dP/dv`.
370    ///
371    /// Should agree with `torus_normal` up to sign conventions.
372    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    /// Test whether a ray intersects the torus and return the hit count.
385    ///
386    /// A finite ray with origin `o`, direction `d`, and parameter `max_toi` can
387    /// intersect the torus at 0, 2, or 4 points.  This counts all positive-t
388    /// intersections within `[0, max_toi]`.
389    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    /// Approximate torus-AABB overlap test via SDF.
431    ///
432    /// Returns `true` if the AABB `[min, max]` overlaps the torus.
433    /// Uses the 8 corners of the AABB as sample points.
434    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        // Check if any corner is inside the torus, or if the SDF changes sign
446        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    /// Aspect ratio of the torus: major_radius / minor_radius.
452    ///
453    /// Returns the ratio R/r.  A value close to 1 means the torus is nearly
454    /// self-intersecting; large values mean a thin tube.
455    pub fn aspect_ratio(&self) -> f64 {
456        self.major_radius / self.minor_radius.max(1e-30)
457    }
458
459    /// Generate `n` equidistant points along the major circle (the ring itself).
460    ///
461    /// Points lie on the spine of the torus (on the ring circle in the XZ plane,
462    /// at `y = 0`).
463    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    // -----------------------------------------------------------------------
478    // Internal ray-cast implementation (quartic via Ferrari / companion matrix).
479    // -----------------------------------------------------------------------
480    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        // Normalize direction for parameter computation; scale toi back after.
485        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        // Coefficients of the quartic  c4*t^4 + c3*t^3 + c2*t^2 + c1*t + c0 = 0
493        // derived from ((ox+t*dx)²+(oy+t*dy)²+(oz+t*dz)² + R²-r²)² = 4R²((ox+t*dx)²+(oz+t*dz)²)
494        let oo = o.dot(&o);
495        let od = o.dot(&d);
496        let _dd = d.dot(&d); // = 1 since d is normalised
497        let rr = r_big * r_big;
498        let aa = r_small * r_small;
499        let alpha = oo - rr - aa; // constant term inside big parens (before squaring)
500        let beta = 2.0 * od;
501        // Let f(t) = oo + 2*od*t + dd*t² + R²-r² -4R²*(... ) is expanded below.
502
503        // Using the parametric form:
504        //   Let S(t) = |o + t*d|² + R² - r² = (dd)*t² + 2*od*t + (oo + R² - r²)
505        //            = t² + beta*t + (oo + rr - aa)   [since dd=1]
506        // Let T(t) = (ox+t*dx)² + (oz+t*dz)²  (XZ only)
507        //          = (dx²+dz²)*t² + 2*(ox*dx+oz*dz)*t + (ox²+oz²)
508        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        // Quartic: (t² + beta*t + (oo+rr-aa))² - 4rr*(dxz2*t² + 2*od_xz*t + oxz2) = 0
513        let c_alpha = oo + rr - aa; // = oo + R² - r²
514        // Expand (t² + beta*t + c_alpha)² = t^4 + 2beta t^3 + (beta²+2c_alpha) t² + 2beta*c_alpha t + c_alpha²
515        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; // suppress unused warning
521
522        // Solve quartic numerically – we use a simple companion-matrix eigenvalue
523        // approach via Durand-Kerner / iterative refinement, or fall back to
524        // a straightforward bisection in likely intervals.
525        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            // Scale back from normalised-direction t to actual t along original dir
533            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
552/// Wrapped angular difference (shortest path), result in `(-π, π]`.
553fn angle_diff(a: f64, b: f64) -> f64 {
554    let d = b - a;
555    // wrap into (-π, π]
556
557    d - (2.0 * PI) * ((d + PI) / (2.0 * PI)).floor()
558}
559
560/// 3-component cross product.
561fn 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
569/// Compute the outward normal on the torus surface at `p`.
570fn 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    // Nearest point on the ring circle
576    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
587/// Solve a monic quartic t^4 + (c3/c4)*t^3 + ... analytically via Ferrari's method
588/// or by numerical polishing.  Returns up to 4 real roots (may include duplicates).
589fn solve_quartic(c4: f64, c3: f64, c2: f64, c1: f64, c0: f64) -> [f64; 4] {
590    // Normalise to monic
591    let a = c3 / c4;
592    let b = c2 / c4;
593    let c = c1 / c4;
594    let d = c0 / c4;
595
596    // Depress the quartic: substitute t = u - a/4
597    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    // Solve the depressed quartic u^4 + p*u^2 + q*u + r = 0 via Ferrari
604    // Resolvent cubic: m^3 + (5p/2)*m^2 + (2p²-r)*m + (p³/2-p*r/2-q²/8-... )
605    // Using standard Ferrari resolvent: 8m³ + 8p*m² + (2p²-8r)*m - q² = 0
606    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        // Two quadratics:
630        // u² + sqrt(2m+p)*u + (m + q/(2*sqrt(2m+p))) = 0
631        // u² - sqrt(2m+p)*u + (m - q/(2*sqrt(2m+p))) = 0
632        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            // discriminant of u² ∓ sqrt(2m+p)*u + c_q = 0 is (sqrt_2mp/2)² - c_q
637            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; // suppress warning
653        }
654        if ri >= 4 {
655            break;
656        }
657    }
658    roots
659}
660
661/// Returns one real root of the cubic a*t³ + b*t² + c*t + d = 0,
662/// plus two more (may be complex → NAN).
663fn solve_cubic_3roots(a: f64, b: f64, c: f64, d: f64) -> (f64, f64, f64) {
664    // Monic: t³ + (b/a)*t² + (c/a)*t + (d/a) = 0
665    let b = b / a;
666    let c = c / a;
667    let d = d / a;
668
669    // Depress: t = u - b/3
670    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        // Three real roots using trigonometric method
678        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        // One real root via Cardano
688        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; // suppress warning
695        (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        // Project direction onto XZ plane
712        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        // Tube center on the ring
720        let tube_center = xz_norm * self.major_radius;
721        // Add minor radius in full direction
722        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        // Solid torus inertia tensors (axis of symmetry = Y):
743        //   I_y = mass * (R² + (3/4)*r²)
744        //   I_x = I_z = mass * (5/8*r² + R²/2)
745        // (from standard solid torus formulas)
746        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        // V = 2π²*1*0.25 ≈ 4.935
769        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        // Approximately 4.935
777        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        // SA = 4π²*1*0.5 ≈ 19.74
788        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        // half_extents = [2.5, 0.5, 2.5]
806        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        // dir = (1,0,0) → support should be at (R+r, 0, 0)
821        let dir = Vec3::new(1.0, 0.0, 0.0);
822        let sp = t.support_point(&dir);
823        let expected = 1.0 + 0.5; // R + r
824        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        // dir = (0,1,0) → y component = r (minor radius)
838        let dir = Vec3::new(0.0, 1.0, 0.0);
839        let sp = t.support_point(&dir);
840        // tube center at (R, 0, 0) + r*(0,1,0)/|(1,1,0)| -- actually dir_xz is zero so
841        // tube_center = (R, 0, 0), dir_norm = (0,1,0), so result = (R, r, 0)
842        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        // Point on the tube center circle in XZ plane → inside
883        assert!(
884            t.contains_point([3.0, 0.0, 0.0]),
885            "ring centre should be inside"
886        );
887        // Point clearly outside
888        assert!(
889            !t.contains_point([0.0, 0.0, 0.0]),
890            "origin should be outside"
891        );
892        // Point on tube surface
893        assert!(
894            t.contains_point([3.0, 1.0, 0.0]),
895            "top of tube should be inside/on"
896        );
897        // Point just beyond tube
898        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        // A point far outside in +X direction
908        let cp = t.closest_point([10.0, 0.0, 0.0]);
909        // Should be at (R+r, 0, 0) = (4, 0, 0)
910        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        // Sample at u=0, v=0 → (R+r, 0, 0) = (4, 0, 0)
919        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        // Verify point is actually on the torus surface
923        assert!(
924            t.contains_point(p),
925            "sampled point {:?} should be on torus surface",
926            p
927        );
928        // Sample at u=PI/2, v=PI/2 → top of tube at 90°
929        let p2 = t.sample_surface(PI / 2.0, PI / 2.0);
930        // (R + r*cos(π/2)) * cos(π/2) = R*0 = 0, y = r*sin(π/2) = r, z = R*sin(π/2) = R
931        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        // All diagonal entries should be positive
941        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        // Off-diagonal should be zero
945        assert!(it[0][1].abs() < 1e-12);
946        assert!(it[0][2].abs() < 1e-12);
947        // Ixx == Izz by symmetry
948        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        // Ray along +Y from below, aimed at (3,0,0) which is on the tube
955        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        // Should hit somewhere between origin and tube; toi must be positive
961        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        // Ray from far outside the torus going away from it
972        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    // ── Expanded tests ──
992
993    #[test]
994    fn test_torus_sdf_inside_tube() {
995        let t = Torus::new(3.0, 1.0);
996        // Point on the tube ring center: sdf should be -minor_radius
997        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        // Point far outside
1009        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        // Point on the outer surface: (R+r, 0, 0) = (4, 0, 0)
1017        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        // Should be at (R+r, 0, 0)
1037        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        // u should match (mod 2π)
1049        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); // minor > major
1085        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    // ── Extended tests for new methods ──────────────────────────────────────
1101
1102    #[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        // u and v should be in [0, 1)
1108        assert!((0.0..1.0).contains(&u), "u={u}");
1109        assert!((0.0..1.0).contains(&v), "v={v}");
1110        // Reconstruct point
1111        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        // (R+r, 0, 0) → u=0, v=0
1120        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        // Half tube circumference = π * r
1140        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        // At v=0 (outer equator): factor = R + r
1149        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        // At v=π (inner equator): factor = R - r = 2
1156        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        // Should agree within 0.1%
1165        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        // (2,3) torus knot should be a closed curve after 1 full parameter cycle
1209        let t = Torus::new(3.0, 1.0);
1210        let pts = t.torus_knot_path(2, 3, 60);
1211        // First and "last+1" point should wrap (they're not the same since we
1212        // sample [0, 2π) exclusive, but the path is topologically closed)
1213        assert_eq!(pts.len(), 60);
1214    }
1215
1216    #[test]
1217    fn test_torus_winding_number_major_circle() {
1218        // A curve following the major circle once
1219        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) // v=0 fixed, u winds around once
1225            })
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        // Ray along +Y through a tube: enters and exits the tube = 2 intersections,
1265        // but the quartic may produce up to 4 roots (2 real, 2 near-degenerate).
1266        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        // Ray going away from torus
1274        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        // AABB that has a corner at (3,0,0) which is inside the tube (sdf < 0)
1282        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        // AABB far from torus
1289        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); // should clamp to 3
1353        assert_eq!(pts.len(), 3);
1354    }
1355
1356    #[test]
1357    fn test_torus_knot_path_trivial_11() {
1358        // A (1,1) torus knot is just a circle on the surface
1359        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 major > minor, factor is always positive
1370        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}