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 #[allow(dead_code)]
26 pub fn surface_area(&self) -> Real {
27 4.0 * PI * self.radius * self.radius
28 }
29
30 #[allow(dead_code)]
32 pub fn volume_explicit(&self) -> Real {
33 (4.0 / 3.0) * PI * self.radius.powi(3)
34 }
35
36 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 #[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 #[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; }
206
207 let r1 = self.radius;
208 let r2 = other.radius;
209
210 if d > r1 + r2 || d < (r1 - r2).abs() {
211 return None; }
213
214 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 #[allow(dead_code)]
233 pub fn closest_point_to_line(&self, line_point: [f64; 3], line_dir: [f64; 3]) -> [f64; 3] {
234 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 #[allow(dead_code)]
255 pub fn closest_point_to_plane(&self, normal: [f64; 3], d: f64) -> [f64; 3] {
256 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 #[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 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 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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 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 #[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; 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 #[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 #[allow(dead_code)]
486 pub fn stereographic_project(&self, p: [f64; 3]) -> Option<[f64; 2]> {
487 let r = self.radius;
488 let denom = r + p[2]; if denom.abs() < 1e-10 * r {
491 return None; }
493 let scale = 2.0 * r / denom;
494 Some([p[0] * scale, p[1] * scale])
495 }
496
497 #[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 #[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 #[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 #[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
561fn 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
607fn 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
616fn 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 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#[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#[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#[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#[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#[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
824fn 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 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 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 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 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 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 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 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 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 #[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); 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 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); assert!((normal[0] - 1.0).abs() < 1e-10); 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 let cp = s.closest_point_to_line([3.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
1238 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 let cp = s.closest_point_to_line([0.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1249 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 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 let cp = s.closest_point_to_plane([1.0, 0.0, 0.0], -5.0);
1267 assert!((cp[0] + 2.0).abs() < 1e-10); }
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); }
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]); let p2 = s.project_inside([5.0, 0.0, 0.0]);
1317 assert!((p2[0] - 2.0).abs() < 1e-10); }
1319
1320 #[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 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 #[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 #[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 #[test]
1408 fn test_spherical_harmonic_y00_constant() {
1409 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 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 #[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 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 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 #[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 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 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 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 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 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 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 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 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]; 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 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 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 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}