1use std::ops::{Add, Mul};
6
7use glam::{Vec2, Vec3, Vec4};
8
9pub trait Bivector: Copy + Add<Output = Self> + Mul<f32, Output = Self> {
10 type Rotor: Rotor<Bivector = Self>;
11
12 fn zero() -> Self;
13
14 fn exp(self) -> Self::Rotor;
15}
16
17pub trait Rotor: Copy + Mul<Output = Self> {
19 type Bivector: Bivector<Rotor = Self>;
20
21 type Vector: Copy;
22
23 fn identity() -> Self;
24
25 fn inverse(self) -> Self;
26
27 fn apply(&self, v: Self::Vector) -> Self::Vector;
28
29 fn log(self) -> Self::Bivector;
31}
32
33#[derive(Copy, Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
34pub struct Bivector2(pub f32);
35
36impl Add for Bivector2 {
37 type Output = Self;
38 fn add(self, rhs: Self) -> Self {
39 Self(self.0 + rhs.0)
40 }
41}
42
43impl Mul<f32> for Bivector2 {
44 type Output = Self;
45 fn mul(self, k: f32) -> Self {
46 Self(self.0 * k)
47 }
48}
49
50impl Bivector for Bivector2 {
51 type Rotor = Rotor2;
52
53 fn zero() -> Self {
54 Self(0.0)
55 }
56
57 fn exp(self) -> Rotor2 {
58 let half = self.0 * 0.5;
59 Rotor2 {
60 a: half.cos(),
61 b: half.sin(),
62 }
63 }
64}
65
66#[derive(Copy, Clone, Debug, PartialEq)]
68pub struct Rotor2 {
69 pub a: f32,
70 pub b: f32,
71}
72
73impl Rotor2 {
74 pub const IDENTITY: Self = Self { a: 1.0, b: 0.0 };
75}
76
77impl Default for Rotor2 {
78 fn default() -> Self {
79 Self::IDENTITY
80 }
81}
82
83impl Mul for Rotor2 {
84 type Output = Self;
85 fn mul(self, rhs: Self) -> Self {
86 Self {
87 a: self.a * rhs.a - self.b * rhs.b,
88 b: self.a * rhs.b + self.b * rhs.a,
89 }
90 }
91}
92
93impl Rotor for Rotor2 {
94 type Bivector = Bivector2;
95 type Vector = Vec2;
96
97 fn identity() -> Self {
98 Self::IDENTITY
99 }
100
101 fn inverse(self) -> Self {
102 Self {
103 a: self.a,
104 b: -self.b,
105 }
106 }
107
108 fn apply(&self, v: Vec2) -> Vec2 {
109 let c = self.a * self.a - self.b * self.b;
110 let s = 2.0 * self.a * self.b;
111 Vec2::new(c * v.x - s * v.y, s * v.x + c * v.y)
112 }
113
114 fn log(self) -> Bivector2 {
115 Bivector2(2.0 * self.b.atan2(self.a))
116 }
117}
118
119#[derive(Copy, Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
120pub struct Bivector3 {
121 pub xy: f32,
122 pub yz: f32,
123 pub zx: f32,
124}
125
126impl Bivector3 {
127 pub const ZERO: Self = Self {
128 xy: 0.0,
129 yz: 0.0,
130 zx: 0.0,
131 };
132
133 pub fn new(xy: f32, yz: f32, zx: f32) -> Self {
134 Self { xy, yz, zx }
135 }
136
137 pub fn magnitude(self) -> f32 {
138 (self.xy * self.xy + self.yz * self.yz + self.zx * self.zx).sqrt()
139 }
140}
141
142impl Add for Bivector3 {
143 type Output = Self;
144 fn add(self, rhs: Self) -> Self {
145 Self {
146 xy: self.xy + rhs.xy,
147 yz: self.yz + rhs.yz,
148 zx: self.zx + rhs.zx,
149 }
150 }
151}
152
153impl Mul<f32> for Bivector3 {
154 type Output = Self;
155 fn mul(self, k: f32) -> Self {
156 Self {
157 xy: self.xy * k,
158 yz: self.yz * k,
159 zx: self.zx * k,
160 }
161 }
162}
163
164impl Bivector for Bivector3 {
165 type Rotor = Rotor3;
166
167 fn zero() -> Self {
168 Self::ZERO
169 }
170
171 fn exp(self) -> Rotor3 {
172 let mag_sq = self.xy * self.xy + self.yz * self.yz + self.zx * self.zx;
173
174 if mag_sq < 1e-16 {
175 return Rotor3 {
176 s: 1.0,
177 xy: self.xy * 0.5,
178 yz: self.yz * 0.5,
179 zx: self.zx * 0.5,
180 };
181 }
182 let mag = mag_sq.sqrt();
183 let half = mag * 0.5;
184 let c = half.cos();
185 let k = half.sin() / mag;
186 Rotor3 {
187 s: c,
188 xy: self.xy * k,
189 yz: self.yz * k,
190 zx: self.zx * k,
191 }
192 }
193}
194
195#[derive(Copy, Clone, Debug, PartialEq)]
196pub struct Rotor3 {
197 pub s: f32,
198 pub xy: f32,
199 pub yz: f32,
200 pub zx: f32,
201}
202
203impl Rotor3 {
204 pub const IDENTITY: Self = Self {
205 s: 1.0,
206 xy: 0.0,
207 yz: 0.0,
208 zx: 0.0,
209 };
210}
211
212impl Default for Rotor3 {
213 fn default() -> Self {
214 Self::IDENTITY
215 }
216}
217
218impl Mul for Rotor3 {
219 type Output = Self;
220 fn mul(self, rhs: Self) -> Self {
221 let (s1, a1, b1, c1) = (self.s, self.xy, self.yz, self.zx);
222 let (s2, a2, b2, c2) = (rhs.s, rhs.xy, rhs.yz, rhs.zx);
223 Self {
224 s: s1 * s2 - a1 * a2 - b1 * b2 - c1 * c2,
225 xy: s1 * a2 + s2 * a1 - b1 * c2 + c1 * b2,
226 yz: s1 * b2 + s2 * b1 + a1 * c2 - c1 * a2,
227 zx: s1 * c2 + s2 * c1 - a1 * b2 + b1 * a2,
228 }
229 }
230}
231
232impl Rotor for Rotor3 {
233 type Bivector = Bivector3;
234 type Vector = Vec3;
235
236 fn identity() -> Self {
237 Self::IDENTITY
238 }
239
240 fn inverse(self) -> Self {
241 Self {
242 s: self.s,
243 xy: -self.xy,
244 yz: -self.yz,
245 zx: -self.zx,
246 }
247 }
248
249 fn apply(&self, v: Vec3) -> Vec3 {
250 let (s, a, b, c) = (self.s, self.xy, self.yz, self.zx);
251 let (vx, vy, vz) = (v.x, v.y, v.z);
252
253 let p1 = s * vx - a * vy + c * vz;
254 let p2 = s * vy + a * vx - b * vz;
255 let p3 = s * vz + b * vy - c * vx;
256 let pt = -(a * vz + b * vx + c * vy);
257
258 Vec3::new(
259 p1 * s - p2 * a + p3 * c - pt * b,
260 p2 * s + p1 * a - p3 * b - pt * c,
261 p3 * s + p2 * b - p1 * c - pt * a,
262 )
263 }
264
265 fn log(self) -> Bivector3 {
266 let mag_sq = self.xy * self.xy + self.yz * self.yz + self.zx * self.zx;
267 if mag_sq < 1e-16 {
268 return Bivector3 {
269 xy: self.xy * 2.0,
270 yz: self.yz * 2.0,
271 zx: self.zx * 2.0,
272 };
273 }
274 let mag = mag_sq.sqrt();
275 let theta = 2.0 * mag.atan2(self.s);
276 let k = theta / mag;
277 Bivector3 {
278 xy: self.xy * k,
279 yz: self.yz * k,
280 zx: self.zx * k,
281 }
282 }
283}
284
285#[derive(Copy, Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
286pub struct Bivector4 {
287 pub xy: f32,
288 pub xz: f32,
289 pub xw: f32,
290 pub yz: f32,
291 pub yw: f32,
292 pub zw: f32,
293}
294
295impl Bivector4 {
296 pub const ZERO: Self = Self {
297 xy: 0.0,
298 xz: 0.0,
299 xw: 0.0,
300 yz: 0.0,
301 yw: 0.0,
302 zw: 0.0,
303 };
304
305 pub fn new(xy: f32, xz: f32, xw: f32, yz: f32, yw: f32, zw: f32) -> Self {
306 Self {
307 xy,
308 xz,
309 xw,
310 yz,
311 yw,
312 zw,
313 }
314 }
315
316 pub fn basis(i: usize) -> Self {
318 let mut c = [0.0_f32; 6];
319 c[i] = 1.0;
320 Self::new(c[0], c[1], c[2], c[3], c[4], c[5])
321 }
322
323 pub fn magnitude_squared(self) -> f32 {
324 self.xy * self.xy
325 + self.xz * self.xz
326 + self.xw * self.xw
327 + self.yz * self.yz
328 + self.yw * self.yw
329 + self.zw * self.zw
330 }
331
332 pub fn magnitude(self) -> f32 {
334 self.magnitude_squared().sqrt()
335 }
336
337 pub fn component(self, plane: Plane4) -> f32 {
338 match plane {
339 Plane4::Xy => self.xy,
340 Plane4::Xz => self.xz,
341 Plane4::Xw => self.xw,
342 Plane4::Yz => self.yz,
343 Plane4::Yw => self.yw,
344 Plane4::Zw => self.zw,
345 }
346 }
347
348 pub fn set_component(&mut self, plane: Plane4, value: f32) {
349 match plane {
350 Plane4::Xy => self.xy = value,
351 Plane4::Xz => self.xz = value,
352 Plane4::Xw => self.xw = value,
353 Plane4::Yz => self.yz = value,
354 Plane4::Yw => self.yw = value,
355 Plane4::Zw => self.zw = value,
356 }
357 }
358
359 pub fn dot(self, other: Self) -> f32 {
361 self.xy * other.xy
362 + self.xz * other.xz
363 + self.xw * other.xw
364 + self.yz * other.yz
365 + self.yw * other.yw
366 + self.zw * other.zw
367 }
368
369 pub fn wedge_self_coeff(self) -> f32 {
371 2.0 * (self.xy * self.zw - self.xz * self.yw + self.xw * self.yz)
372 }
373
374 pub fn wedge(u: Vec4, v: Vec4) -> Self {
375 Self {
376 xy: u.x * v.y - u.y * v.x,
377 xz: u.x * v.z - u.z * v.x,
378 xw: u.x * v.w - u.w * v.x,
379 yz: u.y * v.z - u.z * v.y,
380 yw: u.y * v.w - u.w * v.y,
381 zw: u.z * v.w - u.w * v.z,
382 }
383 }
384
385 pub fn contract_vec(self, v: Vec4) -> Vec4 {
387 Vec4::new(
388 self.xy * v.y + self.xz * v.z + self.xw * v.w,
389 -self.xy * v.x + self.yz * v.z + self.yw * v.w,
390 -self.xz * v.x - self.yz * v.y + self.zw * v.w,
391 -self.xw * v.x - self.yw * v.y - self.zw * v.z,
392 )
393 }
394
395 pub fn dual(self) -> Self {
396 Self {
397 xy: -self.zw,
398 xz: self.yw,
399 xw: -self.yz,
400 yz: -self.xw,
401 yw: self.xz,
402 zw: -self.xy,
403 }
404 }
405}
406
407#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
408#[repr(usize)]
409pub enum Plane4 {
410 Xy = 0,
411 Xz = 1,
412 Xw = 2,
413 Yz = 3,
414 Yw = 4,
415 Zw = 5,
416}
417
418impl Plane4 {
419 pub const ALL: [Self; 6] = [Self::Xy, Self::Xz, Self::Xw, Self::Yz, Self::Yw, Self::Zw];
420
421 pub fn label(self) -> &'static str {
423 match self {
424 Self::Xy => "xy",
425 Self::Xz => "xz",
426 Self::Xw => "xw",
427 Self::Yz => "yz",
428 Self::Yw => "yw",
429 Self::Zw => "zw",
430 }
431 }
432
433 pub fn unit_bivector(self) -> Bivector4 {
434 Bivector4::basis(self as usize)
435 }
436}
437
438impl Add for Bivector4 {
439 type Output = Self;
440 fn add(self, rhs: Self) -> Self {
441 Self {
442 xy: self.xy + rhs.xy,
443 xz: self.xz + rhs.xz,
444 xw: self.xw + rhs.xw,
445 yz: self.yz + rhs.yz,
446 yw: self.yw + rhs.yw,
447 zw: self.zw + rhs.zw,
448 }
449 }
450}
451
452impl Mul<f32> for Bivector4 {
453 type Output = Self;
454 fn mul(self, k: f32) -> Self {
455 Self {
456 xy: self.xy * k,
457 xz: self.xz * k,
458 xw: self.xw * k,
459 yz: self.yz * k,
460 yw: self.yw * k,
461 zw: self.zw * k,
462 }
463 }
464}
465
466impl Bivector for Bivector4 {
467 type Rotor = Rotor4;
468
469 fn zero() -> Self {
470 Self::ZERO
471 }
472
473 fn exp(self) -> Rotor4 {
474 let s = self.magnitude_squared();
475 if s < 1e-16 {
476 return Rotor4::IDENTITY;
477 }
478 let delta = self.wedge_self_coeff();
479
480 if delta.abs() < 1e-6 * s.max(1.0) {
481 let mag = s.sqrt();
482 let half = mag * 0.5;
483 let c = half.cos();
484 let k = half.sin() / mag;
485 return Rotor4 {
486 s: c,
487 xy: self.xy * k,
488 xz: self.xz * k,
489 xw: self.xw * k,
490 yz: self.yz * k,
491 yw: self.yw * k,
492 zw: self.zw * k,
493 xyzw: 0.0,
494 };
495 }
496
497 let disc_sq = (s * s - delta * delta).max(0.0);
498 let disc = disc_sq.sqrt();
499
500 if disc < 1e-6 * s.max(1.0) {
501 let theta_sq = s * 0.5;
502 let theta = theta_sq.sqrt();
503 let half = theta * 0.5;
504 let ch = half.cos();
505 let sh = half.sin();
506 let sign_i = delta.signum();
507
508 let b_coef = if theta > 1e-8 {
509 (theta.sin()) / (2.0 * theta)
510 } else {
511 0.5
512 };
513 return Rotor4 {
514 s: ch * ch,
515 xy: self.xy * b_coef,
516 xz: self.xz * b_coef,
517 xw: self.xw * b_coef,
518 yz: self.yz * b_coef,
519 yw: self.yw * b_coef,
520 zw: self.zw * b_coef,
521 xyzw: sh * sh * sign_i,
522 };
523 }
524
525 let t1 = ((s + disc) * 0.5).max(0.0).sqrt();
527 let t2 = delta / (2.0 * t1);
528
529 let half1 = t1 * 0.5;
530 let half2 = t2 * 0.5;
531 let c1 = half1.cos();
532 let s1 = half1.sin();
533 let c2 = half2.cos();
534 let s2 = half2.sin();
535
536 let s1_t1 = s1 / t1;
537 let s2_t2 = s2 / t2;
538
539 let b_coef = (s1_t1 * c2 * (s + disc) - c1 * s2_t2 * (s - disc)) / (2.0 * disc);
540 let bstar_coef = delta * (s1_t1 * c2 - c1 * s2_t2) / (2.0 * disc);
541
542 let dual = self.dual();
543 Rotor4 {
544 s: c1 * c2,
545 xy: self.xy * b_coef + dual.xy * bstar_coef,
546 xz: self.xz * b_coef + dual.xz * bstar_coef,
547 xw: self.xw * b_coef + dual.xw * bstar_coef,
548 yz: self.yz * b_coef + dual.yz * bstar_coef,
549 yw: self.yw * b_coef + dual.yw * bstar_coef,
550 zw: self.zw * b_coef + dual.zw * bstar_coef,
551 xyzw: s1 * s2,
552 }
553 }
554}
555
556#[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
557pub struct Rotor4 {
558 pub s: f32,
559 pub xy: f32,
560 pub xz: f32,
561 pub xw: f32,
562 pub yz: f32,
563 pub yw: f32,
564 pub zw: f32,
565 pub xyzw: f32,
566}
567
568impl Rotor4 {
569 pub fn from_rotation_arc(from: Vec4, to: Vec4) -> Self {
571 let plane = Bivector4::wedge(from, to);
572 let magnitude = plane.magnitude();
573 if magnitude < ARC_PLANE_EPSILON {
574 if from.dot(to) > 0.0 {
575 return Self::IDENTITY;
576 }
577 let axes = [Vec4::X, Vec4::Y, Vec4::Z, Vec4::W];
578 let least = (1..4).fold(0, |best, i| {
579 if from[i].abs() < from[best].abs() {
580 i
581 } else {
582 best
583 }
584 });
585 let axis = (axes[least] - from * from[least]).normalize();
586 return (Bivector4::wedge(from, axis) * std::f32::consts::PI)
587 .exp()
588 .normalize();
589 }
590 let angle = 2.0 * (to - from).length().atan2((to + from).length());
592 (plane * (angle / magnitude)).exp().normalize()
593 }
594
595 pub const IDENTITY: Self = Self {
596 s: 1.0,
597 xy: 0.0,
598 xz: 0.0,
599 xw: 0.0,
600 yz: 0.0,
601 yw: 0.0,
602 zw: 0.0,
603 xyzw: 0.0,
604 };
605
606 pub const IDENTITY_SLOT: [f32; 8] = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
608
609 pub fn norm_squared(self) -> f32 {
610 self.s * self.s
611 + self.xy * self.xy
612 + self.xz * self.xz
613 + self.xw * self.xw
614 + self.yz * self.yz
615 + self.yw * self.yw
616 + self.zw * self.zw
617 + self.xyzw * self.xyzw
618 }
619
620 pub fn normalize(self) -> Self {
622 let n = self.norm_squared().sqrt();
623 if n > 0.0 {
624 let k = 1.0 / n;
625 Self {
626 s: self.s * k,
627 xy: self.xy * k,
628 xz: self.xz * k,
629 xw: self.xw * k,
630 yz: self.yz * k,
631 yw: self.yw * k,
632 zw: self.zw * k,
633 xyzw: self.xyzw * k,
634 }
635 } else {
636 Self::IDENTITY
637 }
638 }
639}
640
641impl Default for Rotor4 {
642 fn default() -> Self {
643 Self::IDENTITY
644 }
645}
646
647impl From<Rotor4> for [f32; 8] {
649 fn from(r: Rotor4) -> Self {
650 [r.s, r.xy, r.xz, r.xw, r.yz, r.yw, r.zw, r.xyzw]
651 }
652}
653
654impl Rotor4 {
655 pub fn to_mat4(&self) -> [[f32; 4]; 4] {
658 let c0 = <Self as Rotor>::apply(self, Vec4::new(1.0, 0.0, 0.0, 0.0));
659 let c1 = <Self as Rotor>::apply(self, Vec4::new(0.0, 1.0, 0.0, 0.0));
660 let c2 = <Self as Rotor>::apply(self, Vec4::new(0.0, 0.0, 1.0, 0.0));
661 let c3 = <Self as Rotor>::apply(self, Vec4::new(0.0, 0.0, 0.0, 1.0));
662 [c0.to_array(), c1.to_array(), c2.to_array(), c3.to_array()]
663 }
664
665 pub fn from_mat4(columns: &[[f32; 4]; 4]) -> Option<Self> {
667 let target = columns.map(Vec4::from_array);
668 let axes = [Vec4::X, Vec4::Y, Vec4::Z];
669 let mut rotor = Self::IDENTITY;
670 for (axis, basis) in axes.into_iter().enumerate() {
671 let from = <Self as Rotor>::apply(&rotor, basis);
672 rotor = rotor * arc_holding(from, target[axis], &target[..axis]);
673 }
674 rotor
675 .to_mat4()
676 .iter()
677 .flatten()
678 .zip(columns.iter().flatten())
679 .all(|(built, given)| (built - given).abs() <= ROTATION_TOLERANCE)
680 .then_some(rotor)
681 }
682}
683
684const ARC_PLANE_EPSILON: f32 = 1e-6;
685const ROTATION_TOLERANCE: f32 = 1e-3;
686
687fn arc_holding(from: Vec4, to: Vec4, held: &[Vec4]) -> Rotor4 {
688 if Bivector4::wedge(from, to).magnitude() >= ARC_PLANE_EPSILON || from.dot(to) > 0.0 {
689 return Rotor4::from_rotation_arc(from, to);
690 }
691 let mut free = Vec4::ZERO;
692 for axis in [Vec4::X, Vec4::Y, Vec4::Z, Vec4::W] {
693 let mut residual = axis - from * axis.dot(from);
694 for fixed in held {
695 residual -= *fixed * residual.dot(*fixed);
696 }
697 if residual.length_squared() > free.length_squared() {
698 free = residual;
699 }
700 }
701 (Bivector4::wedge(from, free.normalize()) * std::f32::consts::PI).exp()
702}
703
704impl Mul for Rotor4 {
705 type Output = Self;
706 fn mul(self, rhs: Self) -> Self {
707 let (a0, a12, a13, a14, a23, a24, a34, a_i) = (
708 self.s, self.xy, self.xz, self.xw, self.yz, self.yw, self.zw, self.xyzw,
709 );
710 let (b0, b12, b13, b14, b23, b24, b34, b_i) = (
711 rhs.s, rhs.xy, rhs.xz, rhs.xw, rhs.yz, rhs.yw, rhs.zw, rhs.xyzw,
712 );
713
714 let s = a0 * b0 - a12 * b12 - a13 * b13 - a14 * b14 - a23 * b23 - a24 * b24 - a34 * b34
715 + a_i * b_i;
716
717 let xy = a0 * b12 + a12 * b0 - a13 * b23 + a23 * b13 - a14 * b24 + a24 * b14
718 - a34 * b_i
719 - a_i * b34;
720 let xz = a0 * b13 + a13 * b0 + a12 * b23 - a14 * b34 - a23 * b12
721 + a34 * b14
722 + a24 * b_i
723 + a_i * b24;
724 let xw = a0 * b14 + a14 * b0 + a12 * b24 + a13 * b34
725 - a24 * b12
726 - a34 * b13
727 - a23 * b_i
728 - a_i * b23;
729 let yz = a0 * b23 + a23 * b0 - a12 * b13 + a13 * b12 - a24 * b34 + a34 * b24
730 - a14 * b_i
731 - a_i * b14;
732 let yw = a0 * b24 + a24 * b0 - a12 * b14 + a14 * b12 + a23 * b34 - a34 * b23
733 + a13 * b_i
734 + a_i * b13;
735 let zw = a0 * b34 + a34 * b0 - a13 * b14 + a14 * b13 - a23 * b24 + a24 * b23
736 - a12 * b_i
737 - a_i * b12;
738
739 let xyzw = a0 * b_i + a_i * b0 + a12 * b34 + a34 * b12 - a13 * b24 - a24 * b13
740 + a14 * b23
741 + a23 * b14;
742
743 Self {
744 s,
745 xy,
746 xz,
747 xw,
748 yz,
749 yw,
750 zw,
751 xyzw,
752 }
753 }
754}
755
756impl Rotor for Rotor4 {
757 type Bivector = Bivector4;
758 type Vector = Vec4;
759
760 fn identity() -> Self {
761 Self::IDENTITY
762 }
763
764 fn inverse(self) -> Self {
765 Self {
766 s: self.s,
767 xy: -self.xy,
768 xz: -self.xz,
769 xw: -self.xw,
770 yz: -self.yz,
771 yw: -self.yw,
772 zw: -self.zw,
773 xyzw: self.xyzw,
774 }
775 }
776
777 fn apply(&self, v: Vec4) -> Vec4 {
778 let (vx, vy, vz, vw) = (v.x, v.y, v.z, v.w);
779 let rs = self.s;
780 let rxy = self.xy;
781 let rxz = self.xz;
782 let rxw = self.xw;
783 let ryz = self.yz;
784 let ryw = self.yw;
785 let rzw = self.zw;
786 let r_i = self.xyzw;
787
788 let p1 = rs * vx - rxy * vy - rxz * vz - rxw * vw;
789 let p2 = rs * vy + rxy * vx - ryz * vz - ryw * vw;
790 let p3 = rs * vz + rxz * vx + ryz * vy - rzw * vw;
791 let p4 = rs * vw + rxw * vx + ryw * vy + rzw * vz;
792
793 let t123 = -rxy * vz + rxz * vy - ryz * vx + r_i * vw;
794 let t124 = -rxy * vw + rxw * vy - ryw * vx - r_i * vz;
795 let t134 = -rxz * vw + rxw * vz - rzw * vx + r_i * vy;
796 let t234 = -ryz * vw + ryw * vz - rzw * vy - r_i * vx;
797
798 let q1 = rs * p1 - rxy * p2 - rxz * p3 - rxw * p4 - ryz * t123 - ryw * t124 - rzw * t134
799 + r_i * t234;
800 let q2 = rs * p2 + rxy * p1 - ryz * p3 - ryw * p4 + rxz * t123 + rxw * t124
801 - rzw * t234
802 - r_i * t134;
803 let q3 = rs * p3 + rxz * p1 + ryz * p2 - rzw * p4 - rxy * t123
804 + rxw * t134
805 + ryw * t234
806 + r_i * t124;
807 let q4 = rs * p4 + rxw * p1 + ryw * p2 + rzw * p3
808 - rxy * t124
809 - rxz * t134
810 - ryz * t234
811 - r_i * t123;
812
813 Vec4::new(q1, q2, q3, q4)
814 }
815
816 fn log(self) -> Bivector4 {
817 let branch = if self.s < 0.0 { -1.0 } else { 1.0 };
819 let c = branch * self.s;
820 let p = branch * self.xyzw;
821 let sum_part = Vec3::new(self.xy + self.zw, self.xz - self.yw, self.xw + self.yz) * branch;
822 let diff_part = Vec3::new(self.xy - self.zw, self.xz + self.yw, self.xw - self.yz) * branch;
823 let sin_sum = sum_part.length();
824 let sin_diff = diff_part.length();
825
826 if sum_part == Vec3::ZERO && diff_part == Vec3::ZERO && p.abs() > c {
827 let half_turn = std::f32::consts::PI;
828 return Bivector4::new(half_turn, 0.0, 0.0, 0.0, 0.0, half_turn * p.signum());
829 }
830
831 let k_sum = if sin_sum > 0.0 {
832 sin_sum.atan2(c - p) / sin_sum
833 } else {
834 1.0
835 };
836 let k_diff = if sin_diff > 0.0 {
837 sin_diff.atan2(c + p) / sin_diff
838 } else {
839 1.0
840 };
841
842 Bivector4 {
843 xy: k_sum * sum_part.x + k_diff * diff_part.x,
844 xz: k_sum * sum_part.y + k_diff * diff_part.y,
845 xw: k_sum * sum_part.z + k_diff * diff_part.z,
846 yz: k_sum * sum_part.z - k_diff * diff_part.z,
847 yw: k_diff * diff_part.y - k_sum * sum_part.y,
848 zw: k_sum * sum_part.x - k_diff * diff_part.x,
849 }
850 }
851}
852
853#[cfg(test)]
854mod tests {
855 use super::*;
856 use std::f32::consts::{FRAC_PI_2, PI, SQRT_2, TAU};
857
858 fn assert_close(a: f32, b: f32) {
859 assert!(
860 (a - b).abs() <= 1e-5,
861 "expected {a} close to {b} (diff {})",
862 (a - b).abs()
863 );
864 }
865
866 fn assert_vec2_close(a: Vec2, b: Vec2) {
867 assert!(
868 (a - b).length() <= 1e-5,
869 "expected {a:?} close to {b:?} (diff {})",
870 (a - b).length()
871 );
872 }
873
874 #[test]
875 fn zero_bivector_exp_is_identity() {
876 let r = Bivector2::zero().exp();
877 assert_eq!(r, Rotor2::identity());
878 }
879
880 #[test]
881 fn quarter_turn_rotates_x_to_y() {
882 let r = Bivector2(FRAC_PI_2).exp();
883 assert_vec2_close(r.apply(Vec2::X), Vec2::Y);
884 assert_vec2_close(r.apply(Vec2::Y), -Vec2::X);
885 }
886
887 #[test]
888 fn half_turn_negates() {
889 let r = Bivector2(PI).exp();
890 assert_vec2_close(r.apply(Vec2::new(1.0, 2.0)), Vec2::new(-1.0, -2.0));
891 }
892
893 #[test]
894 fn full_turn_is_identity_up_to_sign() {
895 let r = Bivector2(TAU).exp();
896 assert_vec2_close(r.apply(Vec2::X), Vec2::X);
897 }
898
899 #[test]
900 fn composition_adds_angles() {
901 let a = Bivector2(0.3).exp();
902 let b = Bivector2(0.5).exp();
903 let composed = a * b;
904 let direct = Bivector2(0.8).exp();
905 assert_close(composed.a, direct.a);
906 assert_close(composed.b, direct.b);
907 }
908
909 #[test]
910 fn inverse_cancels() {
911 let r = Bivector2(1.234).exp();
912 let id = r * r.inverse();
913 assert_close(id.a, 1.0);
914 assert_close(id.b, 0.0);
915 }
916
917 #[test]
918 fn rotation_arc_handles_parallel_and_antipodal_vectors() {
919 let cases = [
920 (Vec4::X, Vec4::Y),
921 (Vec4::W, -Vec4::Y),
922 (Vec4::Y, Vec4::Y),
923 (Vec4::Y, -Vec4::Y),
924 (Vec4::new(0.5, 0.5, 0.5, 0.5), -Vec4::Y),
925 (
926 Vec4::new(0.5, -0.5, 0.5, -0.5),
927 Vec4::new(1.0, 0.0, 0.0, 0.0),
928 ),
929 ];
930 for (from, to) in cases {
931 let turned = Rotor4::from_rotation_arc(from, to).apply(from);
932 assert!(
933 (turned - to).length() < 1e-5,
934 "{from} turned to {turned}, not {to}"
935 );
936 }
937 }
938
939 #[test]
940 fn rotor4_to_mat4_agrees_with_apply() {
941 let b = Bivector4 {
942 xy: 0.7,
943 xz: 0.0,
944 xw: 0.0,
945 yz: 0.0,
946 yw: 0.0,
947 zw: 0.3,
948 };
949 let r: Rotor4 = b.exp();
950 let m = r.to_mat4();
951
952 let e1 = Vec4::new(1.0, 0.0, 0.0, 0.0);
953 let img1 = <Rotor4 as Rotor>::apply(&r, e1);
954 assert_close(m[0][0], img1.x);
955 assert_close(m[0][1], img1.y);
956 assert_close(m[0][2], img1.z);
957 assert_close(m[0][3], img1.w);
958
959 let v = Vec4::new(0.6, -0.4, 0.2, 0.9);
960 let row = |k: usize| m[0][k] * v.x + m[1][k] * v.y + m[2][k] * v.z + m[3][k] * v.w;
961 let by_matrix = Vec4::new(row(0), row(1), row(2), row(3));
962 let by_rotor = <Rotor4 as Rotor>::apply(&r, v);
963 assert!(
964 (by_matrix - by_rotor).length() < 1e-5,
965 "matrix application {by_matrix:?} should match rotor application {by_rotor:?}"
966 );
967 }
968
969 #[test]
970 fn rotor4_from_mat4_rebuilds_the_frame_including_half_turns() {
971 let quarter = std::f32::consts::FRAC_PI_2;
972 let half = std::f32::consts::PI;
973 for rotor in [
974 Rotor4::IDENTITY,
975 Bivector4::new(0.7, 0.0, 0.0, 0.0, 0.0, 0.3).exp(),
976 Bivector4::new(quarter, 0.0, 0.0, 0.0, 0.0, quarter).exp(),
977 Bivector4::new(0.0, 0.0, 0.0, half, 0.0, 0.0).exp(),
978 Bivector4::new(0.0, 0.0, 0.0, 0.0, 0.0, half).exp(),
979 ] {
980 let frame = rotor.to_mat4();
981 let rebuilt = Rotor4::from_mat4(&frame)
982 .unwrap_or_else(|| panic!("{rotor:?} gave a frame no rotor rebuilds"))
983 .to_mat4();
984 for (slot, (built, given)) in rebuilt
985 .iter()
986 .flatten()
987 .zip(frame.iter().flatten())
988 .enumerate()
989 {
990 assert!(
991 (built - given).abs() < 1e-5,
992 "{rotor:?} slot {slot}: rebuilt {built} against {given}"
993 );
994 }
995 }
996 }
997
998 #[test]
999 fn log_is_inverse_of_exp() {
1000 for &theta in &[-1.0_f32, -0.1, 0.0, 0.1, 1.0, 3.0] {
1001 let back = Bivector2(theta).exp().log();
1002 assert_close(back.0, theta);
1003 }
1004 }
1005
1006 fn assert_vec3_close(a: Vec3, b: Vec3) {
1007 assert!((a - b).length() <= 1e-5, "expected {a:?} close to {b:?}");
1008 }
1009
1010 #[test]
1011 fn bivector3_zero_exp_is_identity() {
1012 let r = Bivector3::ZERO.exp();
1013 assert_eq!(r, Rotor3::IDENTITY);
1014 }
1015
1016 #[test]
1017 fn rotor3_identity_leaves_vector_unchanged() {
1018 let v = Vec3::new(1.2, -3.4, 0.5);
1019 assert_vec3_close(Rotor3::IDENTITY.apply(v), v);
1020 }
1021
1022 #[test]
1023 fn bivector3_xy_quarter_turn_sends_x_to_y() {
1024 let r = Bivector3::new(FRAC_PI_2, 0.0, 0.0).exp();
1025 assert_vec3_close(r.apply(Vec3::X), Vec3::Y);
1026 assert_vec3_close(r.apply(Vec3::Y), -Vec3::X);
1027 assert_vec3_close(r.apply(Vec3::Z), Vec3::Z);
1028 }
1029
1030 #[test]
1031 fn bivector3_yz_rotation() {
1032 let r = Bivector3::new(0.0, FRAC_PI_2, 0.0).exp();
1033 assert_vec3_close(r.apply(Vec3::Y), Vec3::Z);
1034 assert_vec3_close(r.apply(Vec3::Z), -Vec3::Y);
1035 assert_vec3_close(r.apply(Vec3::X), Vec3::X);
1036 }
1037
1038 #[test]
1039 fn bivector3_zx_rotation() {
1040 let r = Bivector3::new(0.0, 0.0, FRAC_PI_2).exp();
1041 assert_vec3_close(r.apply(Vec3::Z), Vec3::X);
1042 assert_vec3_close(r.apply(Vec3::X), -Vec3::Z);
1043 assert_vec3_close(r.apply(Vec3::Y), Vec3::Y);
1044 }
1045
1046 #[test]
1047 fn rotor3_full_turn_is_identity_on_vectors() {
1048 let r = Bivector3::new(TAU, 0.0, 0.0).exp();
1049 assert_vec3_close(r.apply(Vec3::new(1.0, 2.0, 3.0)), Vec3::new(1.0, 2.0, 3.0));
1050 }
1051
1052 #[test]
1053 fn rotor3_inverse_cancels() {
1054 let r = Bivector3::new(0.7, -0.3, 0.5).exp();
1055 let id = r * r.inverse();
1056 assert_close(id.s, 1.0);
1057 assert_close(id.xy, 0.0);
1058 assert_close(id.yz, 0.0);
1059 assert_close(id.zx, 0.0);
1060 }
1061
1062 #[test]
1063 fn rotor3_composition_matches_sequential_apply() {
1064 let ra = Bivector3::new(0.4, 0.0, 0.0).exp();
1065 let rb = Bivector3::new(0.0, 0.5, 0.0).exp();
1066 let composed = ra * rb;
1067 let v = Vec3::new(0.7, -0.3, 1.1);
1068 assert_vec3_close(composed.apply(v), rb.apply(ra.apply(v)));
1069 }
1070
1071 #[test]
1072 fn rotor3_preserves_length() {
1073 let r = Bivector3::new(0.3, 0.4, 0.5).exp();
1074 for &v in &[Vec3::X, Vec3::Y, Vec3::Z, Vec3::new(1.0, 2.0, -3.0)] {
1075 assert!(
1076 (r.apply(v).length() - v.length()).abs() < 1e-5,
1077 "length not preserved: input {v}, output length {}",
1078 r.apply(v).length()
1079 );
1080 }
1081 }
1082
1083 #[test]
1084 fn rotor3_log_is_inverse_of_exp() {
1085 for bv in [
1086 Bivector3::new(0.0, 0.0, 0.0),
1087 Bivector3::new(0.1, 0.0, 0.0),
1088 Bivector3::new(0.0, 0.6, 0.0),
1089 Bivector3::new(-0.3, 0.4, 0.5),
1090 Bivector3::new(1.0, -0.5, 0.2),
1091 ] {
1092 let back = bv.exp().log();
1093 assert!(
1094 (back.xy - bv.xy).abs() < 1e-5
1095 && (back.yz - bv.yz).abs() < 1e-5
1096 && (back.zx - bv.zx).abs() < 1e-5,
1097 "log∘exp mismatch: {bv:?} -> {back:?}"
1098 );
1099 }
1100 }
1101
1102 #[test]
1103 fn rotor3_matches_glam_quat_for_axis_rotation() {
1104 use glam::Quat;
1105
1106 let theta = 0.7;
1107 let v = Vec3::new(1.0, 2.0, 3.0).normalize();
1108
1109 let rotor = Bivector3::new(theta, 0.0, 0.0).exp();
1110 let quat = Quat::from_axis_angle(Vec3::Z, theta);
1111 assert_vec3_close(rotor.apply(v), quat * v);
1112
1113 let rotor = Bivector3::new(0.0, theta, 0.0).exp();
1114 let quat = Quat::from_axis_angle(Vec3::X, theta);
1115 assert_vec3_close(rotor.apply(v), quat * v);
1116
1117 let rotor = Bivector3::new(0.0, 0.0, theta).exp();
1118 let quat = Quat::from_axis_angle(Vec3::Y, theta);
1119 assert_vec3_close(rotor.apply(v), quat * v);
1120 }
1121
1122 fn assert_vec4_close(a: Vec4, b: Vec4) {
1123 assert!(
1124 (a - b).length() <= 1e-4,
1125 "expected {a:?} close to {b:?} (diff {})",
1126 (a - b).length()
1127 );
1128 }
1129
1130 fn assert_vec4_close_tol(a: Vec4, b: Vec4, tol: f32) {
1131 assert!(
1132 (a - b).length() <= tol,
1133 "expected {a:?} close to {b:?} (diff {})",
1134 (a - b).length()
1135 );
1136 }
1137
1138 #[test]
1139 fn bivector4_zero_exp_is_identity() {
1140 let r = Bivector4::ZERO.exp();
1141 assert_eq!(r, Rotor4::IDENTITY);
1142 }
1143
1144 #[test]
1145 fn bivector4_dot_inner_product_invariants() {
1146 let xy = Plane4::Xy.unit_bivector();
1147 let zw = Plane4::Zw.unit_bivector();
1148 let xw = Plane4::Xw.unit_bivector();
1149 assert_eq!(xy.dot(xy), 1.0);
1150 assert_eq!(xy.dot(zw), 0.0);
1151 assert_eq!(xy.dot(xw), 0.0);
1152 let a = Bivector4::new(1.0, 0.5, -0.25, 0.75, -1.0, 2.0);
1153 let b = xy * 3.0;
1154 let c = xw * 4.0;
1155 assert!((a.dot(b + c) - (a.dot(b) + a.dot(c))).abs() < 1e-6);
1156 }
1157
1158 #[test]
1159 fn rotor4_identity_leaves_vector_unchanged() {
1160 let v = Vec4::new(1.2, -3.4, 0.5, 0.7);
1161 assert_vec4_close(Rotor4::IDENTITY.apply(v), v);
1162 }
1163
1164 #[test]
1165 fn bivector4_single_plane_rotations_are_planar() {
1166 let r = Bivector4::new(FRAC_PI_2, 0.0, 0.0, 0.0, 0.0, 0.0).exp();
1167 assert_vec4_close(r.apply(Vec4::X), Vec4::Y);
1168 assert_vec4_close(r.apply(Vec4::Y), -Vec4::X);
1169 assert_vec4_close(r.apply(Vec4::Z), Vec4::Z);
1170 assert_vec4_close(r.apply(Vec4::W), Vec4::W);
1171
1172 let r = Bivector4::new(0.0, FRAC_PI_2, 0.0, 0.0, 0.0, 0.0).exp();
1173 assert_vec4_close(r.apply(Vec4::X), Vec4::Z);
1174 assert_vec4_close(r.apply(Vec4::Z), -Vec4::X);
1175 assert_vec4_close(r.apply(Vec4::Y), Vec4::Y);
1176 assert_vec4_close(r.apply(Vec4::W), Vec4::W);
1177
1178 let r = Bivector4::new(0.0, 0.0, FRAC_PI_2, 0.0, 0.0, 0.0).exp();
1179 assert_vec4_close(r.apply(Vec4::X), Vec4::W);
1180 assert_vec4_close(r.apply(Vec4::W), -Vec4::X);
1181
1182 let r = Bivector4::new(0.0, 0.0, 0.0, FRAC_PI_2, 0.0, 0.0).exp();
1183 assert_vec4_close(r.apply(Vec4::Y), Vec4::Z);
1184 assert_vec4_close(r.apply(Vec4::Z), -Vec4::Y);
1185
1186 let r = Bivector4::new(0.0, 0.0, 0.0, 0.0, FRAC_PI_2, 0.0).exp();
1187 assert_vec4_close(r.apply(Vec4::Y), Vec4::W);
1188 assert_vec4_close(r.apply(Vec4::W), -Vec4::Y);
1189
1190 let r = Bivector4::new(0.0, 0.0, 0.0, 0.0, 0.0, FRAC_PI_2).exp();
1191 assert_vec4_close(r.apply(Vec4::Z), Vec4::W);
1192 assert_vec4_close(r.apply(Vec4::W), -Vec4::Z);
1193 }
1194
1195 #[test]
1196 fn bivector4_double_rotation_xy_plus_zw() {
1197 let theta = FRAC_PI_2;
1198 let r = Bivector4::new(theta, 0.0, 0.0, 0.0, 0.0, theta).exp();
1199 assert_vec4_close(r.apply(Vec4::X), Vec4::Y);
1200 assert_vec4_close(r.apply(Vec4::Y), -Vec4::X);
1201 assert_vec4_close(r.apply(Vec4::Z), Vec4::W);
1202 assert_vec4_close(r.apply(Vec4::W), -Vec4::Z);
1203
1204 assert_close(r.xyzw, 0.5);
1205 }
1206
1207 #[test]
1208 fn rotor4_scalar_and_pseudoscalar_carry_the_two_invariant_half_angles() {
1209 for (t1, t2) in [(0.7, 0.0), (1.1, 0.4), (0.9, 0.9)] {
1210 let r = Bivector4::new(t1, 0.0, 0.0, 0.0, 0.0, t2).exp();
1211 assert_close(r.s, (t1 * 0.5).cos() * (t2 * 0.5).cos());
1212 assert_close(r.xyzw, (t1 * 0.5).sin() * (t2 * 0.5).sin());
1213 }
1214
1215 for b in [
1216 Bivector4::new(0.0, 0.0, 0.0, 0.0, 0.0, 1.3),
1217 Bivector4::new(0.0, 0.6, 0.0, 0.0, 0.0, 0.0),
1218 ] {
1219 assert_close(b.exp().xyzw, 0.0);
1220 }
1221 }
1222
1223 #[test]
1224 fn rotor4_full_turn_is_identity_on_vectors() {
1225 let r = Bivector4::new(TAU, 0.0, 0.0, 0.0, 0.0, 0.0).exp();
1226 assert_vec4_close_tol(
1227 r.apply(Vec4::new(1.0, 2.0, 3.0, 4.0)),
1228 Vec4::new(1.0, 2.0, 3.0, 4.0),
1229 3e-4,
1230 );
1231 }
1232
1233 #[test]
1234 fn rotor4_inverse_cancels() {
1235 let r = Bivector4::new(0.3, -0.2, 0.4, 0.1, -0.5, 0.25).exp();
1236 let id = r * r.inverse();
1237 assert_close(id.s, 1.0);
1238 assert_close(id.xy, 0.0);
1239 assert_close(id.xz, 0.0);
1240 assert_close(id.xw, 0.0);
1241 assert_close(id.yz, 0.0);
1242 assert_close(id.yw, 0.0);
1243 assert_close(id.zw, 0.0);
1244 assert_close(id.xyzw, 0.0);
1245 }
1246
1247 #[test]
1248 fn rotor4_is_unit_norm() {
1249 for b in [
1250 Bivector4::new(0.1, 0.0, 0.0, 0.0, 0.0, 0.0),
1251 Bivector4::new(0.5, 0.3, -0.4, 0.0, 0.0, 0.0),
1252 Bivector4::new(0.7, 0.0, 0.0, 0.0, 0.0, 0.5),
1253 Bivector4::new(0.3, -0.2, 0.4, 0.1, -0.5, 0.25),
1254 ] {
1255 let r = b.exp();
1256 let n = r.norm_squared();
1257 assert!(
1258 (n - 1.0).abs() < 1e-4,
1259 "rotor not unit-norm: |R|² = {n} for B = {b:?}"
1260 );
1261 }
1262 }
1263
1264 #[test]
1265 fn rotor4_preserves_length() {
1266 let r = Bivector4::new(0.3, -0.2, 0.4, 0.1, -0.5, 0.25).exp();
1267 for v in [
1268 Vec4::X,
1269 Vec4::Y,
1270 Vec4::Z,
1271 Vec4::W,
1272 Vec4::new(1.0, 2.0, 3.0, 4.0),
1273 Vec4::new(-0.5, 0.5, -0.5, 0.5),
1274 ] {
1275 let rotated = r.apply(v);
1276 assert!(
1277 (rotated.length() - v.length()).abs() < 1e-3,
1278 "length drift: {v:?} -> {rotated:?}"
1279 );
1280 }
1281 }
1282
1283 #[test]
1284 fn rotor4_composition_matches_sequential_apply() {
1285 let ra = Bivector4::new(0.4, 0.0, 0.0, 0.0, 0.0, 0.0).exp();
1286 let rb = Bivector4::new(0.0, 0.0, 0.0, 0.5, 0.0, 0.0).exp();
1287 let composed = ra * rb;
1288 let v = Vec4::new(0.7, -0.3, 1.1, 0.2);
1289 assert_vec4_close(composed.apply(v), rb.apply(ra.apply(v)));
1290 }
1291
1292 fn invariant_plane_pairs() -> [(Bivector4, Bivector4); 4] {
1293 let root_half = 0.5_f32.sqrt();
1294 let u1 = Vec4::new(root_half, root_half, 0.0, 0.0);
1295 let v1 = Vec4::new(0.0, 0.0, root_half, root_half);
1296 let u2 = Vec4::new(root_half, -root_half, 0.0, 0.0);
1297 let v2 = Vec4::new(0.0, 0.0, root_half, -root_half);
1298 let [oblique, orthogonal] = nondegenerate_plane_pairs();
1299 [
1300 (Bivector4::basis(0), Bivector4::basis(5)),
1301 (Bivector4::wedge(u1, v1), Bivector4::wedge(v2, u2)),
1302 oblique,
1303 orthogonal,
1304 ]
1305 }
1306
1307 fn plane_pair_from_eigenparts(sum: Vec3, diff: Vec3) -> (Bivector4, Bivector4) {
1308 let plane = |d: Vec3| Bivector4 {
1309 xy: 0.5 * (sum.x + d.x),
1310 xz: 0.5 * (sum.y + d.y),
1311 xw: 0.5 * (sum.z + d.z),
1312 yz: 0.5 * (sum.z - d.z),
1313 yw: 0.5 * (d.y - sum.y),
1314 zw: 0.5 * (sum.x - d.x),
1315 };
1316 (plane(diff), plane(-diff))
1317 }
1318
1319 fn nondegenerate_plane_pairs() -> [(Bivector4, Bivector4); 2] {
1320 let sum = Vec3::new(2.0, 3.0, 6.0) / 7.0;
1321 let oblique_diff = Vec3::new(9.0, 2.0, 6.0) / 11.0;
1322 let orthogonal_diff = Vec3::new(3.0, -6.0, 2.0) / 7.0;
1323 [
1324 plane_pair_from_eigenparts(sum, oblique_diff),
1325 plane_pair_from_eigenparts(sum, orthogonal_diff),
1326 ]
1327 }
1328
1329 const HALF_ANGLE_STRESS_PAIRS: [(f32, f32); 10] = [
1330 (1.0e-3, 1.0e-3),
1331 (1.0e-3, 5.0e-4),
1332 (1.0e-3, -1.0e-3),
1333 (1.0e-3, 0.0),
1334 (0.7, 0.7),
1335 (0.7, 0.6999),
1336 (1.0, 0.999),
1337 (2.0, 1.999),
1338 (1.2, 0.3),
1339 (0.5, -0.4),
1340 ];
1341
1342 #[test]
1343 fn rotor4_log_recovers_both_invariant_angles() {
1344 for (p1, p2) in invariant_plane_pairs() {
1345 for (t1, t2) in HALF_ANGLE_STRESS_PAIRS {
1346 let b = p1 * t1 + p2 * t2;
1347 let back = ((p1 * t1).exp() * (p2 * t2).exp()).log();
1348 let diff = (back + b * (-1.0)).magnitude();
1349 assert!(
1350 diff <= 1e-6,
1351 "log mismatch at ({t1}, {t2}): {b:?} -> {back:?}"
1352 );
1353 }
1354 }
1355 }
1356
1357 #[test]
1358 fn rotor4_log_of_exp_round_trips_at_small_and_near_equal_angles() {
1359 for (p1, p2) in invariant_plane_pairs() {
1360 for (t1, t2) in HALF_ANGLE_STRESS_PAIRS {
1361 let b = p1 * t1 + p2 * t2;
1362 let back = b.exp().log();
1363 let diff = (back + b * (-1.0)).magnitude();
1364 assert!(
1365 diff <= 2e-5 * b.magnitude(),
1366 "log∘exp mismatch at ({t1}, {t2}): {b:?} -> {back:?}"
1367 );
1368 }
1369 }
1370 }
1371
1372 #[test]
1373 fn rotor4_log_of_a_simple_turn_takes_the_short_way_round() {
1374 let long_way = 1.9 * PI;
1375 let logged = Bivector4::new(long_way, 0.0, 0.0, 0.0, 0.0, 0.0)
1376 .exp()
1377 .log();
1378 assert_close(logged.xy, long_way - TAU);
1379 assert_close(logged.magnitude(), TAU - long_way);
1380
1381 const STEPS: i32 = 47;
1382 for step in -STEPS..=STEPS {
1383 let theta = step as f32 * (TAU / STEPS as f32);
1384 let short = theta - TAU * (theta / TAU).round();
1385 let back = Bivector4::new(0.0, 0.0, 0.0, theta, 0.0, 0.0).exp().log();
1386 assert!(
1387 (back.yz - short).abs() <= 1e-5 && back.magnitude() <= PI,
1388 "a turn of {theta} logged as {back:?}, not {short}"
1389 );
1390 }
1391 }
1392
1393 struct Xorshift(u32);
1394
1395 impl Xorshift {
1396 fn next_u32(&mut self) -> u32 {
1397 self.0 ^= self.0 << 13;
1398 self.0 ^= self.0 >> 17;
1399 self.0 ^= self.0 << 5;
1400 self.0
1401 }
1402
1403 fn signed(&mut self, span: f32) -> f32 {
1404 let unit = self.next_u32() as f32 / u32::MAX as f32;
1405 (unit * 2.0 - 1.0) * span
1406 }
1407
1408 fn bivector4(&mut self, span: f32) -> Bivector4 {
1409 Bivector4::new(
1410 self.signed(span),
1411 self.signed(span),
1412 self.signed(span),
1413 self.signed(span),
1414 self.signed(span),
1415 self.signed(span),
1416 )
1417 }
1418 }
1419
1420 #[test]
1421 fn rotor4_log_is_the_shorter_of_the_two_representatives() {
1422 const SAMPLES: usize = 100_000;
1423 let probes = [Vec4::X, Vec4::W, Vec4::new(1.0, -0.5, 0.3, 0.7)];
1424 let mut rng = Xorshift(0x5eed_4104);
1425 for _ in 0..SAMPLES {
1426 let rotor = rng.bivector4(TAU).exp();
1427 let back = rotor.log();
1428 let sum = Vec3::new(back.xy + back.zw, back.xz - back.yw, back.xw + back.yz);
1429 let diff = Vec3::new(back.xy - back.zw, back.xz + back.yw, back.xw - back.yz);
1430
1431 assert!(
1432 sum.length() + diff.length() <= 2.0 * PI + 1e-5,
1433 "long way round: {rotor:?} -> {back:?}"
1434 );
1435 assert!(
1436 back.magnitude() <= PI * SQRT_2 + 1e-5,
1437 "past the SO(4) diameter: {rotor:?} -> {back:?}"
1438 );
1439 let regenerated = back.exp();
1440 for v in probes {
1441 assert_vec4_close_tol(regenerated.apply(v), rotor.apply(v), 1e-3);
1442 }
1443 }
1444 }
1445
1446 #[test]
1447 fn rotor4_exp_survives_a_double_rotation_with_one_tiny_angle() {
1448 for (t1, t2) in [(3.0_f32, 1.0e-4_f32), (1.0, 1.0e-5), (0.5, -1.0e-4)] {
1449 let b = Bivector4::new(t1, 0.0, 0.0, 0.0, 0.0, t2);
1450 let rotor = b.exp();
1451 assert_close(rotor.norm_squared(), 1.0);
1452 let back = rotor.log();
1453 assert!(
1454 (back + b * (-1.0)).magnitude() <= 1e-6,
1455 "({t1}, {t2}) round-tripped as {back:?}"
1456 );
1457 }
1458 }
1459
1460 #[test]
1461 fn isoclinic_half_turn_log_preserves_action() {
1462 for sign in [-1.0, 1.0] {
1463 let pseudoscalar = Rotor4 {
1464 s: 0.0,
1465 xy: 0.0,
1466 xz: 0.0,
1467 xw: 0.0,
1468 yz: 0.0,
1469 yw: 0.0,
1470 zw: 0.0,
1471 xyzw: sign,
1472 };
1473 let recovered = pseudoscalar.log().exp();
1474 for v in [
1475 Vec4::X,
1476 Vec4::Y,
1477 Vec4::Z,
1478 Vec4::W,
1479 Vec4::new(1.0, -0.5, 0.3, 0.7),
1480 ] {
1481 assert_vec4_close_tol(pseudoscalar.apply(v), -v, 1e-6);
1482 assert_vec4_close_tol(recovered.apply(v), -v, 1e-6);
1483 }
1484 }
1485
1486 let near = Bivector4::new(0.99 * PI, 0.0, 0.0, 0.0, 0.0, 0.99 * PI);
1487 let back = near.exp().log();
1488 assert!(back.magnitude() <= PI * SQRT_2);
1489 assert!((back + near * (-1.0)).magnitude() <= 1e-3, "{back:?}");
1490 }
1491
1492 #[test]
1493 fn rotor4_xy_matches_mat4_rotation_z_on_xy_subspace() {
1494 use glam::Mat4;
1495 let theta = 0.7;
1496 let rotor = Bivector4::new(theta, 0.0, 0.0, 0.0, 0.0, 0.0).exp();
1497 let mat = Mat4::from_rotation_z(theta);
1498
1499 for v in [
1500 Vec4::new(1.0, 0.0, 0.0, 0.0),
1501 Vec4::new(0.0, 1.0, 0.0, 0.0),
1502 Vec4::new(0.6, -0.8, 0.0, 0.0),
1503 ] {
1504 let via_rotor = rotor.apply(v);
1505 let via_mat = mat * v;
1506 assert_vec4_close_tol(via_rotor, via_mat, 1e-4);
1507 }
1508 }
1509
1510 #[test]
1511 fn rotor4_normalize_produces_unit() {
1512 let r = Bivector4::new(0.3, 0.1, -0.2, 0.4, 0.1, 0.0).exp();
1513 let perturbed = Rotor4 {
1514 s: r.s * 1.01,
1515 xy: r.xy * 1.01,
1516 xz: r.xz * 1.01,
1517 xw: r.xw * 1.01,
1518 yz: r.yz * 1.01,
1519 yw: r.yw * 1.01,
1520 zw: r.zw * 1.01,
1521 xyzw: r.xyzw * 1.01,
1522 };
1523 let back = perturbed.normalize();
1524 assert_close(back.norm_squared(), 1.0);
1525 }
1526
1527 #[test]
1528 fn rotor4_compound_xy_xz_xw_yz_integrated_matches_closed_form() {
1529 let omega = Bivector4::new(1.0, 1.0, 1.0, 1.0, 0.0, 0.0);
1530 let dt = 1.0 / 60.0;
1531 let n_steps = 60_u32;
1532 let total_angle = dt * n_steps as f32;
1533 let delta = (omega * dt).exp();
1534
1535 let mut integrated = Rotor4::IDENTITY;
1536 for _ in 0..n_steps {
1537 integrated = delta * integrated;
1538 }
1539 let closed_form = (omega * total_angle).exp();
1540
1541 for v in [
1542 Vec4::X,
1543 Vec4::Y,
1544 Vec4::Z,
1545 Vec4::W,
1546 Vec4::new(1.0, 2.0, 3.0, 4.0),
1547 Vec4::new(-0.5, 0.5, -0.5, 0.5),
1548 ] {
1549 let via_integrated = integrated.apply(v);
1550 let via_closed = closed_form.apply(v);
1551 let diff = (via_integrated - via_closed).length();
1552 assert!(
1553 diff < 1e-5,
1554 "compound xy+xz+xw+yz integration drift: v={v:?} \
1555 integrated={via_integrated:?} closed={via_closed:?} \
1556 diff={diff}",
1557 );
1558 }
1559 }
1560
1561 #[test]
1562 fn rotor4_compound_integration_preserves_unit_norm_over_900_steps() {
1563 let omega = Bivector4::new(1.0, 1.0, 1.0, 1.0, 0.0, 0.0);
1564 let dt = 1.0 / 60.0;
1565 let delta = (omega * dt).exp();
1566 let mut r = Rotor4::IDENTITY;
1567 for _ in 0..900 {
1568 r = delta * r;
1569 }
1570 let n2 = r.norm_squared();
1571
1572 assert!(
1573 (n2 - 1.0).abs() < 1e-5,
1574 "rotor norm drifted after 900 compositions: |R|² = {n2}",
1575 );
1576 }
1577
1578 #[test]
1579 fn bivector4_contract_vec_is_clifford_left_contraction() {
1580 let b = Bivector4::new(1.0, 0.0, 0.0, 0.0, 0.0, 0.0);
1581 assert_vec4_close_tol(b.contract_vec(Vec4::X), -Vec4::Y, 1e-6);
1582 assert_vec4_close_tol(b.contract_vec(Vec4::Y), Vec4::X, 1e-6);
1583 let b = Bivector4::new(0.0, 0.0, 0.0, 0.0, 0.0, 1.0);
1584 assert_vec4_close_tol(b.contract_vec(Vec4::Z), -Vec4::W, 1e-6);
1585 assert_vec4_close_tol(b.contract_vec(Vec4::W), Vec4::Z, 1e-6);
1586 let b = Bivector4::new(1.0, 0.0, 0.0, 0.0, 0.0, 0.0);
1587 assert_vec4_close_tol(b.contract_vec(Vec4::Z), Vec4::ZERO, 1e-6);
1588 assert_vec4_close_tol(b.contract_vec(Vec4::W), Vec4::ZERO, 1e-6);
1589 }
1590}