1use crate::bivector::Bivector;
5use crate::point::Point;
6use nalgebra::{SMatrix, SVector};
7
8#[derive(Clone, Debug)]
17pub struct Rotor<const D: usize> {
18 mat: SMatrix<f64, D, D>,
19}
20
21impl<const D: usize> Rotor<D> {
22 pub fn identity() -> Self {
24 Self {
25 mat: SMatrix::identity(),
26 }
27 }
28
29 pub fn from_plane_angle(plane: &Bivector<D>, angle: f64) -> Self {
39 let norm = plane.norm();
40 if norm < 1e-15 || angle.abs() < 1e-15 {
41 return Self::identity();
42 }
43
44 let b_hat = plane.to_matrix() / norm;
45 let b_hat_sq = b_hat * b_hat;
46 let mat = SMatrix::identity() - b_hat * angle.sin() + b_hat_sq * (1.0 - angle.cos());
47
48 Self { mat }
49 }
50
51 pub fn from_vectors(from: &SVector<f64, D>, to: &SVector<f64, D>) -> Self {
53 let dot = from.dot(to).clamp(-1.0, 1.0);
54 let angle = dot.acos();
55
56 if angle.abs() < 1e-14 {
57 return Self::identity();
58 }
59
60 let mut ext = SMatrix::<f64, D, D>::zeros();
62 for i in 0..D {
63 for j in 0..D {
64 ext[(i, j)] = from[i] * to[j] - to[i] * from[j];
65 }
66 }
67 let plane = Bivector::from_matrix(&ext);
68 Self::from_plane_angle(&plane, angle)
69 }
70
71 pub fn from_matrix(mat: SMatrix<f64, D, D>) -> Self {
73 Self { mat }
74 }
75
76 #[inline]
78 pub fn to_matrix(&self) -> &SMatrix<f64, D, D> {
79 &self.mat
80 }
81
82 #[inline]
84 pub fn reverse(&self) -> Self {
85 Self {
86 mat: self.mat.transpose(),
87 }
88 }
89
90 #[inline]
92 pub fn compose(&self, other: &Self) -> Self {
93 Self {
94 mat: self.mat * other.mat,
95 }
96 }
97
98 #[inline]
100 pub fn rotate_point(&self, point: &Point<D>) -> Point<D> {
101 Point(self.mat * point.0)
102 }
103
104 #[inline]
106 pub fn rotate_vector(&self, v: &SVector<f64, D>) -> SVector<f64, D> {
107 self.mat * v
108 }
109
110 pub fn slerp(&self, t: f64) -> Self {
115 if t <= 1e-14 {
116 return Self::identity();
117 }
118 if (t - 1.0).abs() <= 1e-14 {
119 return self.clone();
120 }
121
122 let antisym = (self.mat - self.mat.transpose()) / 2.0;
124 let plane = Bivector::from_matrix(&(-antisym));
126 let sin_theta = plane.norm();
127
128 if sin_theta < 1e-14 {
129 return Self::identity();
130 }
131
132 let trace = self.mat.trace();
133 let cos_theta = ((trace - (D as f64 - 2.0)) / 2.0).clamp(-1.0, 1.0);
134 let theta = f64::atan2(sin_theta, cos_theta);
135
136 Self::from_plane_angle(&plane, theta * t)
137 }
138}
139
140impl<const D: usize> Default for Rotor<D> {
141 fn default() -> Self {
142 Self::identity()
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use std::f64::consts::{FRAC_PI_2, FRAC_PI_4, PI};
150
151 fn approx_eq(a: f64, b: f64) -> bool {
152 (a - b).abs() < 1e-10
153 }
154
155 fn approx_vec<const N: usize>(a: &SVector<f64, N>, b: &SVector<f64, N>) -> bool {
156 a.iter().zip(b.iter()).all(|(x, y)| (x - y).abs() < 1e-10)
157 }
158
159 #[test]
160 fn identity_preserves_point() {
161 let r = Rotor::<4>::identity();
162 let p = Point::new([1.0, 2.0, 3.0, 4.0]);
163 assert_eq!(r.rotate_point(&p), p);
164 }
165
166 #[test]
167 fn rotation_2d_90() {
168 let plane = Bivector::<2>::unit_plane(0, 1);
169 let r = Rotor::from_plane_angle(&plane, FRAC_PI_2);
170 let p = Point::new([1.0, 0.0]);
171 let q = r.rotate_point(&p);
172 assert!(approx_eq(q.coord(0), 0.0));
173 assert!(approx_eq(q.coord(1), 1.0));
174 }
175
176 #[test]
177 fn rotation_3d_xy_plane() {
178 let plane = Bivector::<3>::unit_plane(0, 1);
179 let r = Rotor::from_plane_angle(&plane, FRAC_PI_2);
180 let p = Point::new([1.0, 0.0, 0.0]);
181 let q = r.rotate_point(&p);
182 assert!(approx_eq(q.coord(0), 0.0));
183 assert!(approx_eq(q.coord(1), 1.0));
184 assert!(approx_eq(q.coord(2), 0.0));
185 }
186
187 #[test]
188 fn preserves_distance() {
189 let plane = Bivector::<4>::unit_plane(1, 3);
190 let r = Rotor::from_plane_angle(&plane, 1.0);
191 let p = Point::new([1.0, 2.0, 3.0, 4.0]);
192 let q = r.rotate_point(&p);
193 assert!(approx_eq(p.0.norm(), q.0.norm()));
194 }
195
196 #[test]
197 fn full_rotation() {
198 let plane = Bivector::<3>::unit_plane(0, 2);
199 let r = Rotor::from_plane_angle(&plane, 2.0 * PI);
200 let p = Point::new([1.0, 2.0, 3.0]);
201 let q = r.rotate_point(&p);
202 assert!(approx_vec(&p.0, &q.0));
203 }
204
205 #[test]
206 fn compose_additive() {
207 let plane = Bivector::<3>::unit_plane(0, 1);
208 let r1 = Rotor::from_plane_angle(&plane, FRAC_PI_4);
209 let r2 = Rotor::from_plane_angle(&plane, FRAC_PI_4);
210 let composed = r1.compose(&r2);
211 let direct = Rotor::from_plane_angle(&plane, FRAC_PI_2);
212
213 let p = Point::new([1.0, 0.0, 0.0]);
214 assert!(approx_vec(
215 &composed.rotate_point(&p).0,
216 &direct.rotate_point(&p).0
217 ));
218 }
219
220 #[test]
221 fn reverse_undoes() {
222 let plane = Bivector::<4>::unit_plane(0, 3);
223 let r = Rotor::from_plane_angle(&plane, 1.7);
224 let p = Point::new([1.0, 2.0, 3.0, 4.0]);
225 let q = r.rotate_point(&p);
226 let back = r.reverse().rotate_point(&q);
227 assert!(approx_vec(&p.0, &back.0));
228 }
229
230 #[test]
231 fn from_vectors_3d() {
232 let from = SVector::from([1.0, 0.0, 0.0]);
233 let to = SVector::from([0.0, 1.0, 0.0]);
234 let r = Rotor::<3>::from_vectors(&from, &to);
235 assert!(approx_vec(&r.rotate_vector(&from), &to));
236 }
237
238 #[test]
239 fn from_vectors_4d() {
240 let from = SVector::from([1.0, 0.0, 0.0, 0.0]);
241 let to = SVector::from([0.0, 0.0, 0.0, 1.0]);
242 let r = Rotor::<4>::from_vectors(&from, &to);
243 assert!(approx_vec(&r.rotate_vector(&from), &to));
244 }
245
246 #[test]
247 fn orthogonal_matrix() {
248 let plane = Bivector::<4>::unit_plane(0, 2);
249 let r = Rotor::from_plane_angle(&plane, 1.23);
250 let mat = r.to_matrix();
251 let product = mat * mat.transpose();
252 let identity = SMatrix::<f64, 4, 4>::identity();
253 for i in 0..4 {
254 for j in 0..4 {
255 assert!(
256 (product[(i, j)] - identity[(i, j)]).abs() < 1e-10,
257 "not orthogonal at ({i},{j})"
258 );
259 }
260 }
261 }
262
263 #[test]
264 fn det_is_one() {
265 let plane = Bivector::<3>::unit_plane(0, 1);
266 let r = Rotor::from_plane_angle(&plane, 0.5);
267 assert!(approx_eq(r.to_matrix().determinant(), 1.0));
268 }
269
270 #[test]
271 fn slerp_endpoints() {
272 let plane = Bivector::<3>::unit_plane(0, 1);
273 let r = Rotor::from_plane_angle(&plane, 1.0);
274 let p = Point::new([1.0, 0.0, 0.0]);
275
276 let at_0 = r.slerp(0.0).rotate_point(&p);
277 assert!(approx_vec(&at_0.0, &p.0));
278
279 let at_1 = r.slerp(1.0).rotate_point(&p);
280 assert!(approx_vec(&at_1.0, &r.rotate_point(&p).0));
281 }
282
283 #[test]
284 fn compose_different_planes() {
285 let r1 = Rotor::from_plane_angle(&Bivector::<4>::unit_plane(0, 1), 0.3);
287 let r2 = Rotor::from_plane_angle(&Bivector::<4>::unit_plane(2, 3), 0.5);
288 let composed = r2.compose(&r1);
289
290 let p = Point::new([1.0, 1.0, 1.0, 1.0]);
291 let sequential = r2.rotate_point(&r1.rotate_point(&p));
292 let via_compose = composed.rotate_point(&p);
293 assert!(approx_vec(&sequential.0, &via_compose.0));
294 }
295
296 #[test]
297 fn slerp_midpoint() {
298 let plane = Bivector::<3>::unit_plane(0, 1);
299 let r = Rotor::from_plane_angle(&plane, FRAC_PI_2);
300 let half = r.slerp(0.5);
301 let expected = Rotor::from_plane_angle(&plane, FRAC_PI_4);
302
303 let p = Point::new([1.0, 0.0, 0.0]);
304 assert!(approx_vec(
305 &half.rotate_point(&p).0,
306 &expected.rotate_point(&p).0
307 ));
308 }
309}