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