1use crate::conversions::{cartesian_to_spherical, spherical_to_cartesian};
2use crate::distance::angular_distance;
3use crate::types::{CartesianPoint, SphericalPoint};
4
5pub fn slerp(a: &SphericalPoint, b: &SphericalPoint, t: f64) -> SphericalPoint {
26 let t = t.clamp(0.0, 1.0);
27 let a_unit = SphericalPoint::new_unchecked(1.0, a.theta, a.phi);
28 let b_unit = SphericalPoint::new_unchecked(1.0, b.theta, b.phi);
29
30 let omega = angular_distance(&a_unit, &b_unit);
31
32 if omega.abs() < 1e-10 {
33 return a_unit;
34 }
35
36 let ac = spherical_to_cartesian(&a_unit);
37 let bc = spherical_to_cartesian(&b_unit);
38
39 let sin_omega = omega.sin();
40 if sin_omega.abs() < 1e-10 {
41 let basis = if ac.z.abs() < 0.9 {
45 [0.0, 0.0, 1.0]
46 } else {
47 [1.0, 0.0, 0.0]
48 };
49 let proj = basis[0] * ac.x + basis[1] * ac.y + basis[2] * ac.z;
50 let mut ux = basis[0] - proj * ac.x;
51 let mut uy = basis[1] - proj * ac.y;
52 let mut uz = basis[2] - proj * ac.z;
53 let u_norm = (ux * ux + uy * uy + uz * uz).sqrt();
54 ux /= u_norm;
55 uy /= u_norm;
56 uz /= u_norm;
57 let (ca, sa) = ((omega * t).cos(), (omega * t).sin());
58 let result = CartesianPoint::new(
59 ca * ac.x + sa * ux,
60 ca * ac.y + sa * uy,
61 ca * ac.z + sa * uz,
62 );
63 return cartesian_to_spherical(&result);
64 }
65
66 let factor_a = ((1.0 - t) * omega).sin() / sin_omega;
67 let factor_b = (t * omega).sin() / sin_omega;
68
69 let result = CartesianPoint::new(
70 factor_a * ac.x + factor_b * bc.x,
71 factor_a * ac.y + factor_b * bc.y,
72 factor_a * ac.z + factor_b * bc.z,
73 );
74
75 cartesian_to_spherical(&result)
76}
77
78pub fn nlerp(a: &SphericalPoint, b: &SphericalPoint, t: f64) -> SphericalPoint {
86 let t = t.clamp(0.0, 1.0);
87 let a_unit = SphericalPoint::new_unchecked(1.0, a.theta, a.phi);
88 let b_unit = SphericalPoint::new_unchecked(1.0, b.theta, b.phi);
89
90 let ac = spherical_to_cartesian(&a_unit);
91 let bc = spherical_to_cartesian(&b_unit);
92
93 let lerped = CartesianPoint::new(
94 ac.x + t * (bc.x - ac.x),
95 ac.y + t * (bc.y - ac.y),
96 ac.z + t * (bc.z - ac.z),
97 );
98
99 if lerped.magnitude() < f64::EPSILON {
100 return SphericalPoint::new_unchecked(1.0, a.theta, a.phi);
101 }
102
103 cartesian_to_spherical(&lerped.normalize())
104}
105
106pub fn full_slerp(a: &SphericalPoint, b: &SphericalPoint, t: f64) -> SphericalPoint {
112 let t = t.clamp(0.0, 1.0);
113 let direction = slerp(a, b, t);
114 let r = a.r + t * (b.r - a.r);
115 SphericalPoint::new_unchecked(r, direction.theta, direction.phi)
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use approx::assert_relative_eq;
122 use std::f64::consts::FRAC_PI_2;
123
124 fn unit_point(theta: f64, phi: f64) -> SphericalPoint {
125 SphericalPoint::new_unchecked(1.0, theta, phi)
126 }
127
128 #[test]
129 fn slerp_at_t0_returns_a() {
130 let a = unit_point(0.0, FRAC_PI_2);
131 let b = unit_point(FRAC_PI_2, FRAC_PI_2);
132 let result = slerp(&a, &b, 0.0);
133 assert_relative_eq!(result.theta, a.theta, epsilon = 1e-12);
134 assert_relative_eq!(result.phi, a.phi, epsilon = 1e-12);
135 }
136
137 #[test]
138 fn slerp_at_t1_returns_b() {
139 let a = unit_point(0.0, FRAC_PI_2);
140 let b = unit_point(FRAC_PI_2, FRAC_PI_2);
141 let result = slerp(&a, &b, 1.0);
142 assert_relative_eq!(result.theta, b.theta, epsilon = 1e-12);
143 assert_relative_eq!(result.phi, b.phi, epsilon = 1e-12);
144 }
145
146 #[test]
147 fn slerp_midpoint_is_on_great_circle() {
148 let a = unit_point(0.0, FRAC_PI_2);
149 let b = unit_point(FRAC_PI_2, FRAC_PI_2);
150 let mid = slerp(&a, &b, 0.5);
151
152 let dist_a = angular_distance(&unit_point(mid.theta, mid.phi), &a);
153 let dist_b = angular_distance(&unit_point(mid.theta, mid.phi), &b);
154 assert_relative_eq!(dist_a, dist_b, epsilon = 1e-12);
155
156 let total = angular_distance(&a, &b);
157 assert_relative_eq!(dist_a, total / 2.0, epsilon = 1e-12);
158 }
159
160 #[test]
161 fn slerp_clamps_t() {
162 let a = unit_point(0.0, FRAC_PI_2);
163 let b = unit_point(FRAC_PI_2, FRAC_PI_2);
164
165 let below = slerp(&a, &b, -0.5);
166 let at_zero = slerp(&a, &b, 0.0);
167 assert_relative_eq!(below.theta, at_zero.theta, epsilon = 1e-12);
168 assert_relative_eq!(below.phi, at_zero.phi, epsilon = 1e-12);
169
170 let above = slerp(&a, &b, 1.5);
171 let at_one = slerp(&a, &b, 1.0);
172 assert_relative_eq!(above.theta, at_one.theta, epsilon = 1e-12);
173 assert_relative_eq!(above.phi, at_one.phi, epsilon = 1e-12);
174 }
175
176 #[test]
177 fn nlerp_endpoints_match_slerp() {
178 let a = unit_point(0.3, 1.0);
179 let b = unit_point(1.5, 0.8);
180
181 let s0 = slerp(&a, &b, 0.0);
182 let n0 = nlerp(&a, &b, 0.0);
183 assert_relative_eq!(s0.theta, n0.theta, epsilon = 1e-10);
184 assert_relative_eq!(s0.phi, n0.phi, epsilon = 1e-10);
185
186 let s1 = slerp(&a, &b, 1.0);
187 let n1 = nlerp(&a, &b, 1.0);
188 assert_relative_eq!(s1.theta, n1.theta, epsilon = 1e-10);
189 assert_relative_eq!(s1.phi, n1.phi, epsilon = 1e-10);
190 }
191
192 #[test]
193 fn full_slerp_interpolates_radius() {
194 let a = SphericalPoint::new_unchecked(2.0, 0.0, FRAC_PI_2);
195 let b = SphericalPoint::new_unchecked(8.0, FRAC_PI_2, FRAC_PI_2);
196
197 let mid = full_slerp(&a, &b, 0.5);
198 assert_relative_eq!(mid.r, 5.0, epsilon = 1e-12);
199
200 let quarter = full_slerp(&a, &b, 0.25);
201 assert_relative_eq!(quarter.r, 3.5, epsilon = 1e-12);
202 }
203
204 #[test]
205 fn full_slerp_endpoints() {
206 let a = SphericalPoint::new_unchecked(3.0, 0.5, 1.0);
207 let b = SphericalPoint::new_unchecked(7.0, 1.5, 0.8);
208
209 let at_a = full_slerp(&a, &b, 0.0);
210 assert_relative_eq!(at_a.r, a.r, epsilon = 1e-12);
211 assert_relative_eq!(at_a.theta, a.theta, epsilon = 1e-12);
212 assert_relative_eq!(at_a.phi, a.phi, epsilon = 1e-12);
213
214 let at_b = full_slerp(&a, &b, 1.0);
215 assert_relative_eq!(at_b.r, b.r, epsilon = 1e-12);
216 assert_relative_eq!(at_b.theta, b.theta, epsilon = 1e-12);
217 assert_relative_eq!(at_b.phi, b.phi, epsilon = 1e-12);
218 }
219
220 #[test]
221 fn slerp_antipodal_is_finite_and_on_sphere() {
222 let a = unit_point(0.0, FRAC_PI_2);
225 let b = unit_point(std::f64::consts::PI, FRAC_PI_2);
226
227 let mid = slerp(&a, &b, 0.5);
228 assert!(mid.theta.is_finite());
229 assert!(mid.phi.is_finite());
230
231 let half_pi = FRAC_PI_2;
234 let da = angular_distance(&mid, &a);
235 let db = angular_distance(&mid, &b);
236 assert_relative_eq!(da, half_pi, epsilon = 1e-10);
237 assert_relative_eq!(db, half_pi, epsilon = 1e-10);
238 }
239
240 #[test]
241 fn slerp_antipodal_endpoints_match() {
242 let a = unit_point(0.0, FRAC_PI_2);
243 let b = unit_point(std::f64::consts::PI, FRAC_PI_2);
244
245 let at_zero = slerp(&a, &b, 0.0);
246 assert_relative_eq!(at_zero.theta, a.theta, epsilon = 1e-10);
247 assert_relative_eq!(at_zero.phi, a.phi, epsilon = 1e-10);
248 }
249
250 #[test]
251 fn interpolation_between_identical_points() {
252 let p = unit_point(1.0, 0.5);
253
254 let s = slerp(&p, &p, 0.5);
255 assert_relative_eq!(s.theta, p.theta, epsilon = 1e-12);
256 assert_relative_eq!(s.phi, p.phi, epsilon = 1e-12);
257
258 let n = nlerp(&p, &p, 0.5);
259 assert_relative_eq!(n.theta, p.theta, epsilon = 1e-12);
260 assert_relative_eq!(n.phi, p.phi, epsilon = 1e-12);
261
262 let f = full_slerp(&p, &p, 0.5);
263 assert_relative_eq!(f.r, p.r, epsilon = 1e-12);
264 assert_relative_eq!(f.theta, p.theta, epsilon = 1e-12);
265 assert_relative_eq!(f.phi, p.phi, epsilon = 1e-12);
266 }
267}