1use glam::Vec3;
4use std::f32::consts::PI;
5
6pub enum GeodesicSurface {
10 Plane,
11 Sphere { radius: f32 },
12 Torus { major_r: f32, minor_r: f32 },
13 Hyperbolic,
14 Custom {
17 surface_fn: Box<dyn Fn(f32, f32) -> Vec3>,
18 },
19}
20
21pub fn geodesic_on_surface(surface: &GeodesicSurface, start: Vec3, end: Vec3, steps: usize) -> Vec<Vec3> {
24 match surface {
25 GeodesicSurface::Plane => geodesic_plane(start, end, steps),
26 GeodesicSurface::Sphere { radius } => shortest_path_sphere_pts(start, end, *radius, steps),
27 GeodesicSurface::Torus { major_r, minor_r } => {
28 shortest_path_torus_pts(start, end, *major_r, *minor_r, steps)
29 }
30 GeodesicSurface::Hyperbolic => geodesic_hyperbolic(start, end, steps),
31 GeodesicSurface::Custom { surface_fn } => geodesic_custom(surface_fn, start, end, steps),
32 }
33}
34
35fn geodesic_plane(start: Vec3, end: Vec3, steps: usize) -> Vec<Vec3> {
36 if steps < 2 {
37 return vec![start, end];
38 }
39 (0..steps)
40 .map(|i| {
41 let t = i as f32 / (steps - 1) as f32;
42 start.lerp(end, t)
43 })
44 .collect()
45}
46
47pub fn shortest_path_sphere(a: Vec3, b: Vec3, steps: usize) -> Vec<Vec3> {
51 let r = a.length();
52 shortest_path_sphere_pts(a, b, r, steps)
53}
54
55fn shortest_path_sphere_pts(a: Vec3, b: Vec3, radius: f32, steps: usize) -> Vec<Vec3> {
56 if steps < 2 {
57 return vec![a, b];
58 }
59 let na = a.normalize();
60 let nb = b.normalize();
61 let dot = na.dot(nb).clamp(-1.0, 1.0);
62 let omega = dot.acos();
63
64 if omega.abs() < 1e-8 {
65 return vec![a; steps];
66 }
67
68 let sin_omega = omega.sin();
69 (0..steps)
70 .map(|i| {
71 let t = i as f32 / (steps - 1) as f32;
72 let a_coeff = ((1.0 - t) * omega).sin() / sin_omega;
73 let b_coeff = (t * omega).sin() / sin_omega;
74 (na * a_coeff + nb * b_coeff) * radius
75 })
76 .collect()
77}
78
79pub fn shortest_path_torus(a: Vec3, b: Vec3, major_r: f32, minor_r: f32, steps: usize) -> Vec<Vec3> {
83 shortest_path_torus_pts(a, b, major_r, minor_r, steps)
84}
85
86fn shortest_path_torus_pts(a: Vec3, b: Vec3, major_r: f32, minor_r: f32, steps: usize) -> Vec<Vec3> {
87 if steps < 2 {
88 return vec![a, b];
89 }
90
91 let (u_a, v_a) = cartesian_to_torus_params(a, major_r, minor_r);
93 let (u_b, v_b) = cartesian_to_torus_params(b, major_r, minor_r);
94
95 let du = shortest_angle_diff(u_a, u_b);
97 let dv = shortest_angle_diff(v_a, v_b);
98
99 (0..steps)
100 .map(|i| {
101 let t = i as f32 / (steps - 1) as f32;
102 let u = u_a + du * t;
103 let v = v_a + dv * t;
104 torus_point(major_r, minor_r, u, v)
105 })
106 .collect()
107}
108
109fn cartesian_to_torus_params(p: Vec3, major_r: f32, _minor_r: f32) -> (f32, f32) {
110 let u = p.y.atan2(p.x);
111 let center_x = major_r * u.cos();
112 let center_y = major_r * u.sin();
113 let dx = p.x - center_x;
114 let dy = p.y - center_y;
115 let dz = p.z;
116 let r_in_plane = (dx * dx + dy * dy).sqrt();
117 let v = dz.atan2(r_in_plane - 0.0); (u, v)
119}
120
121fn torus_point(major_r: f32, minor_r: f32, u: f32, v: f32) -> Vec3 {
122 Vec3::new(
123 (major_r + minor_r * v.cos()) * u.cos(),
124 (major_r + minor_r * v.cos()) * u.sin(),
125 minor_r * v.sin(),
126 )
127}
128
129fn shortest_angle_diff(from: f32, to: f32) -> f32 {
130 let mut diff = to - from;
131 while diff > PI {
132 diff -= 2.0 * PI;
133 }
134 while diff < -PI {
135 diff += 2.0 * PI;
136 }
137 diff
138}
139
140fn geodesic_hyperbolic(start: Vec3, end: Vec3, steps: usize) -> Vec<Vec3> {
143 if steps < 2 {
147 return vec![start, end];
148 }
149
150 let minkowski_dot = |a: Vec3, b: Vec3| -> f32 { -a.x * b.x + a.y * b.y + a.z * b.z };
152
153 let dot = -minkowski_dot(start, end).max(1.0);
154 let dist = acosh(dot);
155
156 if dist.abs() < 1e-8 {
157 return vec![start; steps];
158 }
159
160 let sinh_dist = dist.sinh();
161 (0..steps)
162 .map(|i| {
163 let t = i as f32 / (steps - 1) as f32;
164 let a_coeff = ((1.0 - t) * dist).sinh() / sinh_dist;
165 let b_coeff = (t * dist).sinh() / sinh_dist;
166 start * a_coeff + end * b_coeff
167 })
168 .collect()
169}
170
171fn acosh(x: f32) -> f32 {
172 (x + (x * x - 1.0).max(0.0).sqrt()).ln()
173}
174
175fn geodesic_custom(
178 _surface_fn: &dyn Fn(f32, f32) -> Vec3,
179 start: Vec3,
180 end: Vec3,
181 steps: usize,
182) -> Vec<Vec3> {
183 if steps < 2 {
187 return vec![start, end];
188 }
189
190 let mut path: Vec<Vec3> = (0..steps)
193 .map(|i| {
194 let t = i as f32 / (steps - 1) as f32;
195 start.lerp(end, t)
196 })
197 .collect();
198
199 for _iter in 0..10 {
201 let old = path.clone();
202 for i in 1..(steps - 1) {
203 let mid = (old[i - 1] + old[i + 1]) * 0.5;
204 path[i] = mid;
205 }
206 }
207
208 path
209}
210
211pub fn geodesic_curvature(path: &[Vec3]) -> Vec<f32> {
216 if path.len() < 3 {
217 return vec![];
218 }
219
220 let mut curvatures = Vec::with_capacity(path.len() - 2);
221 for i in 1..(path.len() - 1) {
222 let prev = path[i - 1];
223 let curr = path[i];
224 let next = path[i + 1];
225
226 let d1 = curr - prev;
227 let d2 = next - curr;
228 let l1 = d1.length();
229 let l2 = d2.length();
230
231 if l1 < 1e-10 || l2 < 1e-10 {
232 curvatures.push(0.0);
233 continue;
234 }
235
236 let t1 = d1 / l1;
237 let t2 = d2 / l2;
238 let dt = t2 - t1;
239 let ds = (l1 + l2) / 2.0;
240 let kappa = dt.length() / ds;
241 curvatures.push(kappa);
242 }
243 curvatures
244}
245
246pub fn parallel_transport(vector: Vec3, path: &[Vec3]) -> Vec<Vec3> {
251 if path.is_empty() {
252 return vec![];
253 }
254 if path.len() == 1 {
255 return vec![vector];
256 }
257
258 let mut result = Vec::with_capacity(path.len());
259 let mut v = vector;
260 result.push(v);
261
262 for i in 1..path.len() {
263 let tangent = (path[i] - path[i - 1]).normalize_or_zero();
264 if i >= 2 {
268 let prev_tangent = (path[i - 1] - path[i - 2]).normalize_or_zero();
269 let tangent_change = tangent - prev_tangent;
270 let tc_len_sq = tangent_change.length_squared();
272 if tc_len_sq > 1e-10 {
273 v = v - tangent_change * (v.dot(tangent_change) / tc_len_sq);
274 }
275 }
276
277 let orig_len = vector.length();
279 let curr_len = v.length();
280 if curr_len > 1e-10 {
281 v = v * (orig_len / curr_len);
282 }
283
284 result.push(v);
285 }
286
287 result
288}
289
290#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn test_plane_geodesic_is_straight() {
298 let start = Vec3::new(0.0, 0.0, 0.0);
299 let end = Vec3::new(10.0, 0.0, 0.0);
300 let path = geodesic_on_surface(&GeodesicSurface::Plane, start, end, 11);
301 assert_eq!(path.len(), 11);
302 for (i, p) in path.iter().enumerate() {
303 let expected_x = i as f32;
304 assert!((p.x - expected_x).abs() < 1e-4);
305 assert!(p.y.abs() < 1e-4);
306 assert!(p.z.abs() < 1e-4);
307 }
308 }
309
310 #[test]
311 fn test_sphere_geodesic_on_sphere() {
312 let r = 5.0;
313 let a = Vec3::new(r, 0.0, 0.0);
314 let b = Vec3::new(0.0, r, 0.0);
315 let path = shortest_path_sphere(a, b, 20);
316 for p in &path {
317 assert!((p.length() - r).abs() < 0.01, "Point not on sphere: len={}", p.length());
318 }
319 }
320
321 #[test]
322 fn test_sphere_geodesic_endpoints() {
323 let a = Vec3::new(1.0, 0.0, 0.0);
324 let b = Vec3::new(0.0, 0.0, 1.0);
325 let path = shortest_path_sphere(a, b, 10);
326 assert!((path[0] - a).length() < 1e-4);
327 assert!((path[9] - b).length() < 1e-4);
328 }
329
330 #[test]
331 fn test_torus_geodesic() {
332 let a = torus_point(3.0, 1.0, 0.0, 0.0);
333 let b = torus_point(3.0, 1.0, PI / 2.0, 0.0);
334 let path = shortest_path_torus(a, b, 3.0, 1.0, 20);
335 assert_eq!(path.len(), 20);
336 for p in &path {
338 let xy_dist = (p.x * p.x + p.y * p.y).sqrt();
339 assert!(xy_dist > 1.5 && xy_dist < 4.5, "Point not near torus: {}", xy_dist);
340 }
341 }
342
343 #[test]
344 fn test_geodesic_curvature_straight_line() {
345 let path: Vec<Vec3> = (0..10)
346 .map(|i| Vec3::new(i as f32, 0.0, 0.0))
347 .collect();
348 let curvatures = geodesic_curvature(&path);
349 for k in &curvatures {
350 assert!(k.abs() < 1e-4, "Straight line should have zero curvature, got {}", k);
351 }
352 }
353
354 #[test]
355 fn test_geodesic_curvature_circle() {
356 let n = 100;
357 let path: Vec<Vec3> = (0..n)
358 .map(|i| {
359 let t = 2.0 * PI * i as f32 / n as f32;
360 Vec3::new(t.cos(), t.sin(), 0.0)
361 })
362 .collect();
363 let curvatures = geodesic_curvature(&path);
364 for k in &curvatures {
366 assert!((k - 1.0).abs() < 0.2, "Circle curvature should be ~1, got {}", k);
367 }
368 }
369
370 #[test]
371 fn test_parallel_transport_straight() {
372 let path: Vec<Vec3> = (0..5)
373 .map(|i| Vec3::new(i as f32, 0.0, 0.0))
374 .collect();
375 let v = Vec3::new(0.0, 1.0, 0.0);
376 let transported = parallel_transport(v, &path);
377 assert_eq!(transported.len(), 5);
378 for tv in &transported {
380 assert!((tv.y - 1.0).abs() < 1e-3, "Transport should preserve vector: {:?}", tv);
381 }
382 }
383
384 #[test]
385 fn test_parallel_transport_preserves_length() {
386 let n = 50;
387 let path: Vec<Vec3> = (0..n)
388 .map(|i| {
389 let t = PI * i as f32 / n as f32;
390 Vec3::new(t.cos(), t.sin(), 0.0)
391 })
392 .collect();
393 let v = Vec3::new(0.0, 0.0, 1.0);
394 let transported = parallel_transport(v, &path);
395 let orig_len = v.length();
396 for tv in &transported {
397 assert!((tv.length() - orig_len).abs() < 0.1, "Length not preserved: {}", tv.length());
398 }
399 }
400
401 #[test]
402 fn test_hyperbolic_geodesic() {
403 let a = Vec3::new(1.0, 0.0, 0.0); let b = Vec3::new((1.0 + 1.0_f32).sqrt(), 1.0, 0.0);
406 let path = geodesic_on_surface(&GeodesicSurface::Hyperbolic, a, b, 10);
407 assert_eq!(path.len(), 10);
408 }
409
410 #[test]
411 fn test_shortest_angle_diff() {
412 assert!((shortest_angle_diff(0.1, 0.3) - 0.2).abs() < 1e-4);
413 let d = shortest_angle_diff(3.0, -3.0);
415 assert!(d.abs() < PI + 0.1);
416 }
417}