Skip to main content

oxiphysics_geometry/
sphere.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Sphere 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 sphere defined by its radius.
12#[derive(Debug, Clone)]
13pub struct Sphere {
14    /// Radius of the sphere.
15    pub radius: Real,
16}
17
18impl Sphere {
19    /// Create a new sphere with the given radius.
20    pub fn new(radius: Real) -> Self {
21        Self { radius }
22    }
23
24    /// Surface area: 4πr².
25    pub fn surface_area(&self) -> Real {
26        4.0 * PI * self.radius * self.radius
27    }
28
29    /// Volume: (4/3)πr³.
30    pub fn volume_explicit(&self) -> Real {
31        (4.0 / 3.0) * PI * self.radius.powi(3)
32    }
33
34    /// Inertia tensor as \[\[f64;3\\];3] row-major: (2/5)mr² × identity.
35    pub fn inertia_tensor_array(&self, mass: f64) -> [[f64; 3]; 3] {
36        let i = 0.4 * mass * self.radius * self.radius;
37        [[i, 0.0, 0.0], [0.0, i, 0.0], [0.0, 0.0, i]]
38    }
39
40    /// Ray cast returning (t, normal) as plain arrays.
41    pub fn ray_cast_array(
42        &self,
43        origin: [f64; 3],
44        direction: [f64; 3],
45        max_toi: f64,
46    ) -> Option<(f64, [f64; 3])> {
47        let o = Vec3::new(origin[0], origin[1], origin[2]);
48        let d = Vec3::new(direction[0], direction[1], direction[2]);
49        let hit = self.ray_cast(&o, &d, max_toi)?;
50        Some((hit.toi, [hit.normal.x, hit.normal.y, hit.normal.z]))
51    }
52
53    /// Closest point on the sphere surface to `p`.
54    /// If `p` is the origin, returns a point on the +X side.
55    pub fn closest_point(&self, p: [f64; 3]) -> [f64; 3] {
56        let len = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
57        if len < 1e-12 {
58            return [self.radius, 0.0, 0.0];
59        }
60        let s = self.radius / len;
61        [p[0] * s, p[1] * s, p[2] * s]
62    }
63
64    /// GJK support function: farthest point in `direction`.
65    pub fn support(&self, direction: [f64; 3]) -> [f64; 3] {
66        let len = (direction[0] * direction[0]
67            + direction[1] * direction[1]
68            + direction[2] * direction[2])
69            .sqrt();
70        if len < 1e-12 {
71            return [self.radius, 0.0, 0.0];
72        }
73        let s = self.radius / len;
74        [direction[0] * s, direction[1] * s, direction[2] * s]
75    }
76
77    // ── New methods ──
78
79    /// GJK support point with a center offset.
80    /// Returns `center + radius * normalize(direction)`.
81    pub fn support_with_center(&self, center: [f64; 3], direction: [f64; 3]) -> [f64; 3] {
82        let sp = self.support(direction);
83        [sp[0] + center[0], sp[1] + center[1], sp[2] + center[2]]
84    }
85
86    /// Minkowski sum support: support(A) + support(B) in the given direction.
87    /// Both spheres are centered at the origin.
88    pub fn minkowski_sum_support(&self, other: &Sphere, direction: [f64; 3]) -> [f64; 3] {
89        let combined_radius = self.radius + other.radius;
90        let len = (direction[0] * direction[0]
91            + direction[1] * direction[1]
92            + direction[2] * direction[2])
93            .sqrt();
94        if len < 1e-12 {
95            return [combined_radius, 0.0, 0.0];
96        }
97        let s = combined_radius / len;
98        [direction[0] * s, direction[1] * s, direction[2] * s]
99    }
100
101    /// Minkowski difference support: support_A(d) - support_B(-d).
102    /// Both spheres centered at origin.
103    pub fn minkowski_diff_support(&self, other: &Sphere, direction: [f64; 3]) -> [f64; 3] {
104        let sa = self.support(direction);
105        let neg_d = [-direction[0], -direction[1], -direction[2]];
106        let sb = other.support(neg_d);
107        [sa[0] - sb[0], sa[1] - sb[1], sa[2] - sb[2]]
108    }
109
110    /// Compute bounding sphere from a set of points (Ritter's algorithm).
111    /// Returns `(center, radius)`.
112    pub fn bounding_sphere_from_points(points: &[[f64; 3]]) -> ([f64; 3], f64) {
113        if points.is_empty() {
114            return ([0.0; 3], 0.0);
115        }
116        if points.len() == 1 {
117            return (points[0], 0.0);
118        }
119
120        // Find two most separated points along each axis
121        let mut min_x = 0usize;
122        let mut max_x = 0usize;
123        for (i, p) in points.iter().enumerate() {
124            if p[0] < points[min_x][0] {
125                min_x = i;
126            }
127            if p[0] > points[max_x][0] {
128                max_x = i;
129            }
130        }
131
132        let mut center = [
133            (points[min_x][0] + points[max_x][0]) * 0.5,
134            (points[min_x][1] + points[max_x][1]) * 0.5,
135            (points[min_x][2] + points[max_x][2]) * 0.5,
136        ];
137        let dx = points[max_x][0] - points[min_x][0];
138        let dy = points[max_x][1] - points[min_x][1];
139        let dz = points[max_x][2] - points[min_x][2];
140        let mut radius = (dx * dx + dy * dy + dz * dz).sqrt() * 0.5;
141
142        // Expand sphere to include all points
143        for p in points {
144            let dx = p[0] - center[0];
145            let dy = p[1] - center[1];
146            let dz = p[2] - center[2];
147            let dist = (dx * dx + dy * dy + dz * dz).sqrt();
148            if dist > radius {
149                let new_radius = (radius + dist) * 0.5;
150                let shift = (new_radius - radius) / dist;
151                center[0] += dx * shift;
152                center[1] += dy * shift;
153                center[2] += dz * shift;
154                radius = new_radius;
155            }
156        }
157
158        (center, radius)
159    }
160
161    /// Sphere-sphere intersection test.
162    /// Returns true if two spheres (at given centers) overlap.
163    pub fn intersects_sphere(
164        &self,
165        center_a: [f64; 3],
166        other: &Sphere,
167        center_b: [f64; 3],
168    ) -> bool {
169        let dx = center_b[0] - center_a[0];
170        let dy = center_b[1] - center_a[1];
171        let dz = center_b[2] - center_a[2];
172        let dist_sq = dx * dx + dy * dy + dz * dz;
173        let r_sum = self.radius + other.radius;
174        dist_sq <= r_sum * r_sum
175    }
176
177    /// Sphere-sphere intersection circle.
178    /// Returns `Some((center, normal, circle_radius))` if the spheres intersect
179    /// in a circle, `None` if they don't intersect or are concentric.
180    pub fn sphere_intersection_circle(
181        &self,
182        center_a: [f64; 3],
183        other: &Sphere,
184        center_b: [f64; 3],
185    ) -> Option<([f64; 3], [f64; 3], f64)> {
186        let dx = center_b[0] - center_a[0];
187        let dy = center_b[1] - center_a[1];
188        let dz = center_b[2] - center_a[2];
189        let d = (dx * dx + dy * dy + dz * dz).sqrt();
190
191        if d < 1e-12 {
192            return None; // concentric
193        }
194
195        let r1 = self.radius;
196        let r2 = other.radius;
197
198        if d > r1 + r2 || d < (r1 - r2).abs() {
199            return None; // no intersection
200        }
201
202        // Distance from center_a along the axis to the intersection plane
203        let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
204        let h_sq = r1 * r1 - a * a;
205        let h = if h_sq > 0.0 { h_sq.sqrt() } else { 0.0 };
206
207        let nx = dx / d;
208        let ny = dy / d;
209        let nz = dz / d;
210
211        let cx = center_a[0] + a * nx;
212        let cy = center_a[1] + a * ny;
213        let cz = center_a[2] + a * nz;
214
215        Some(([cx, cy, cz], [nx, ny, nz], h))
216    }
217
218    /// Closest point on the sphere surface to a line defined by `point` and `direction`.
219    /// Returns the closest point on the sphere surface.
220    pub fn closest_point_to_line(&self, line_point: [f64; 3], line_dir: [f64; 3]) -> [f64; 3] {
221        // Find closest point on line to origin (sphere center)
222        let dir_len_sq =
223            line_dir[0] * line_dir[0] + line_dir[1] * line_dir[1] + line_dir[2] * line_dir[2];
224        if dir_len_sq < 1e-24 {
225            return self.closest_point(line_point);
226        }
227        let t = -(line_point[0] * line_dir[0]
228            + line_point[1] * line_dir[1]
229            + line_point[2] * line_dir[2])
230            / dir_len_sq;
231        let closest_on_line = [
232            line_point[0] + t * line_dir[0],
233            line_point[1] + t * line_dir[1],
234            line_point[2] + t * line_dir[2],
235        ];
236        self.closest_point(closest_on_line)
237    }
238
239    /// Closest point on the sphere surface to a plane defined by `normal` (unit) and `d`
240    /// where `normal · x = d`.
241    pub fn closest_point_to_plane(&self, normal: [f64; 3], d: f64) -> [f64; 3] {
242        // Signed distance from sphere center (origin) to plane
243        // sign = -(normal · 0 - d) = d
244        // The closest point on the sphere is in the direction toward the plane
245        let sign = if d >= 0.0 { 1.0 } else { -1.0 };
246        [
247            sign * normal[0] * self.radius,
248            sign * normal[1] * self.radius,
249            sign * normal[2] * self.radius,
250        ]
251    }
252
253    /// Sphere sweep (moving sphere): test if a sphere moving from `start` along
254    /// `velocity` hits a static sphere (centered at `target_center` with `target_radius`).
255    /// Returns `Some(t)` in \[0, max_t\] if there is a collision.
256    pub fn sphere_sweep(
257        &self,
258        start: [f64; 3],
259        velocity: [f64; 3],
260        target_center: [f64; 3],
261        target_radius: f64,
262        max_t: f64,
263    ) -> Option<f64> {
264        let combined_r = self.radius + target_radius;
265        // Ray from start toward target
266        let dx = start[0] - target_center[0];
267        let dy = start[1] - target_center[1];
268        let dz = start[2] - target_center[2];
269
270        let a = velocity[0] * velocity[0] + velocity[1] * velocity[1] + velocity[2] * velocity[2];
271        let b = 2.0 * (dx * velocity[0] + dy * velocity[1] + dz * velocity[2]);
272        let c = dx * dx + dy * dy + dz * dz - combined_r * combined_r;
273
274        if a < 1e-24 {
275            // Stationary
276            return if c <= 0.0 { Some(0.0) } else { None };
277        }
278
279        let disc = b * b - 4.0 * a * c;
280        if disc < 0.0 {
281            return None;
282        }
283
284        let sqrt_disc = disc.sqrt();
285        let t1 = (-b - sqrt_disc) / (2.0 * a);
286        let t2 = (-b + sqrt_disc) / (2.0 * a);
287
288        if t1 >= 0.0 && t1 <= max_t {
289            Some(t1)
290        } else if t2 >= 0.0 && t2 <= max_t {
291            Some(t2)
292        } else {
293            None
294        }
295    }
296
297    /// Signed distance from a point to the sphere surface.
298    /// Negative if inside the sphere, positive if outside.
299    pub fn signed_distance(&self, p: [f64; 3]) -> f64 {
300        let len = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
301        len - self.radius
302    }
303
304    /// Returns true if the point is inside (or on) the sphere.
305    pub fn contains_point(&self, p: [f64; 3]) -> bool {
306        p[0] * p[0] + p[1] * p[1] + p[2] * p[2] <= self.radius * self.radius
307    }
308
309    /// Project a point onto the sphere interior (clamp to sphere if outside).
310    pub fn project_inside(&self, p: [f64; 3]) -> [f64; 3] {
311        let len_sq = p[0] * p[0] + p[1] * p[1] + p[2] * p[2];
312        if len_sq <= self.radius * self.radius {
313            p
314        } else {
315            self.closest_point(p)
316        }
317    }
318
319    // ── Extended: geodesic sphere, spherical cap, hemisphere sampling, conformal maps ──
320
321    /// Generate a geodesic icosphere by subdividing an icosahedron.
322    ///
323    /// Returns `(vertices, triangles)` where each vertex is a unit-sphere
324    /// point scaled to `self.radius`, and triangles are `[usize; 3]` index triples.
325    /// `subdivisions` = 0 gives the base icosahedron (20 faces, 12 verts).
326    pub fn geodesic_icosphere(&self, subdivisions: u32) -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
327        let (mut verts, mut tris) = base_icosahedron();
328        for _ in 0..subdivisions {
329            let (v2, t2) = subdivide_icosphere(verts, tris);
330            verts = v2;
331            tris = t2;
332        }
333        // Scale to self.radius
334        let r = self.radius;
335        let verts = verts
336            .iter()
337            .map(|v| {
338                let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-14);
339                [v[0] / len * r, v[1] / len * r, v[2] / len * r]
340            })
341            .collect();
342        (verts, tris)
343    }
344
345    /// Spherical cap volume.
346    ///
347    /// Returns the volume of a spherical cap of height `h` cut from this sphere.
348    /// `h` must be in `[0, 2r]`.
349    pub fn spherical_cap_volume(&self, h: f64) -> f64 {
350        let h_clamped = h.clamp(0.0, 2.0 * self.radius);
351        PI * h_clamped * h_clamped * (3.0 * self.radius - h_clamped) / 3.0
352    }
353
354    /// Spherical cap surface area (curved part only).
355    ///
356    /// Area = 2π r h.
357    pub fn spherical_cap_area(&self, h: f64) -> f64 {
358        let h_clamped = h.clamp(0.0, 2.0 * self.radius);
359        2.0 * PI * self.radius * h_clamped
360    }
361
362    /// Spherical zone area between two parallel planes.
363    ///
364    /// For planes at heights `h1` and `h2` (measured from the bottom of the
365    /// sphere), returns the surface area of the zone.
366    /// Area = 2π r |h2 - h1|.
367    pub fn spherical_zone_area(&self, h1: f64, h2: f64) -> f64 {
368        let dh = (h2 - h1).abs();
369        2.0 * PI * self.radius * dh
370    }
371
372    /// Sample points on the hemisphere (z ≥ 0) using cosine-weighted sampling.
373    ///
374    /// Uses a deterministic xorshift PRNG seeded with `seed`.
375    /// Each point lies on the unit hemisphere surface projected to `self.radius`.
376    pub fn hemisphere_cosine_sample(&self, n: usize, seed: u64) -> Vec<[f64; 3]> {
377        let mut state = seed;
378        let xorshift = |s: &mut u64| -> f64 {
379            *s ^= *s << 13;
380            *s ^= *s >> 7;
381            *s ^= *s << 17;
382            (*s as f64) / (u64::MAX as f64)
383        };
384        let r = self.radius;
385        (0..n)
386            .map(|_| {
387                let u1 = xorshift(&mut state);
388                let u2 = xorshift(&mut state);
389                // cosine-weighted: sqrt(u1) for sin(θ), 2π*u2 for φ
390                let sin_theta = u1.sqrt();
391                let cos_theta = (1.0 - u1).sqrt();
392                let phi = 2.0 * PI * u2;
393                [
394                    r * sin_theta * phi.cos(),
395                    r * sin_theta * phi.sin(),
396                    r * cos_theta,
397                ]
398            })
399            .collect()
400    }
401
402    /// Uniform hemisphere sampling (z ≥ 0).
403    ///
404    /// Each sample is uniformly distributed on the upper hemisphere.
405    pub fn hemisphere_uniform_sample(&self, n: usize, seed: u64) -> Vec<[f64; 3]> {
406        let mut state = seed;
407        let xorshift = |s: &mut u64| -> f64 {
408            *s ^= *s << 13;
409            *s ^= *s >> 7;
410            *s ^= *s << 17;
411            (*s as f64) / (u64::MAX as f64)
412        };
413        let r = self.radius;
414        (0..n)
415            .map(|_| {
416                let u1 = xorshift(&mut state);
417                let u2 = xorshift(&mut state);
418                let cos_theta = u1; // uniform in cos_theta ∈ [0, 1]
419                let sin_theta = (1.0 - cos_theta * cos_theta).max(0.0).sqrt();
420                let phi = 2.0 * PI * u2;
421                [
422                    r * sin_theta * phi.cos(),
423                    r * sin_theta * phi.sin(),
424                    r * cos_theta,
425                ]
426            })
427            .collect()
428    }
429
430    /// 3-D Fibonacci sphere packing.
431    ///
432    /// Generates `n` nearly uniformly distributed points on the sphere surface
433    /// using the golden-angle Fibonacci lattice method.
434    pub fn fibonacci_sphere(&self, n: usize) -> Vec<[f64; 3]> {
435        let n = n.max(1);
436        let golden = (1.0 + 5.0_f64.sqrt()) / 2.0;
437        let r = self.radius;
438        (0..n)
439            .map(|i| {
440                let theta = (1.0 - 2.0 * i as f64 / (n - 1).max(1) as f64)
441                    .clamp(-1.0, 1.0)
442                    .acos();
443                let phi = 2.0 * PI * i as f64 / golden;
444                [
445                    r * theta.sin() * phi.cos(),
446                    r * theta.sin() * phi.sin(),
447                    r * theta.cos(),
448                ]
449            })
450            .collect()
451    }
452
453    /// Stereographic projection from the sphere to the plane.
454    ///
455    /// Projects a point `p` on the sphere to the stereographic plane using
456    /// the south pole as the projection center.  The north pole maps to
457    /// infinity; the south pole is undefined.
458    ///
459    /// Returns `(x_plane, y_plane)` or `None` if `p` is at (or near) the south pole.
460    pub fn stereographic_project(&self, p: [f64; 3]) -> Option<[f64; 2]> {
461        let r = self.radius;
462        // South pole at (0, 0, -r)
463        let denom = r + p[2]; // z + r
464        if denom.abs() < 1e-10 * r {
465            return None; // near south pole
466        }
467        let scale = 2.0 * r / denom;
468        Some([p[0] * scale, p[1] * scale])
469    }
470
471    /// Inverse stereographic projection from the plane to the sphere.
472    ///
473    /// Maps `(x_plane, y_plane)` back to a point on the sphere.
474    /// Uses the formula: X = 4r²x/D, Y = 4r²y/D, Z = r(D - 4r²)/D
475    /// where D = x² + y² + 4r².
476    pub fn stereographic_unproject(&self, uv: [f64; 2]) -> [f64; 3] {
477        let r = self.radius;
478        let x = uv[0];
479        let y = uv[1];
480        let d2 = x * x + y * y;
481        let denom = d2 + 4.0 * r * r;
482        [
483            4.0 * r * r * x / denom,
484            4.0 * r * r * y / denom,
485            r * (4.0 * r * r - d2) / denom,
486        ]
487    }
488
489    /// Spherical harmonic (l=3) coefficient Y_3^m(theta, phi).
490    ///
491    /// Extends `spherical_harmonic` to degree 3.
492    pub fn sh_l3(m: i32, theta: f64, phi: f64) -> f64 {
493        let cos_t = theta.cos();
494        let sin_t = theta.sin();
495        match m {
496            -3 => 0.25 * (35.0 / (2.0 * PI)).sqrt() * sin_t.powi(3) * (3.0 * phi).sin(),
497            -2 => 0.5 * (105.0 / PI).sqrt() * sin_t.powi(2) * cos_t * (2.0 * phi).sin(),
498            -1 => {
499                0.25 * (21.0 / (2.0 * PI)).sqrt() * sin_t * (5.0 * cos_t * cos_t - 1.0) * phi.sin()
500            }
501            0 => 0.25 * (7.0 / PI).sqrt() * (5.0 * cos_t.powi(3) - 3.0 * cos_t),
502            1 => {
503                0.25 * (21.0 / (2.0 * PI)).sqrt() * sin_t * (5.0 * cos_t * cos_t - 1.0) * phi.cos()
504            }
505            2 => 0.25 * (105.0 / PI).sqrt() * sin_t.powi(2) * cos_t * (2.0 * phi).cos(),
506            3 => 0.25 * (35.0 / (2.0 * PI)).sqrt() * sin_t.powi(3) * (3.0 * phi).cos(),
507            _ => 0.0,
508        }
509    }
510
511    /// Compute the solid angle subtended by a spherical cap of height `h`.
512    ///
513    /// Ω = 2π(1 - cos θ) where cos θ = 1 - h/r.
514    pub fn cap_solid_angle(&self, h: f64) -> f64 {
515        let cos_theta = 1.0 - h / self.radius;
516        2.0 * PI * (1.0 - cos_theta.clamp(-1.0, 1.0))
517    }
518
519    /// Longitude/latitude (geographic) coordinates for a point on the sphere.
520    ///
521    /// Returns `(longitude, latitude)` in radians.
522    /// Longitude in `(-π, π]`, latitude in `[-π/2, π/2]`.
523    pub fn lon_lat(&self, p: [f64; 3]) -> (f64, f64) {
524        let lon = p[1].atan2(p[0]);
525        let r = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt().max(1e-14);
526        let lat = (p[2] / r).clamp(-1.0, 1.0).asin();
527        (lon, lat)
528    }
529}
530
531// ── Icosphere helpers ─────────────────────────────────────────────────────────
532
533/// Build the base icosahedron: 12 vertices on the unit sphere, 20 triangles.
534fn base_icosahedron() -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
535    let t = (1.0 + 5.0_f64.sqrt()) / 2.0;
536    let s = 1.0 / (1.0 + t * t).sqrt();
537    let ts = t * s;
538    let verts = vec![
539        [-s, ts, 0.0],
540        [s, ts, 0.0],
541        [-s, -ts, 0.0],
542        [s, -ts, 0.0],
543        [0.0, -s, ts],
544        [0.0, s, ts],
545        [0.0, -s, -ts],
546        [0.0, s, -ts],
547        [ts, 0.0, -s],
548        [ts, 0.0, s],
549        [-ts, 0.0, -s],
550        [-ts, 0.0, s],
551    ];
552    let tris = vec![
553        [0, 11, 5],
554        [0, 5, 1],
555        [0, 1, 7],
556        [0, 7, 10],
557        [0, 10, 11],
558        [1, 5, 9],
559        [5, 11, 4],
560        [11, 10, 2],
561        [10, 7, 6],
562        [7, 1, 8],
563        [3, 9, 4],
564        [3, 4, 2],
565        [3, 2, 6],
566        [3, 6, 8],
567        [3, 8, 9],
568        [4, 9, 5],
569        [2, 4, 11],
570        [6, 2, 10],
571        [8, 6, 7],
572        [9, 8, 1],
573    ];
574    (verts, tris)
575}
576
577/// Midpoint of two 3-D points.
578fn midpoint3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
579    [
580        (a[0] + b[0]) * 0.5,
581        (a[1] + b[1]) * 0.5,
582        (a[2] + b[2]) * 0.5,
583    ]
584}
585
586/// One level of icosphere subdivision (each triangle → 4 triangles).
587fn subdivide_icosphere(
588    verts: Vec<[f64; 3]>,
589    tris: Vec<[usize; 3]>,
590) -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
591    use std::collections::HashMap;
592    let mut new_verts = verts.clone();
593    let mut edge_map: HashMap<(usize, usize), usize> = HashMap::new();
594    let mut new_tris = Vec::with_capacity(tris.len() * 4);
595
596    let mut midpoint_idx = |v: &mut Vec<[f64; 3]>, a: usize, b: usize| -> usize {
597        let key = if a < b { (a, b) } else { (b, a) };
598        if let Some(&idx) = edge_map.get(&key) {
599            return idx;
600        }
601        let mp = midpoint3(v[a], v[b]);
602        let idx = v.len();
603        v.push(mp);
604        edge_map.insert(key, idx);
605        idx
606    };
607
608    for &[a, b, c] in &tris {
609        let ab = midpoint_idx(&mut new_verts, a, b);
610        let bc = midpoint_idx(&mut new_verts, b, c);
611        let ca = midpoint_idx(&mut new_verts, c, a);
612        new_tris.push([a, ab, ca]);
613        new_tris.push([b, bc, ab]);
614        new_tris.push([c, ca, bc]);
615        new_tris.push([ab, bc, ca]);
616    }
617    (new_verts, new_tris)
618}
619
620impl Shape for Sphere {
621    fn bounding_box(&self) -> Aabb {
622        let extent = Vec3::new(self.radius, self.radius, self.radius);
623        Aabb::new(-extent, extent)
624    }
625
626    fn support_point(&self, direction: &Vec3) -> Vec3 {
627        let norm = direction.norm();
628        if norm < 1e-10 {
629            return Vec3::new(self.radius, 0.0, 0.0);
630        }
631        direction * (self.radius / norm)
632    }
633
634    fn volume(&self) -> Real {
635        (4.0 / 3.0) * PI * self.radius.powi(3)
636    }
637
638    fn center_of_mass(&self) -> Vec3 {
639        Vec3::zeros()
640    }
641
642    fn inertia_tensor(&self, mass: Real) -> Mat3 {
643        let i = 0.4 * mass * self.radius * self.radius;
644        Mat3::new(i, 0.0, 0.0, 0.0, i, 0.0, 0.0, 0.0, i)
645    }
646
647    fn ray_cast(&self, ray_origin: &Vec3, ray_direction: &Vec3, max_toi: Real) -> Option<RayHit> {
648        // Solve |origin + t * direction|^2 = radius^2
649        let a = ray_direction.dot(ray_direction);
650        let b = 2.0 * ray_origin.dot(ray_direction);
651        let c = ray_origin.dot(ray_origin) - self.radius * self.radius;
652        let discriminant = b * b - 4.0 * a * c;
653
654        if discriminant < 0.0 {
655            return None;
656        }
657
658        let sqrt_disc = discriminant.sqrt();
659        let t1 = (-b - sqrt_disc) / (2.0 * a);
660        let t2 = (-b + sqrt_disc) / (2.0 * a);
661
662        let t = if t1 >= 0.0 { t1 } else { t2 };
663
664        if t < 0.0 || t > max_toi {
665            return None;
666        }
667
668        let point = ray_origin + ray_direction * t;
669        let normal = point.normalize();
670        Some(RayHit {
671            point,
672            normal,
673            toi: t,
674        })
675    }
676}
677
678// ── Sphere extensions (free functions) ───────────────────────────────────────
679
680/// Minimum enclosing sphere using Welzl's algorithm (randomised).
681///
682/// Returns `(center, radius)` for the smallest sphere containing all points.
683pub fn minimum_enclosing_sphere(points: &[[f64; 3]]) -> ([f64; 3], f64) {
684    if points.is_empty() {
685        return ([0.0; 3], 0.0);
686    }
687    let mut pts: Vec<[f64; 3]> = points.to_vec();
688    let n = pts.len();
689    welzl_sphere(&mut pts, &[], n)
690}
691
692/// Sphere-OBB penetration depth.
693pub fn sphere_obb_penetration(
694    sphere_radius: f64,
695    sphere_center: [f64; 3],
696    obb_center: [f64; 3],
697    obb_half_extents: [f64; 3],
698    obb_rotation: [[f64; 3]; 3],
699) -> Option<f64> {
700    let diff = [
701        sphere_center[0] - obb_center[0],
702        sphere_center[1] - obb_center[1],
703        sphere_center[2] - obb_center[2],
704    ];
705    let local = [
706        diff[0] * obb_rotation[0][0] + diff[1] * obb_rotation[0][1] + diff[2] * obb_rotation[0][2],
707        diff[0] * obb_rotation[1][0] + diff[1] * obb_rotation[1][1] + diff[2] * obb_rotation[1][2],
708        diff[0] * obb_rotation[2][0] + diff[1] * obb_rotation[2][1] + diff[2] * obb_rotation[2][2],
709    ];
710    let clamped = [
711        local[0].clamp(-obb_half_extents[0], obb_half_extents[0]),
712        local[1].clamp(-obb_half_extents[1], obb_half_extents[1]),
713        local[2].clamp(-obb_half_extents[2], obb_half_extents[2]),
714    ];
715    let delta = [
716        local[0] - clamped[0],
717        local[1] - clamped[1],
718        local[2] - clamped[2],
719    ];
720    let dist_sq = delta[0] * delta[0] + delta[1] * delta[1] + delta[2] * delta[2];
721    let r_sq = sphere_radius * sphere_radius;
722    if dist_sq > r_sq {
723        return None;
724    }
725    let dist = dist_sq.sqrt();
726    Some(sphere_radius - dist)
727}
728
729/// Generate `n` uniformly distributed random points on the unit sphere surface.
730/// Uses rejection sampling.  `seed` is an LCG seed.
731pub fn random_points_on_sphere(radius: f64, n: usize, seed: u64) -> Vec<[f64; 3]> {
732    let mut rng_state = seed;
733    let mut next_f64 = move || -> f64 {
734        rng_state = rng_state
735            .wrapping_mul(6364136223846793005)
736            .wrapping_add(1442695040888963407);
737        let bits = 0x3FF0000000000000u64 | (rng_state >> 12);
738        f64::from_bits(bits) - 1.0
739    };
740
741    let mut points = Vec::with_capacity(n);
742    while points.len() < n {
743        let x = next_f64() * 2.0 - 1.0;
744        let y = next_f64() * 2.0 - 1.0;
745        let z = next_f64() * 2.0 - 1.0;
746        let len_sq = x * x + y * y + z * z;
747        if !(1e-30..=1.0).contains(&len_sq) {
748            continue;
749        }
750        let len = len_sq.sqrt();
751        points.push([x / len * radius, y / len * radius, z / len * radius]);
752    }
753    points
754}
755
756/// Compute real spherical harmonics coefficient Y_l^m(theta, phi).
757/// Only l = 0, 1, 2 implemented.
758pub fn spherical_harmonic(l: u32, m: i32, theta: f64, phi: f64) -> f64 {
759    match (l, m) {
760        (0, 0) => 1.0 / (2.0 * PI.sqrt()),
761        (1, -1) => (3.0 / (4.0 * PI)).sqrt() * theta.sin() * phi.sin(),
762        (1, 0) => (3.0 / (4.0 * PI)).sqrt() * theta.cos(),
763        (1, 1) => (3.0 / (4.0 * PI)).sqrt() * theta.sin() * phi.cos(),
764        (2, -2) => 0.5 * (15.0 / PI).sqrt() * theta.sin().powi(2) * (2.0 * phi).sin(),
765        (2, -1) => 0.5 * (15.0 / PI).sqrt() * theta.sin() * theta.cos() * phi.sin(),
766        (2, 0) => 0.25 * (5.0 / PI).sqrt() * (3.0 * theta.cos().powi(2) - 1.0),
767        (2, 1) => 0.5 * (15.0 / PI).sqrt() * theta.sin() * theta.cos() * phi.cos(),
768        (2, 2) => 0.25 * (15.0 / PI).sqrt() * theta.sin().powi(2) * (2.0 * phi).cos(),
769        _ => 0.0,
770    }
771}
772
773/// Sphere-sphere intersection volume (lens volume).
774pub fn sphere_sphere_intersection_volume(r1: f64, r2: f64, d: f64) -> f64 {
775    if d >= r1 + r2 {
776        return 0.0;
777    }
778    if d <= (r1 - r2).abs() {
779        let r_min = r1.min(r2);
780        return (4.0 / 3.0) * PI * r_min.powi(3);
781    }
782    let h1 = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
783    let cap1 = (PI / 3.0) * (r1 - h1).powi(2) * (2.0 * r1 + h1);
784    let h2 = d - h1;
785    let cap2 = (PI / 3.0) * (r2 - h2).powi(2) * (2.0 * r2 + h2);
786    cap1 + cap2
787}
788
789// ── Welzl helpers (module-level) ──────────────────────────────────────────────
790
791/// Minimum sphere enclosing up to 4 boundary points.
792fn min_sphere_with_boundary(boundary: &[[f64; 3]]) -> ([f64; 3], f64) {
793    match boundary.len() {
794        0 => ([0.0; 3], 0.0),
795        1 => (boundary[0], 0.0),
796        2 => {
797            let c = [
798                (boundary[0][0] + boundary[1][0]) * 0.5,
799                (boundary[0][1] + boundary[1][1]) * 0.5,
800                (boundary[0][2] + boundary[1][2]) * 0.5,
801            ];
802            let r = sphere_dist3(boundary[0], c);
803            (c, r)
804        }
805        3 => circumsphere_3(boundary[0], boundary[1], boundary[2]),
806        4 => circumsphere_4(boundary[0], boundary[1], boundary[2], boundary[3]),
807        _ => ([0.0; 3], 0.0),
808    }
809}
810
811fn sphere_dist3(a: [f64; 3], b: [f64; 3]) -> f64 {
812    ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2) + (a[2] - b[2]).powi(2)).sqrt()
813}
814
815fn welzl_sphere(pts: &mut [[f64; 3]], boundary: &[[f64; 3]], n: usize) -> ([f64; 3], f64) {
816    if n == 0 || boundary.len() == 4 {
817        return min_sphere_with_boundary(boundary);
818    }
819    let p = pts[n - 1];
820    let (c, r) = welzl_sphere(pts, boundary, n - 1);
821    if sphere_dist3(p, c) <= r + 1e-10 {
822        return (c, r);
823    }
824    let mut new_boundary = boundary.to_vec();
825    new_boundary.push(p);
826    welzl_sphere(pts, &new_boundary, n - 1)
827}
828
829fn circumsphere_3(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> ([f64; 3], f64) {
830    // circumcenter of triangle in 3D
831    let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
832    let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
833    let ab_cross_ac = cross3sph(ab, ac);
834    let denom = 2.0 * dot3sph(ab_cross_ac, ab_cross_ac);
835    if denom.abs() < 1e-30 {
836        // Degenerate: return circumsphere of ab
837        return (
838            [
839                (a[0] + b[0]) * 0.5,
840                (a[1] + b[1]) * 0.5,
841                (a[2] + b[2]) * 0.5,
842            ],
843            sphere_dist3(
844                a,
845                [
846                    (a[0] + b[0]) * 0.5,
847                    (a[1] + b[1]) * 0.5,
848                    (a[2] + b[2]) * 0.5,
849                ],
850            ),
851        );
852    }
853    let i_denom = 1.0 / denom;
854    let ab2 = dot3sph(ab, ab);
855    let ac2 = dot3sph(ac, ac);
856    let ab_cross_ac2 = cross3sph(ab, ac);
857    let ac_cross_ab = cross3sph(ac, ab);
858    let _ = ac_cross_ab;
859    let t1 = cross3sph(scale3sph(ab, ac2), ab_cross_ac2);
860    let t2 = cross3sph(scale3sph(ac, ab2), ab_cross_ac2);
861    let center = [
862        a[0] + (t1[0] - t2[0]) * i_denom,
863        a[1] + (t1[1] - t2[1]) * i_denom,
864        a[2] + (t1[2] - t2[2]) * i_denom,
865    ];
866    let r = sphere_dist3(a, center);
867    (center, r)
868}
869
870fn circumsphere_4(a: [f64; 3], b: [f64; 3], c: [f64; 3], d: [f64; 3]) -> ([f64; 3], f64) {
871    // Use circumsphere of the three points that give the largest sphere
872    let opts = [
873        circumsphere_3(a, b, c),
874        circumsphere_3(a, b, d),
875        circumsphere_3(a, c, d),
876        circumsphere_3(b, c, d),
877    ];
878    // Return the smallest sphere that contains all 4 points
879    for (center, r) in &opts {
880        let all_inside = [a, b, c, d]
881            .iter()
882            .all(|&p| sphere_dist3(p, *center) <= r + 1e-8);
883        if all_inside {
884            return (*center, *r);
885        }
886    }
887    // Fallback: midpoint of ab
888    let cen = [
889        (a[0] + d[0]) * 0.5,
890        (a[1] + d[1]) * 0.5,
891        (a[2] + d[2]) * 0.5,
892    ];
893    let r = sphere_dist3(a, cen);
894    (cen, r)
895}
896
897fn cross3sph(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
898    [
899        a[1] * b[2] - a[2] * b[1],
900        a[2] * b[0] - a[0] * b[2],
901        a[0] * b[1] - a[1] * b[0],
902    ]
903}
904fn dot3sph(a: [f64; 3], b: [f64; 3]) -> f64 {
905    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
906}
907fn scale3sph(a: [f64; 3], s: f64) -> [f64; 3] {
908    [a[0] * s, a[1] * s, a[2] * s]
909}
910
911#[cfg(test)]
912mod proptest_tests {
913    use super::*;
914    use crate::Sphere;
915
916    use proptest::prelude::*;
917
918    proptest! {
919        #[test]
920        fn prop_sphere_volume_scales_cubically(r in 0.01_f64..100.0_f64) {
921            let s1 = Sphere::new(r);
922            let s2 = Sphere::new(2.0 * r);
923            let ratio = s2.volume() / s1.volume();
924            prop_assert!(
925                (ratio - 8.0).abs() < 1e-6,
926                "volume ratio for 2r vs r should be 8, got {}",
927                ratio
928            );
929        }
930
931        #[test]
932        fn prop_sphere_inertia_positive_definite(
933            r in 0.01_f64..100.0_f64,
934            m in 0.01_f64..1000.0_f64,
935        ) {
936            let s = Sphere::new(r);
937            let it = s.inertia_tensor(m);
938            // Diagonal inertia tensor for sphere: all entries equal 0.4*m*r^2 > 0
939            prop_assert!(it[(0, 0)] > 0.0, "Ixx not positive: {}", it[(0, 0)]);
940            prop_assert!(it[(1, 1)] > 0.0, "Iyy not positive: {}", it[(1, 1)]);
941            prop_assert!(it[(2, 2)] > 0.0, "Izz not positive: {}", it[(2, 2)]);
942        }
943
944        #[test]
945        fn prop_sphere_volume_nonneg(r in 0.01_f64..100.0_f64) {
946            let s = Sphere::new(r);
947            prop_assert!(s.volume() >= 0.0, "volume negative: {}", s.volume());
948        }
949
950        #[test]
951        fn prop_sphere_bounding_box_contains_surface(
952            r in 0.01_f64..100.0_f64,
953        ) {
954            let s = Sphere::new(r);
955            let bb = s.bounding_box();
956            // The bounding box should extend exactly [-r, r] in each axis
957            prop_assert!(
958                (bb.min.x + r).abs() < 1e-10,
959                "bb.min.x={} != -r={}",
960                bb.min.x,
961                -r
962            );
963            prop_assert!(
964                (bb.max.x - r).abs() < 1e-10,
965                "bb.max.x={} != r={}",
966                bb.max.x,
967                r
968            );
969        }
970    }
971}
972
973#[cfg(test)]
974mod tests {
975    use super::*;
976    use crate::Sphere;
977    use crate::sphere::PI;
978    use crate::sphere::minimum_enclosing_sphere;
979    use crate::sphere::random_points_on_sphere;
980    use crate::sphere::sphere_obb_penetration;
981    use crate::sphere::sphere_sphere_intersection_volume;
982    use crate::sphere::spherical_harmonic;
983    use oxiphysics_core::math::Vec3;
984
985    #[test]
986    fn test_sphere_bounding_box() {
987        let s = Sphere::new(2.0);
988        let bb = s.bounding_box();
989        assert_eq!(bb.min, Vec3::new(-2.0, -2.0, -2.0));
990        assert_eq!(bb.max, Vec3::new(2.0, 2.0, 2.0));
991    }
992
993    #[test]
994    fn test_sphere_volume() {
995        let s = Sphere::new(1.0);
996        let expected = (4.0 / 3.0) * PI;
997        assert!((s.volume() - expected).abs() < 1e-10);
998    }
999
1000    #[test]
1001    fn test_sphere_surface_area() {
1002        let s = Sphere::new(1.0);
1003        assert!((s.surface_area() - 4.0 * PI).abs() < 1e-10);
1004    }
1005
1006    #[test]
1007    fn test_sphere_inertia() {
1008        let s = Sphere::new(1.0);
1009        let it = s.inertia_tensor(1.0);
1010        assert!((it[(0, 0)] - 0.4).abs() < 1e-10);
1011    }
1012
1013    #[test]
1014    fn test_sphere_inertia_array() {
1015        let s = Sphere::new(1.0);
1016        let it = s.inertia_tensor_array(1.0);
1017        assert!((it[0][0] - 0.4).abs() < 1e-10);
1018        assert!((it[1][1] - 0.4).abs() < 1e-10);
1019        assert!((it[2][2] - 0.4).abs() < 1e-10);
1020        assert!((it[0][1]).abs() < 1e-10);
1021    }
1022
1023    #[test]
1024    fn test_sphere_raycast() {
1025        let s = Sphere::new(1.0);
1026        let origin = Vec3::new(-5.0, 0.0, 0.0);
1027        let dir = Vec3::new(1.0, 0.0, 0.0);
1028        let hit = s.ray_cast(&origin, &dir, 100.0).unwrap();
1029        assert!((hit.toi - 4.0).abs() < 1e-10);
1030        assert!((hit.point.x + 1.0).abs() < 1e-10);
1031    }
1032
1033    #[test]
1034    fn test_sphere_raycast_miss() {
1035        let s = Sphere::new(1.0);
1036        let origin = Vec3::new(-5.0, 5.0, 0.0);
1037        let dir = Vec3::new(1.0, 0.0, 0.0);
1038        assert!(s.ray_cast(&origin, &dir, 100.0).is_none());
1039    }
1040
1041    #[test]
1042    fn test_sphere_closest_point() {
1043        let s = Sphere::new(2.0);
1044        let cp = s.closest_point([4.0, 0.0, 0.0]);
1045        assert!((cp[0] - 2.0).abs() < 1e-10);
1046        assert!(cp[1].abs() < 1e-10);
1047        assert!(cp[2].abs() < 1e-10);
1048    }
1049
1050    #[test]
1051    fn test_sphere_support() {
1052        let s = Sphere::new(3.0);
1053        let sp = s.support([1.0, 0.0, 0.0]);
1054        assert!((sp[0] - 3.0).abs() < 1e-10);
1055        // diagonal
1056        let len = 3.0_f64.sqrt();
1057        let sp2 = s.support([1.0, 1.0, 1.0]);
1058        assert!((sp2[0] - 3.0 / len).abs() < 1e-6);
1059    }
1060
1061    #[test]
1062    fn test_sphere_raycast_array() {
1063        let s = Sphere::new(1.0);
1064        let (t, n) = s
1065            .ray_cast_array([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0)
1066            .unwrap();
1067        assert!((t - 4.0).abs() < 1e-10);
1068        assert!((n[0] + 1.0).abs() < 1e-10);
1069    }
1070
1071    // ── New tests ──
1072
1073    #[test]
1074    fn test_sphere_support_with_center() {
1075        let s = Sphere::new(2.0);
1076        let sp = s.support_with_center([1.0, 2.0, 3.0], [1.0, 0.0, 0.0]);
1077        assert!((sp[0] - 3.0).abs() < 1e-10); // 1 + 2
1078        assert!((sp[1] - 2.0).abs() < 1e-10);
1079        assert!((sp[2] - 3.0).abs() < 1e-10);
1080    }
1081
1082    #[test]
1083    fn test_sphere_minkowski_sum_support() {
1084        let a = Sphere::new(2.0);
1085        let b = Sphere::new(3.0);
1086        let sp = a.minkowski_sum_support(&b, [1.0, 0.0, 0.0]);
1087        assert!((sp[0] - 5.0).abs() < 1e-10);
1088        assert!(sp[1].abs() < 1e-10);
1089    }
1090
1091    #[test]
1092    fn test_sphere_minkowski_diff_support() {
1093        let a = Sphere::new(5.0);
1094        let b = Sphere::new(2.0);
1095        // support_A([1,0,0]) = [5,0,0], support_B([-1,0,0]) = [-2,0,0]
1096        // diff = [5-(-2), 0, 0] = [7, 0, 0]
1097        let sp = a.minkowski_diff_support(&b, [1.0, 0.0, 0.0]);
1098        assert!((sp[0] - 7.0).abs() < 1e-10);
1099    }
1100
1101    #[test]
1102    fn test_bounding_sphere_from_points_empty() {
1103        let (c, r) = Sphere::bounding_sphere_from_points(&[]);
1104        assert_eq!(c, [0.0, 0.0, 0.0]);
1105        assert_eq!(r, 0.0);
1106    }
1107
1108    #[test]
1109    fn test_bounding_sphere_from_points_single() {
1110        let (c, r) = Sphere::bounding_sphere_from_points(&[[1.0, 2.0, 3.0]]);
1111        assert_eq!(c, [1.0, 2.0, 3.0]);
1112        assert_eq!(r, 0.0);
1113    }
1114
1115    #[test]
1116    fn test_bounding_sphere_from_points_two() {
1117        let pts = [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]];
1118        let (c, r) = Sphere::bounding_sphere_from_points(&pts);
1119        assert!((c[0] - 1.0).abs() < 1e-10);
1120        assert!((r - 1.0).abs() < 1e-10);
1121    }
1122
1123    #[test]
1124    fn test_bounding_sphere_contains_all_points() {
1125        let pts = [
1126            [1.0, 0.0, 0.0],
1127            [-1.0, 0.0, 0.0],
1128            [0.0, 1.0, 0.0],
1129            [0.0, -1.0, 0.0],
1130            [0.0, 0.0, 1.0],
1131            [0.0, 0.0, -1.0],
1132        ];
1133        let (c, r) = Sphere::bounding_sphere_from_points(&pts);
1134        for p in &pts {
1135            let dx = p[0] - c[0];
1136            let dy = p[1] - c[1];
1137            let dz = p[2] - c[2];
1138            let dist = (dx * dx + dy * dy + dz * dz).sqrt();
1139            assert!(
1140                dist <= r + 1e-6,
1141                "point {:?} outside bounding sphere (dist={}, r={})",
1142                p,
1143                dist,
1144                r
1145            );
1146        }
1147    }
1148
1149    #[test]
1150    fn test_sphere_intersection_overlap() {
1151        let a = Sphere::new(1.0);
1152        let b = Sphere::new(1.0);
1153        assert!(a.intersects_sphere([0.0, 0.0, 0.0], &b, [1.5, 0.0, 0.0]));
1154    }
1155
1156    #[test]
1157    fn test_sphere_intersection_no_overlap() {
1158        let a = Sphere::new(1.0);
1159        let b = Sphere::new(1.0);
1160        assert!(!a.intersects_sphere([0.0, 0.0, 0.0], &b, [3.0, 0.0, 0.0]));
1161    }
1162
1163    #[test]
1164    fn test_sphere_intersection_touching() {
1165        let a = Sphere::new(1.0);
1166        let b = Sphere::new(1.0);
1167        assert!(a.intersects_sphere([0.0, 0.0, 0.0], &b, [2.0, 0.0, 0.0]));
1168    }
1169
1170    #[test]
1171    fn test_sphere_intersection_circle() {
1172        let a = Sphere::new(2.0);
1173        let b = Sphere::new(2.0);
1174        let result = a.sphere_intersection_circle([0.0, 0.0, 0.0], &b, [2.0, 0.0, 0.0]);
1175        assert!(result.is_some());
1176        let (center, normal, radius) = result.unwrap();
1177        assert!((center[0] - 1.0).abs() < 1e-10); // midpoint along axis
1178        assert!((normal[0] - 1.0).abs() < 1e-10); // axis direction
1179        assert!(radius > 0.0);
1180    }
1181
1182    #[test]
1183    fn test_sphere_intersection_circle_no_overlap() {
1184        let a = Sphere::new(1.0);
1185        let b = Sphere::new(1.0);
1186        let result = a.sphere_intersection_circle([0.0, 0.0, 0.0], &b, [5.0, 0.0, 0.0]);
1187        assert!(result.is_none());
1188    }
1189
1190    #[test]
1191    fn test_sphere_intersection_circle_concentric() {
1192        let a = Sphere::new(2.0);
1193        let b = Sphere::new(1.0);
1194        let result = a.sphere_intersection_circle([0.0, 0.0, 0.0], &b, [0.0, 0.0, 0.0]);
1195        assert!(result.is_none());
1196    }
1197
1198    #[test]
1199    fn test_closest_point_to_line() {
1200        let s = Sphere::new(1.0);
1201        // Line along Y axis, offset by x=3
1202        let cp = s.closest_point_to_line([3.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1203        // Closest point on line to origin is (3,0,0), so closest on sphere is (1,0,0)
1204        assert!((cp[0] - 1.0).abs() < 1e-10);
1205        assert!(cp[1].abs() < 1e-10);
1206        assert!(cp[2].abs() < 1e-10);
1207    }
1208
1209    #[test]
1210    fn test_closest_point_to_line_through_center() {
1211        let s = Sphere::new(2.0);
1212        // Line through origin along X
1213        let cp = s.closest_point_to_line([0.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1214        // Closest on line to origin is origin itself; sphere returns +X
1215        assert!((cp[0] - 2.0).abs() < 1e-10);
1216    }
1217
1218    #[test]
1219    fn test_closest_point_to_plane() {
1220        let s = Sphere::new(3.0);
1221        // Plane x = 5 => normal=[1,0,0], d=5
1222        let cp = s.closest_point_to_plane([1.0, 0.0, 0.0], 5.0);
1223        assert!((cp[0] - 3.0).abs() < 1e-10);
1224        assert!(cp[1].abs() < 1e-10);
1225    }
1226
1227    #[test]
1228    fn test_closest_point_to_plane_negative() {
1229        let s = Sphere::new(2.0);
1230        // Plane x = -5 => normal=[1,0,0], d=-5
1231        let cp = s.closest_point_to_plane([1.0, 0.0, 0.0], -5.0);
1232        assert!((cp[0] + 2.0).abs() < 1e-10); // closest in -X direction
1233    }
1234
1235    #[test]
1236    fn test_sphere_sweep_hit() {
1237        let s = Sphere::new(0.5);
1238        let t = s.sphere_sweep([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [5.0, 0.0, 0.0], 0.5, 10.0);
1239        assert!(t.is_some());
1240        let t = t.unwrap();
1241        assert!((t - 4.0).abs() < 1e-10); // combined radius = 1, distance = 5
1242    }
1243
1244    #[test]
1245    fn test_sphere_sweep_miss() {
1246        let s = Sphere::new(0.5);
1247        let t = s.sphere_sweep([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 5.0, 0.0], 0.5, 10.0);
1248        assert!(t.is_none());
1249    }
1250
1251    #[test]
1252    fn test_sphere_sweep_already_overlapping() {
1253        let s = Sphere::new(2.0);
1254        let t = s.sphere_sweep([0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], 2.0, 10.0);
1255        assert!(t.is_some());
1256        assert!((t.unwrap()).abs() < 1e-10);
1257    }
1258
1259    #[test]
1260    fn test_sphere_signed_distance() {
1261        let s = Sphere::new(2.0);
1262        assert!((s.signed_distance([3.0, 0.0, 0.0]) - 1.0).abs() < 1e-10);
1263        assert!((s.signed_distance([1.0, 0.0, 0.0]) - (-1.0)).abs() < 1e-10);
1264        assert!((s.signed_distance([2.0, 0.0, 0.0])).abs() < 1e-10);
1265    }
1266
1267    #[test]
1268    fn test_sphere_contains_point() {
1269        let s = Sphere::new(2.0);
1270        assert!(s.contains_point([0.0, 0.0, 0.0]));
1271        assert!(s.contains_point([1.0, 1.0, 0.0]));
1272        assert!(!s.contains_point([3.0, 0.0, 0.0]));
1273    }
1274
1275    #[test]
1276    fn test_sphere_project_inside() {
1277        let s = Sphere::new(2.0);
1278        let p = s.project_inside([1.0, 0.0, 0.0]);
1279        assert_eq!(p, [1.0, 0.0, 0.0]); // already inside
1280
1281        let p2 = s.project_inside([5.0, 0.0, 0.0]);
1282        assert!((p2[0] - 2.0).abs() < 1e-10); // clamped to surface
1283    }
1284
1285    // ── minimum_enclosing_sphere ─────────────────────────────────────────────
1286
1287    #[test]
1288    fn test_minimum_enclosing_sphere_empty() {
1289        let (c, r) = minimum_enclosing_sphere(&[]);
1290        assert_eq!(c, [0.0; 3]);
1291        assert_eq!(r, 0.0);
1292    }
1293
1294    #[test]
1295    fn test_minimum_enclosing_sphere_two_points() {
1296        let pts = [[0.0, 0.0, 0.0], [4.0, 0.0, 0.0]];
1297        let (c, r) = minimum_enclosing_sphere(&pts);
1298        // Center at midpoint (2,0,0), radius = 2
1299        assert!((c[0] - 2.0).abs() < 0.1, "c[0]={}", c[0]);
1300        assert!((r - 2.0).abs() < 0.1, "r={r}");
1301    }
1302
1303    #[test]
1304    fn test_minimum_enclosing_sphere_contains_all() {
1305        let pts = [
1306            [1.0, 0.0, 0.0],
1307            [-1.0, 0.0, 0.0],
1308            [0.0, 1.0, 0.0],
1309            [0.0, -1.0, 0.0],
1310            [0.0, 0.0, 1.0],
1311            [0.0, 0.0, -1.0],
1312        ];
1313        let (c, r) = minimum_enclosing_sphere(&pts);
1314        for p in &pts {
1315            let d = ((p[0] - c[0]).powi(2) + (p[1] - c[1]).powi(2) + (p[2] - c[2]).powi(2)).sqrt();
1316            assert!(d <= r + 1e-6, "point {:?} outside sphere r={r}", p);
1317        }
1318    }
1319
1320    // ── sphere_obb_penetration ───────────────────────────────────────────────
1321
1322    #[test]
1323    fn test_sphere_obb_penetration_overlap() {
1324        let identity = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1325        let pen = sphere_obb_penetration(
1326            1.0,
1327            [1.5, 0.0, 0.0],
1328            [0.0, 0.0, 0.0],
1329            [1.0, 1.0, 1.0],
1330            identity,
1331        );
1332        assert!(pen.is_some(), "sphere at 1.5 should overlap unit OBB");
1333        assert!(pen.unwrap() > 0.0);
1334    }
1335
1336    #[test]
1337    fn test_sphere_obb_penetration_no_overlap() {
1338        let identity = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1339        let pen = sphere_obb_penetration(
1340            0.5,
1341            [5.0, 0.0, 0.0],
1342            [0.0, 0.0, 0.0],
1343            [1.0, 1.0, 1.0],
1344            identity,
1345        );
1346        assert!(pen.is_none());
1347    }
1348
1349    // ── random_points_on_sphere ──────────────────────────────────────────────
1350
1351    #[test]
1352    fn test_random_points_on_sphere_count() {
1353        let pts = random_points_on_sphere(1.0, 100, 42);
1354        assert_eq!(pts.len(), 100);
1355    }
1356
1357    #[test]
1358    fn test_random_points_on_sphere_on_surface() {
1359        let r = 2.5;
1360        let pts = random_points_on_sphere(r, 50, 123);
1361        for p in &pts {
1362            let dist = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
1363            assert!(
1364                (dist - r).abs() < 1e-8,
1365                "point not on sphere surface: dist={dist}"
1366            );
1367        }
1368    }
1369
1370    // ── spherical_harmonic ───────────────────────────────────────────────────
1371
1372    #[test]
1373    fn test_spherical_harmonic_y00_constant() {
1374        // Y_0^0 is constant
1375        let v1 = spherical_harmonic(0, 0, 0.5, 1.0);
1376        let v2 = spherical_harmonic(0, 0, 1.2, 2.5);
1377        assert!((v1 - v2).abs() < 1e-14, "Y_0^0 should be constant");
1378    }
1379
1380    #[test]
1381    fn test_spherical_harmonic_y10_at_poles() {
1382        // Y_1^0(0, phi) = sqrt(3/(4π)) * cos(0) = sqrt(3/(4π))
1383        let val = spherical_harmonic(1, 0, 0.0, 0.0);
1384        let expected = (3.0 / (4.0 * PI)).sqrt();
1385        assert!(
1386            (val - expected).abs() < 1e-10,
1387            "val={val} expected={expected}"
1388        );
1389    }
1390
1391    #[test]
1392    fn test_spherical_harmonic_unsupported_returns_zero() {
1393        let val = spherical_harmonic(5, 3, 1.0, 1.0);
1394        assert_eq!(val, 0.0);
1395    }
1396
1397    // ── sphere_sphere_intersection_volume ────────────────────────────────────
1398
1399    #[test]
1400    fn test_sphere_sphere_intersection_volume_no_overlap() {
1401        let vol = sphere_sphere_intersection_volume(1.0, 1.0, 3.0);
1402        assert_eq!(vol, 0.0);
1403    }
1404
1405    #[test]
1406    fn test_sphere_sphere_intersection_volume_contained() {
1407        // Sphere of radius 0.5 fully inside sphere of radius 2
1408        let vol = sphere_sphere_intersection_volume(2.0, 0.5, 0.0);
1409        let expected = (4.0 / 3.0) * PI * 0.5_f64.powi(3);
1410        assert!(
1411            (vol - expected).abs() < 1e-10,
1412            "vol={vol} expected={expected}"
1413        );
1414    }
1415
1416    #[test]
1417    fn test_sphere_sphere_intersection_volume_symmetric() {
1418        // For equal radii and some distance, intersection should be symmetric
1419        let vol1 = sphere_sphere_intersection_volume(1.0, 1.0, 1.0);
1420        let vol2 = sphere_sphere_intersection_volume(1.0, 1.0, 1.0);
1421        assert!((vol1 - vol2).abs() < 1e-14);
1422        assert!(vol1 > 0.0);
1423    }
1424
1425    // ── Extended tests for geodesic sphere, caps, Fibonacci, hemisphere, conformal ──
1426
1427    #[test]
1428    fn test_geodesic_icosphere_base_vert_count() {
1429        let s = Sphere::new(1.0);
1430        let (verts, tris) = s.geodesic_icosphere(0);
1431        assert_eq!(verts.len(), 12);
1432        assert_eq!(tris.len(), 20);
1433    }
1434
1435    #[test]
1436    fn test_geodesic_icosphere_subdivision_1() {
1437        let s = Sphere::new(1.0);
1438        let (verts, tris) = s.geodesic_icosphere(1);
1439        // After 1 subdivision: 42 verts, 80 tris
1440        assert_eq!(verts.len(), 42);
1441        assert_eq!(tris.len(), 80);
1442    }
1443
1444    #[test]
1445    fn test_geodesic_icosphere_vertices_on_sphere() {
1446        let s = Sphere::new(2.0);
1447        let (verts, _) = s.geodesic_icosphere(1);
1448        for v in &verts {
1449            let r = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
1450            assert!((r - 2.0).abs() < 1e-9, "vertex not on sphere r={r}");
1451        }
1452    }
1453
1454    #[test]
1455    fn test_geodesic_icosphere_subdivision_2_count() {
1456        let s = Sphere::new(1.0);
1457        let (verts, tris) = s.geodesic_icosphere(2);
1458        // 2 subdivisions: 162 verts, 320 tris
1459        assert_eq!(verts.len(), 162);
1460        assert_eq!(tris.len(), 320);
1461    }
1462
1463    #[test]
1464    fn test_spherical_cap_volume_zero_height() {
1465        let s = Sphere::new(2.0);
1466        assert_eq!(s.spherical_cap_volume(0.0), 0.0);
1467    }
1468
1469    #[test]
1470    fn test_spherical_cap_volume_full_sphere() {
1471        let s = Sphere::new(2.0);
1472        // h = 2r → full sphere volume = (4/3)π r³
1473        let v = s.spherical_cap_volume(4.0);
1474        let full = s.volume();
1475        assert!((v - full).abs() < 1e-9, "cap vol {v} vs sphere vol {full}");
1476    }
1477
1478    #[test]
1479    fn test_spherical_cap_volume_hemisphere() {
1480        let s = Sphere::new(1.0);
1481        // h = r → hemisphere: V = (2/3)π r³
1482        let v = s.spherical_cap_volume(1.0);
1483        let expected = (2.0 / 3.0) * PI;
1484        assert!((v - expected).abs() < 1e-9);
1485    }
1486
1487    #[test]
1488    fn test_spherical_cap_area_zero() {
1489        let s = Sphere::new(1.0);
1490        assert_eq!(s.spherical_cap_area(0.0), 0.0);
1491    }
1492
1493    #[test]
1494    fn test_spherical_cap_area_full_sphere() {
1495        let s = Sphere::new(1.0);
1496        // h = 2r → full sphere surface area = 4π r²
1497        let a = s.spherical_cap_area(2.0);
1498        assert!((a - 4.0 * PI).abs() < 1e-9);
1499    }
1500
1501    #[test]
1502    fn test_spherical_zone_area_hemisphere() {
1503        let s = Sphere::new(1.0);
1504        // Zone from bottom (h=0) to equator (h=r): 2π r * r = 2π
1505        let a = s.spherical_zone_area(0.0, 1.0);
1506        assert!((a - 2.0 * PI).abs() < 1e-9);
1507    }
1508
1509    #[test]
1510    fn test_spherical_zone_area_symmetric() {
1511        let s = Sphere::new(2.0);
1512        let a1 = s.spherical_zone_area(0.0, 1.0);
1513        let a2 = s.spherical_zone_area(1.0, 0.0);
1514        assert!((a1 - a2).abs() < 1e-12);
1515    }
1516
1517    #[test]
1518    fn test_hemisphere_cosine_sample_count() {
1519        let s = Sphere::new(1.0);
1520        let pts = s.hemisphere_cosine_sample(50, 42);
1521        assert_eq!(pts.len(), 50);
1522    }
1523
1524    #[test]
1525    fn test_hemisphere_cosine_sample_on_sphere() {
1526        let s = Sphere::new(2.0);
1527        let pts = s.hemisphere_cosine_sample(30, 7);
1528        for p in &pts {
1529            let r = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
1530            assert!((r - 2.0).abs() < 1e-9, "r={r}");
1531        }
1532    }
1533
1534    #[test]
1535    fn test_hemisphere_cosine_sample_positive_z() {
1536        let s = Sphere::new(1.0);
1537        let pts = s.hemisphere_cosine_sample(40, 99);
1538        for p in &pts {
1539            assert!(p[2] >= 0.0, "z={}", p[2]);
1540        }
1541    }
1542
1543    #[test]
1544    fn test_hemisphere_uniform_sample_count() {
1545        let s = Sphere::new(1.0);
1546        let pts = s.hemisphere_uniform_sample(25, 11);
1547        assert_eq!(pts.len(), 25);
1548    }
1549
1550    #[test]
1551    fn test_hemisphere_uniform_sample_positive_z() {
1552        let s = Sphere::new(1.0);
1553        let pts = s.hemisphere_uniform_sample(30, 55);
1554        for p in &pts {
1555            assert!(p[2] >= 0.0, "z={}", p[2]);
1556        }
1557    }
1558
1559    #[test]
1560    fn test_fibonacci_sphere_count() {
1561        let s = Sphere::new(1.0);
1562        let pts = s.fibonacci_sphere(50);
1563        assert_eq!(pts.len(), 50);
1564    }
1565
1566    #[test]
1567    fn test_fibonacci_sphere_on_sphere() {
1568        let s = Sphere::new(3.0);
1569        let pts = s.fibonacci_sphere(30);
1570        for p in &pts {
1571            let r = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
1572            assert!((r - 3.0).abs() < 1e-9, "r={r}");
1573        }
1574    }
1575
1576    #[test]
1577    fn test_fibonacci_sphere_roughly_uniform() {
1578        // No two adjacent Fibonacci points should be exactly equal
1579        let s = Sphere::new(1.0);
1580        let pts = s.fibonacci_sphere(20);
1581        for i in 0..pts.len() - 1 {
1582            let p = pts[i];
1583            let q = pts[i + 1];
1584            let d = ((p[0] - q[0]).powi(2) + (p[1] - q[1]).powi(2) + (p[2] - q[2]).powi(2)).sqrt();
1585            assert!(d > 0.01, "consecutive Fibonacci points too close d={d}");
1586        }
1587    }
1588
1589    #[test]
1590    fn test_stereographic_project_north_pole() {
1591        let s = Sphere::new(1.0);
1592        // North pole (0, 0, 1) → (0, 0)
1593        let uv = s.stereographic_project([0.0, 0.0, 1.0]);
1594        assert!(uv.is_some());
1595        let [x, y] = uv.unwrap();
1596        assert!(x.abs() < 1e-10);
1597        assert!(y.abs() < 1e-10);
1598    }
1599
1600    #[test]
1601    fn test_stereographic_project_south_pole_none() {
1602        let s = Sphere::new(1.0);
1603        let uv = s.stereographic_project([0.0, 0.0, -1.0]);
1604        assert!(uv.is_none());
1605    }
1606
1607    #[test]
1608    fn test_stereographic_roundtrip() {
1609        let s = Sphere::new(1.0);
1610        let p_orig = [0.6, 0.0, 0.8]; // on unit sphere
1611        let uv = s.stereographic_project(p_orig).unwrap();
1612        let p_back = s.stereographic_unproject(uv);
1613        for i in 0..3 {
1614            assert!((p_orig[i] - p_back[i]).abs() < 1e-9, "mismatch at [{i}]");
1615        }
1616    }
1617
1618    #[test]
1619    fn test_stereographic_unproject_on_sphere() {
1620        let s = Sphere::new(2.0);
1621        let p = s.stereographic_unproject([1.0, 0.5]);
1622        let r = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
1623        assert!((r - 2.0).abs() < 1e-9, "r={r}");
1624    }
1625
1626    #[test]
1627    fn test_sh_l3_m0_at_pole() {
1628        // Y_3^0(θ=0) = (1/4)*sqrt(7/π)*(5*1 - 3) = (1/4)*sqrt(7/π)*2
1629        let val = Sphere::sh_l3(0, 0.0, 0.0);
1630        let expected = 0.25 * (7.0 / PI).sqrt() * 2.0;
1631        assert!((val - expected).abs() < 1e-10);
1632    }
1633
1634    #[test]
1635    fn test_sh_l3_unknown_m_zero() {
1636        assert_eq!(Sphere::sh_l3(5, 1.0, 1.0), 0.0);
1637    }
1638
1639    #[test]
1640    fn test_cap_solid_angle_hemisphere() {
1641        let s = Sphere::new(1.0);
1642        // h = r = 1 → cos_theta = 0 → Ω = 2π
1643        let omega = s.cap_solid_angle(1.0);
1644        assert!((omega - 2.0 * PI).abs() < 1e-10);
1645    }
1646
1647    #[test]
1648    fn test_cap_solid_angle_zero() {
1649        let s = Sphere::new(1.0);
1650        assert!((s.cap_solid_angle(0.0)).abs() < 1e-10);
1651    }
1652
1653    #[test]
1654    fn test_cap_solid_angle_full_sphere() {
1655        let s = Sphere::new(1.0);
1656        // h = 2r → Ω = 4π
1657        let omega = s.cap_solid_angle(2.0);
1658        assert!((omega - 4.0 * PI).abs() < 1e-10);
1659    }
1660
1661    #[test]
1662    fn test_lon_lat_north_pole() {
1663        let s = Sphere::new(1.0);
1664        let (_, lat) = s.lon_lat([0.0, 0.0, 1.0]);
1665        assert!((lat - PI / 2.0).abs() < 1e-10);
1666    }
1667
1668    #[test]
1669    fn test_lon_lat_south_pole() {
1670        let s = Sphere::new(1.0);
1671        let (_, lat) = s.lon_lat([0.0, 0.0, -1.0]);
1672        assert!((lat + PI / 2.0).abs() < 1e-10);
1673    }
1674
1675    #[test]
1676    fn test_lon_lat_equator() {
1677        let s = Sphere::new(1.0);
1678        let (lon, lat) = s.lon_lat([1.0, 0.0, 0.0]);
1679        assert!(lat.abs() < 1e-10);
1680        assert!(lon.abs() < 1e-10);
1681    }
1682
1683    #[test]
1684    fn test_fibonacci_sphere_single_point() {
1685        let s = Sphere::new(1.0);
1686        let pts = s.fibonacci_sphere(1);
1687        assert_eq!(pts.len(), 1);
1688        let r = (pts[0][0].powi(2) + pts[0][1].powi(2) + pts[0][2].powi(2)).sqrt();
1689        assert!((r - 1.0).abs() < 1e-9);
1690    }
1691
1692    #[test]
1693    fn test_spherical_cap_volume_increases_with_h() {
1694        let s = Sphere::new(1.0);
1695        let v1 = s.spherical_cap_volume(0.5);
1696        let v2 = s.spherical_cap_volume(1.0);
1697        let v3 = s.spherical_cap_volume(1.5);
1698        assert!(v1 < v2 && v2 < v3);
1699    }
1700
1701    #[test]
1702    fn test_sh_l3_all_m_finite() {
1703        for m in -3..=3 {
1704            let v = Sphere::sh_l3(m, 0.5, 1.0);
1705            assert!(v.is_finite(), "sh_l3({m}) not finite: {v}");
1706        }
1707    }
1708}