1use glam::{Vec2, Vec3};
4use std::f32::consts::PI;
5
6#[derive(Clone, Copy, Debug, PartialEq)]
10pub struct SphericalCoord {
11 pub theta: f32,
13 pub phi: f32,
15}
16
17impl SphericalCoord {
18 pub fn new(theta: f32, phi: f32) -> Self {
19 Self { theta, phi }
20 }
21}
22
23pub fn to_cartesian(coord: SphericalCoord, radius: f32) -> Vec3 {
25 let sin_theta = coord.theta.sin();
26 Vec3::new(
27 radius * sin_theta * coord.phi.cos(),
28 radius * sin_theta * coord.phi.sin(),
29 radius * coord.theta.cos(),
30 )
31}
32
33pub fn from_cartesian(pos: Vec3) -> SphericalCoord {
36 let r = pos.length();
37 if r < 1e-10 {
38 return SphericalCoord::new(0.0, 0.0);
39 }
40 let theta = (pos.z / r).clamp(-1.0, 1.0).acos();
41 let phi = pos.y.atan2(pos.x);
42 let phi = if phi < 0.0 { phi + 2.0 * PI } else { phi };
43 SphericalCoord::new(theta, phi)
44}
45
46pub fn great_circle_distance(a: SphericalCoord, b: SphericalCoord) -> f32 {
49 let ca = to_cartesian(a, 1.0);
50 let cb = to_cartesian(b, 1.0);
51 let dot = ca.dot(cb).clamp(-1.0, 1.0);
52 dot.acos()
53}
54
55pub fn geodesic(a: SphericalCoord, b: SphericalCoord, steps: usize) -> Vec<Vec3> {
58 if steps < 2 {
59 return vec![to_cartesian(a, 1.0), to_cartesian(b, 1.0)];
60 }
61 let pa = to_cartesian(a, 1.0);
62 let pb = to_cartesian(b, 1.0);
63
64 let dot = pa.dot(pb).clamp(-1.0, 1.0);
65 let omega = dot.acos();
66
67 if omega.abs() < 1e-8 {
68 return vec![pa; steps];
70 }
71
72 let sin_omega = omega.sin();
73 let mut result = Vec::with_capacity(steps);
74
75 for i in 0..steps {
76 let t = i as f32 / (steps - 1) as f32;
77 let a_coeff = ((1.0 - t) * omega).sin() / sin_omega;
78 let b_coeff = (t * omega).sin() / sin_omega;
79 result.push(pa * a_coeff + pb * b_coeff);
80 }
81 result
82}
83
84pub fn spherical_triangle_area(a: SphericalCoord, b: SphericalCoord, c: SphericalCoord) -> f32 {
88 let pa = to_cartesian(a, 1.0);
89 let pb = to_cartesian(b, 1.0);
90 let pc = to_cartesian(c, 1.0);
91
92 let ab = pb - pa;
94 let ac = pc - pa;
95 let ba = pa - pb;
96 let bc = pc - pb;
97 let ca = pa - pc;
98 let cb = pb - pc;
99
100 let n_ab = pa.cross(pb);
102 let n_ac = pa.cross(pc);
103 let n_ba = pb.cross(pa);
104 let n_bc = pb.cross(pc);
105 let n_ca = pc.cross(pa);
106 let n_cb = pc.cross(pb);
107
108 let angle_a = angle_between_planes(n_ab, n_ac);
109 let angle_b = angle_between_planes(n_ba, n_bc);
110 let angle_c = angle_between_planes(n_ca, n_cb);
111
112 let excess = angle_a + angle_b + angle_c - PI;
113 excess.abs()
114}
115
116fn angle_between_planes(n1: Vec3, n2: Vec3) -> f32 {
117 let l1 = n1.length();
118 let l2 = n2.length();
119 if l1 < 1e-10 || l2 < 1e-10 {
120 return 0.0;
121 }
122 let cos_angle = n1.dot(n2) / (l1 * l2);
123 cos_angle.clamp(-1.0, 1.0).acos()
124}
125
126pub fn stereographic_projection(point3d: Vec3) -> Vec2 {
132 let denom = 1.0 - point3d.z;
133 if denom.abs() < 1e-8 {
134 return Vec2::new(f32::MAX, f32::MAX);
136 }
137 Vec2::new(point3d.x / denom, point3d.y / denom)
138}
139
140pub fn inverse_stereographic(point2d: Vec2) -> Vec3 {
142 let x = point2d.x;
143 let y = point2d.y;
144 let r_sq = x * x + y * y;
145 let denom = r_sq + 1.0;
146 Vec3::new(
147 2.0 * x / denom,
148 2.0 * y / denom,
149 (r_sq - 1.0) / denom,
150 )
151}
152
153pub struct SphericalGrid {
157 pub lat_divisions: usize,
158 pub lon_divisions: usize,
159}
160
161impl SphericalGrid {
162 pub fn new(lat_divisions: usize, lon_divisions: usize) -> Self {
163 Self {
164 lat_divisions,
165 lon_divisions,
166 }
167 }
168
169 pub fn generate(&self, radius: f32) -> Vec<Vec3> {
171 let mut points = Vec::new();
172 for i in 0..=self.lat_divisions {
173 let theta = PI * i as f32 / self.lat_divisions as f32;
174 for j in 0..self.lon_divisions {
175 let phi = 2.0 * PI * j as f32 / self.lon_divisions as f32;
176 let coord = SphericalCoord::new(theta, phi);
177 points.push(to_cartesian(coord, radius));
178 }
179 }
180 points
181 }
182
183 pub fn generate_lines(&self, radius: f32) -> Vec<(Vec3, Vec3)> {
185 let mut lines = Vec::new();
186 let points = self.generate(radius);
187
188 for i in 0..=self.lat_divisions {
189 for j in 0..self.lon_divisions {
190 let idx = i * self.lon_divisions + j;
191 let next_j = i * self.lon_divisions + (j + 1) % self.lon_divisions;
192 if idx < points.len() && next_j < points.len() {
193 lines.push((points[idx], points[next_j]));
194 }
195 if i < self.lat_divisions {
196 let next_i = (i + 1) * self.lon_divisions + j;
197 if idx < points.len() && next_i < points.len() {
198 lines.push((points[idx], points[next_i]));
199 }
200 }
201 }
202 }
203 lines
204 }
205}
206
207pub fn fibonacci_sphere(n: usize) -> Vec<Vec3> {
209 if n == 0 {
210 return vec![];
211 }
212 let golden_ratio = (1.0 + 5.0_f32.sqrt()) / 2.0;
213 let mut points = Vec::with_capacity(n);
214
215 for i in 0..n {
216 let theta = (1.0 - 2.0 * i as f32 / (n as f32 - 1.0).max(1.0)).clamp(-1.0, 1.0).acos();
217 let phi = 2.0 * PI * i as f32 / golden_ratio;
218 let sin_theta = theta.sin();
219 points.push(Vec3::new(
220 sin_theta * phi.cos(),
221 sin_theta * phi.sin(),
222 theta.cos(),
223 ));
224 }
225 points
226}
227
228#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn test_cartesian_roundtrip() {
236 let coord = SphericalCoord::new(1.2, 0.8);
237 let cart = to_cartesian(coord, 1.0);
238 let back = from_cartesian(cart);
239 assert!((back.theta - coord.theta).abs() < 1e-4);
240 assert!((back.phi - coord.phi).abs() < 1e-4);
241 }
242
243 #[test]
244 fn test_north_pole() {
245 let coord = SphericalCoord::new(0.0, 0.0);
246 let cart = to_cartesian(coord, 1.0);
247 assert!((cart.z - 1.0).abs() < 1e-6);
248 assert!(cart.x.abs() < 1e-6);
249 assert!(cart.y.abs() < 1e-6);
250 }
251
252 #[test]
253 fn test_south_pole() {
254 let coord = SphericalCoord::new(PI, 0.0);
255 let cart = to_cartesian(coord, 1.0);
256 assert!((cart.z - (-1.0)).abs() < 1e-5);
257 }
258
259 #[test]
260 fn test_great_circle_distance_same_point() {
261 let a = SphericalCoord::new(0.5, 1.0);
262 let d = great_circle_distance(a, a);
263 assert!(d.abs() < 1e-4);
264 }
265
266 #[test]
267 fn test_great_circle_distance_antipodal() {
268 let a = SphericalCoord::new(0.0, 0.0); let b = SphericalCoord::new(PI, 0.0); let d = great_circle_distance(a, b);
271 assert!((d - PI).abs() < 1e-4, "Antipodal distance should be PI, got {}", d);
272 }
273
274 #[test]
275 fn test_geodesic_endpoints() {
276 let a = SphericalCoord::new(0.5, 0.3);
277 let b = SphericalCoord::new(1.2, 2.0);
278 let path = geodesic(a, b, 50);
279 let pa = to_cartesian(a, 1.0);
280 let pb = to_cartesian(b, 1.0);
281 assert!((path[0] - pa).length() < 1e-4);
282 assert!((path[49] - pb).length() < 1e-4);
283 }
284
285 #[test]
286 fn test_geodesic_on_sphere() {
287 let a = SphericalCoord::new(0.5, 0.0);
288 let b = SphericalCoord::new(1.5, 0.0);
289 let path = geodesic(a, b, 10);
290 for p in &path {
291 assert!((p.length() - 1.0).abs() < 1e-4, "Point not on unit sphere");
292 }
293 }
294
295 #[test]
296 fn test_spherical_triangle_area_octant() {
297 let a = SphericalCoord::new(0.0, 0.0); let b = SphericalCoord::new(PI / 2.0, 0.0); let c = SphericalCoord::new(PI / 2.0, PI / 2.0); let area = spherical_triangle_area(a, b, c);
302 assert!((area - PI / 2.0).abs() < 0.2, "Octant area should be ~PI/2, got {}", area);
304 }
305
306 #[test]
307 fn test_stereographic_roundtrip() {
308 let p3 = Vec3::new(0.5, 0.3, -0.8124).normalize();
309 let p2 = stereographic_projection(p3);
310 let back = inverse_stereographic(p2);
311 assert!((p3 - back).length() < 1e-3, "Stereographic roundtrip: {:?} vs {:?}", p3, back);
312 }
313
314 #[test]
315 fn test_stereographic_south_pole() {
316 let south = Vec3::new(0.0, 0.0, -1.0);
318 let proj = stereographic_projection(south);
319 assert!(proj.length() < 1e-4);
320 }
321
322 #[test]
323 fn test_inverse_stereographic_origin() {
324 let p = inverse_stereographic(Vec2::ZERO);
325 assert!((p - Vec3::new(0.0, 0.0, -1.0)).length() < 1e-4);
326 }
327
328 #[test]
329 fn test_fibonacci_sphere_count() {
330 let points = fibonacci_sphere(100);
331 assert_eq!(points.len(), 100);
332 }
333
334 #[test]
335 fn test_fibonacci_sphere_on_sphere() {
336 let points = fibonacci_sphere(50);
337 for p in &points {
338 assert!((p.length() - 1.0).abs() < 1e-4, "Fibonacci point not on unit sphere");
339 }
340 }
341
342 #[test]
343 fn test_fibonacci_sphere_distribution() {
344 let points = fibonacci_sphere(100);
346 let mut min_dist = f32::MAX;
347 for i in 0..points.len() {
348 for j in (i + 1)..points.len() {
349 let d = (points[i] - points[j]).length();
350 if d < min_dist {
351 min_dist = d;
352 }
353 }
354 }
355 assert!(min_dist > 0.05, "Points too clustered: min_dist = {}", min_dist);
357 }
358
359 #[test]
360 fn test_spherical_grid() {
361 let grid = SphericalGrid::new(10, 20);
362 let points = grid.generate(1.0);
363 assert_eq!(points.len(), 11 * 20);
364 for p in &points {
365 assert!((p.length() - 1.0).abs() < 1e-4);
366 }
367 }
368}