1use crate::shape::{RayHit, Shape};
7use oxiphysics_core::Aabb;
8use oxiphysics_core::math::{Mat3, Real, Vec3};
9use std::f64::consts::PI;
10
11#[derive(Debug, Clone)]
13pub struct Sphere {
14 pub radius: Real,
16}
17
18impl Sphere {
19 pub fn new(radius: Real) -> Self {
21 Self { radius }
22 }
23
24 pub fn surface_area(&self) -> Real {
26 4.0 * PI * self.radius * self.radius
27 }
28
29 pub fn volume_explicit(&self) -> Real {
31 (4.0 / 3.0) * PI * self.radius.powi(3)
32 }
33
34 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 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 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 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 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 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 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 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 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 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 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 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; }
194
195 let r1 = self.radius;
196 let r2 = other.radius;
197
198 if d > r1 + r2 || d < (r1 - r2).abs() {
199 return None; }
201
202 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 pub fn closest_point_to_line(&self, line_point: [f64; 3], line_dir: [f64; 3]) -> [f64; 3] {
221 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 pub fn closest_point_to_plane(&self, normal: [f64; 3], d: f64) -> [f64; 3] {
242 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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; 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 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 pub fn stereographic_project(&self, p: [f64; 3]) -> Option<[f64; 2]> {
461 let r = self.radius;
462 let denom = r + p[2]; if denom.abs() < 1e-10 * r {
465 return None; }
467 let scale = 2.0 * r / denom;
468 Some([p[0] * scale, p[1] * scale])
469 }
470
471 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 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 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 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
531fn 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
577fn 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
586fn 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 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
678pub 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
692pub 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
729pub 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
756pub 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
773pub 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
789fn 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 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 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 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 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 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 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 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 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 #[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); 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 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); assert!((normal[0] - 1.0).abs() < 1e-10); 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 let cp = s.closest_point_to_line([3.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1203 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 let cp = s.closest_point_to_line([0.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1214 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 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 let cp = s.closest_point_to_plane([1.0, 0.0, 0.0], -5.0);
1232 assert!((cp[0] + 2.0).abs() < 1e-10); }
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); }
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]); let p2 = s.project_inside([5.0, 0.0, 0.0]);
1282 assert!((p2[0] - 2.0).abs() < 1e-10); }
1284
1285 #[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 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 #[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 #[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 #[test]
1373 fn test_spherical_harmonic_y00_constant() {
1374 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 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 #[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 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 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 #[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 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 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 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 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 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 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 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 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]; 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 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 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 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}