Skip to main content

cliffy_core/
transforms.rs

1//! Geometric transformations for state manipulation
2//!
3//! This module provides explicit geometric transformation types:
4//! - `Rotor`: Represents rotations in geometric algebra
5//! - `Versor`: General geometric transformations (rotations + reflections)
6//! - `Motor`: Translations combined with rotations (for CGA)
7//!
8//! These allow users to apply explicit geometric operations to state,
9//! rather than hiding all geometry behind scalar updates.
10
11use crate::geometric::GA3;
12use amari_core::{Bivector, Rotor as AmariRotor, Vector};
13
14/// A rotor represents a rotation in geometric algebra.
15///
16/// Rotors are even-grade multivectors with unit magnitude that
17/// implement rotations via the sandwich product: R v R†
18///
19/// In 3D, a rotor encodes rotation by angle θ around an axis n as:
20/// R = cos(θ/2) + sin(θ/2) * B
21/// where B is the unit bivector representing the rotation plane.
22#[derive(Clone, Debug)]
23pub struct Rotor {
24    /// The internal multivector representation
25    inner: GA3,
26}
27
28impl Rotor {
29    /// Create a rotor from a multivector (assumes it's already a valid rotor)
30    pub fn from_multivector(mv: GA3) -> Self {
31        Self { inner: mv }
32    }
33
34    /// Create the identity rotor (no rotation)
35    pub fn identity() -> Self {
36        Self {
37            inner: GA3::scalar(1.0),
38        }
39    }
40
41    /// Create a rotor for rotation by angle (in radians) in the XY plane
42    ///
43    /// This is equivalent to rotation around the Z axis.
44    pub fn xy(angle: f64) -> Self {
45        Self::from_bivector_angle(angle, 1.0, 0.0, 0.0)
46    }
47
48    /// Create a rotor for rotation by angle (in radians) in the XZ plane
49    ///
50    /// This is equivalent to rotation around the Y axis.
51    pub fn xz(angle: f64) -> Self {
52        Self::from_bivector_angle(angle, 0.0, 1.0, 0.0)
53    }
54
55    /// Create a rotor for rotation by angle (in radians) in the YZ plane
56    ///
57    /// This is equivalent to rotation around the X axis.
58    pub fn yz(angle: f64) -> Self {
59        Self::from_bivector_angle(angle, 0.0, 0.0, 1.0)
60    }
61
62    /// Create a rotor from an angle and bivector components
63    ///
64    /// The bivector (xy, xz, yz) defines the rotation plane.
65    /// Components are normalized internally.
66    pub fn from_bivector_angle(angle: f64, xy: f64, xz: f64, yz: f64) -> Self {
67        let half_angle = angle / 2.0;
68
69        // Create unit bivector
70        // Note: Negate to match standard right-hand rotation convention
71        let biv = Bivector::<3, 0, 0>::from_components(-xy, -xz, -yz);
72        let biv_mv = GA3::from_bivector(&biv);
73        let mag = biv_mv.magnitude();
74
75        if mag < 1e-10 {
76            // Degenerate case - return identity
77            return Self::identity();
78        }
79
80        let biv_unit = &biv_mv * (1.0 / mag);
81
82        // R = cos(θ/2) + sin(θ/2) * B
83        let cos_part = GA3::scalar(half_angle.cos());
84        let sin_part = &biv_unit * half_angle.sin();
85
86        Self {
87            inner: &cos_part + &sin_part,
88        }
89    }
90
91    /// Create a rotor for rotation around an axis vector by an angle
92    ///
93    /// The axis does not need to be normalized.
94    pub fn from_axis_angle(axis_x: f64, axis_y: f64, axis_z: f64, angle: f64) -> Self {
95        // The bivector for rotation around axis (x,y,z) is proportional to
96        // x*(e2^e3) + y*(e3^e1) + z*(e1^e2)
97        // which in our basis order (e12, e13, e23) is:
98        // xy = z, xz = -y, yz = x
99        Self::from_bivector_angle(angle, axis_z, -axis_y, axis_x)
100    }
101
102    /// Get the internal multivector
103    pub fn as_multivector(&self) -> &GA3 {
104        &self.inner
105    }
106
107    /// Apply this rotor to transform a multivector (sandwich product)
108    ///
109    /// Returns R * v * R†
110    pub fn transform(&self, v: &GA3) -> GA3 {
111        let rev = self.inner.reverse();
112        self.inner.geometric_product(v).geometric_product(&rev)
113    }
114
115    /// Compose two rotors: self followed by other
116    ///
117    /// The result applies self first, then other.
118    pub fn then(&self, other: &Rotor) -> Rotor {
119        Rotor {
120            inner: other.inner.geometric_product(&self.inner),
121        }
122    }
123
124    /// Get the inverse rotor (reverse rotation)
125    pub fn inverse(&self) -> Rotor {
126        // For unit rotors, inverse = reverse
127        Rotor {
128            inner: self.inner.reverse(),
129        }
130    }
131
132    /// Normalize the rotor to unit magnitude
133    pub fn normalize(&self) -> Rotor {
134        match self.inner.normalize() {
135            Some(normalized) => Rotor { inner: normalized },
136            None => Self::identity(),
137        }
138    }
139
140    /// Get the rotation angle (in radians)
141    pub fn angle(&self) -> f64 {
142        // For a rotor R = cos(θ/2) + sin(θ/2)*B
143        // The scalar part is cos(θ/2)
144        let scalar = self.inner.get(0);
145        2.0 * scalar.clamp(-1.0, 1.0).acos()
146    }
147
148    /// Convert to an amari-core `Rotor<3,0,0>`
149    ///
150    /// This enables interop with amari-core's typed rotor operations
151    /// (slerp, compose, logarithm, power, etc.)
152    pub fn to_amari_rotor(&self) -> AmariRotor<3, 0, 0> {
153        // Extract and normalize the bivector plane
154        let e12 = self.inner.get(3);
155        let e13 = self.inner.get(5);
156        let e23 = self.inner.get(6);
157        let biv_mag = (e12 * e12 + e13 * e13 + e23 * e23).sqrt();
158
159        if biv_mag < 1e-10 {
160            // Identity or near-identity rotor
161            return AmariRotor::identity();
162        }
163
164        // Create unit bivector (negate to match amari convention;
165        // cliffy negates bivector components in from_bivector_angle)
166        let biv =
167            Bivector::<3, 0, 0>::from_components(-e12 / biv_mag, -e13 / biv_mag, -e23 / biv_mag);
168        AmariRotor::from_bivector(&biv, self.angle())
169    }
170
171    /// Create from an amari-core `Rotor<3,0,0>`
172    pub fn from_amari_rotor(rotor: &AmariRotor<3, 0, 0>) -> Self {
173        Self {
174            inner: rotor.as_multivector().clone(),
175        }
176    }
177
178    /// Spherical linear interpolation between identity and this rotor
179    ///
180    /// t=0 gives identity, t=1 gives self
181    pub fn slerp(&self, t: f64) -> Rotor {
182        let angle = self.angle();
183        let new_angle = angle * t;
184
185        // Extract the bivector part and renormalize
186        // Note: Internal representation has negated bivector, so negate when extracting
187        let biv_xy = -self.inner.get(3); // e12
188        let biv_xz = -self.inner.get(5); // e13
189        let biv_yz = -self.inner.get(6); // e23
190        let biv_mag = (biv_xy * biv_xy + biv_xz * biv_xz + biv_yz * biv_yz).sqrt();
191
192        if biv_mag < 1e-10 {
193            // No rotation - return identity
194            Self::identity()
195        } else {
196            Self::from_bivector_angle(
197                new_angle,
198                biv_xy / biv_mag,
199                biv_xz / biv_mag,
200                biv_yz / biv_mag,
201            )
202        }
203    }
204
205    /// Spherical linear interpolation between two rotors
206    pub fn slerp_to(&self, other: &Rotor, t: f64) -> Rotor {
207        // Compute relative rotation: other = relative * self
208        // So relative = other * self.inverse()
209        let relative = other.then(&self.inverse());
210        let interpolated = relative.slerp(t);
211        interpolated.then(self)
212    }
213}
214
215/// A versor is a general geometric transformation (rotation, reflection, or their composition).
216///
217/// Every versor can be written as a product of vectors.
218/// Versors with an even number of vectors are rotors.
219/// Versors with an odd number of vectors include reflections.
220#[derive(Clone, Debug)]
221pub struct Versor {
222    /// The internal multivector representation
223    inner: GA3,
224    /// Whether this is an even versor (rotor) or odd versor (includes reflection)
225    is_even: bool,
226}
227
228impl Versor {
229    /// Create a versor from a multivector
230    pub fn from_multivector(mv: GA3, is_even: bool) -> Self {
231        Self { inner: mv, is_even }
232    }
233
234    /// Create the identity versor
235    pub fn identity() -> Self {
236        Self {
237            inner: GA3::scalar(1.0),
238            is_even: true,
239        }
240    }
241
242    /// Create a reflection through a plane with normal vector (x, y, z)
243    ///
244    /// Reflects points through the plane passing through origin with the given normal.
245    pub fn reflection(normal_x: f64, normal_y: f64, normal_z: f64) -> Self {
246        // A reflection is represented by the unit vector normal to the plane
247        let vec = Vector::<3, 0, 0>::from_components(normal_x, normal_y, normal_z);
248        let mv = GA3::from_vector(&vec);
249        let normalized = mv
250            .normalize()
251            .unwrap_or_else(|| GA3::from_vector(&Vector::from_components(1.0, 0.0, 0.0)));
252
253        Self {
254            inner: normalized,
255            is_even: false,
256        }
257    }
258
259    /// Convert from a Rotor
260    pub fn from_rotor(rotor: Rotor) -> Self {
261        Self {
262            inner: rotor.inner,
263            is_even: true,
264        }
265    }
266
267    /// Get the internal multivector
268    pub fn as_multivector(&self) -> &GA3 {
269        &self.inner
270    }
271
272    /// Apply this versor to transform a multivector
273    ///
274    /// For even versors: V * v * V†
275    /// For odd versors: V * v * V† (with grade involution handling)
276    pub fn transform(&self, v: &GA3) -> GA3 {
277        let rev = self.inner.reverse();
278        if self.is_even {
279            self.inner.geometric_product(v).geometric_product(&rev)
280        } else {
281            // For odd versors, we need to handle the sign change
282            // This is simplified - full implementation would use grade involution
283            let result = self.inner.geometric_product(v).geometric_product(&rev);
284            &result * -1.0
285        }
286    }
287
288    /// Compose two versors
289    pub fn then(&self, other: &Versor) -> Versor {
290        Versor {
291            inner: other.inner.geometric_product(&self.inner),
292            is_even: self.is_even == other.is_even, // Even * Even = Even, Odd * Odd = Even
293        }
294    }
295
296    /// Check if this is an even versor (rotor)
297    pub fn is_rotor(&self) -> bool {
298        self.is_even
299    }
300
301    /// Try to convert to a Rotor (only succeeds for even versors)
302    pub fn to_rotor(&self) -> Option<Rotor> {
303        if self.is_even {
304            Some(Rotor {
305                inner: self.inner.clone(),
306            })
307        } else {
308            None
309        }
310    }
311}
312
313/// A translation represented geometrically
314///
315/// In standard GA3 (Euclidean), translations are not directly representable
316/// as versors. This type provides a convenient API that internally uses
317/// vector addition.
318#[derive(Clone, Debug)]
319pub struct Translation {
320    /// Translation vector components
321    x: f64,
322    y: f64,
323    z: f64,
324}
325
326impl Translation {
327    /// Create a new translation
328    pub fn new(x: f64, y: f64, z: f64) -> Self {
329        Self { x, y, z }
330    }
331
332    /// Create a translation along the X axis
333    pub fn x(amount: f64) -> Self {
334        Self::new(amount, 0.0, 0.0)
335    }
336
337    /// Create a translation along the Y axis
338    pub fn y(amount: f64) -> Self {
339        Self::new(0.0, amount, 0.0)
340    }
341
342    /// Create a translation along the Z axis
343    pub fn z(amount: f64) -> Self {
344        Self::new(0.0, 0.0, amount)
345    }
346
347    /// Apply this translation to a multivector
348    ///
349    /// For vectors, this adds the translation. For other grades,
350    /// behavior depends on the geometric interpretation.
351    pub fn transform(&self, v: &GA3) -> GA3 {
352        let trans_vec = Vector::<3, 0, 0>::from_components(self.x, self.y, self.z);
353        let trans_mv = GA3::from_vector(&trans_vec);
354        v + &trans_mv
355    }
356
357    /// Compose two translations
358    pub fn then(&self, other: &Translation) -> Translation {
359        Translation {
360            x: self.x + other.x,
361            y: self.y + other.y,
362            z: self.z + other.z,
363        }
364    }
365
366    /// Get the inverse translation
367    pub fn inverse(&self) -> Translation {
368        Translation {
369            x: -self.x,
370            y: -self.y,
371            z: -self.z,
372        }
373    }
374
375    /// Linear interpolation of translation
376    pub fn lerp(&self, t: f64) -> Translation {
377        Translation {
378            x: self.x * t,
379            y: self.y * t,
380            z: self.z * t,
381        }
382    }
383
384    /// Linear interpolation to another translation
385    pub fn lerp_to(&self, other: &Translation, t: f64) -> Translation {
386        Translation {
387            x: self.x + (other.x - self.x) * t,
388            y: self.y + (other.y - self.y) * t,
389            z: self.z + (other.z - self.z) * t,
390        }
391    }
392}
393
394/// A general geometric transformation combining rotation and translation
395#[derive(Clone, Debug)]
396pub struct Transform {
397    /// Rotation component
398    rotor: Rotor,
399    /// Translation component (applied after rotation)
400    translation: Translation,
401}
402
403impl Transform {
404    /// Create a new transform with rotation and translation
405    pub fn new(rotor: Rotor, translation: Translation) -> Self {
406        Self { rotor, translation }
407    }
408
409    /// Create identity transform
410    pub fn identity() -> Self {
411        Self {
412            rotor: Rotor::identity(),
413            translation: Translation::new(0.0, 0.0, 0.0),
414        }
415    }
416
417    /// Create a pure rotation transform
418    pub fn rotation(rotor: Rotor) -> Self {
419        Self {
420            rotor,
421            translation: Translation::new(0.0, 0.0, 0.0),
422        }
423    }
424
425    /// Create a pure translation transform
426    pub fn translation(translation: Translation) -> Self {
427        Self {
428            rotor: Rotor::identity(),
429            translation,
430        }
431    }
432
433    /// Apply this transform to a multivector
434    pub fn transform(&self, v: &GA3) -> GA3 {
435        let rotated = self.rotor.transform(v);
436        self.translation.transform(&rotated)
437    }
438
439    /// Compose two transforms: self followed by other
440    pub fn then(&self, other: &Transform) -> Transform {
441        // First apply self's rotation, then self's translation
442        // Then apply other's rotation, then other's translation
443        //
444        // Combined rotation: other.rotor * self.rotor
445        // Translation is more complex due to rotation interaction
446        let combined_rotor = self.rotor.then(&other.rotor);
447
448        // Transform self's translation by other's rotation, then add other's translation
449        let self_trans_vec = Vector::<3, 0, 0>::from_components(
450            self.translation.x,
451            self.translation.y,
452            self.translation.z,
453        );
454        let self_trans_mv = GA3::from_vector(&self_trans_vec);
455        let rotated_trans = other.rotor.transform(&self_trans_mv);
456
457        let combined_translation = Translation::new(
458            rotated_trans.get(1) + other.translation.x,
459            rotated_trans.get(2) + other.translation.y,
460            rotated_trans.get(4) + other.translation.z,
461        );
462
463        Transform {
464            rotor: combined_rotor,
465            translation: combined_translation,
466        }
467    }
468
469    /// Get the inverse transform
470    pub fn inverse(&self) -> Transform {
471        let inv_rotor = self.rotor.inverse();
472        let inv_trans_base = self.translation.inverse();
473
474        // Rotate the inverse translation by the inverse rotation
475        let trans_vec = Vector::<3, 0, 0>::from_components(
476            inv_trans_base.x,
477            inv_trans_base.y,
478            inv_trans_base.z,
479        );
480        let trans_mv = GA3::from_vector(&trans_vec);
481        let rotated = inv_rotor.transform(&trans_mv);
482
483        Transform {
484            rotor: inv_rotor,
485            translation: Translation::new(rotated.get(1), rotated.get(2), rotated.get(4)),
486        }
487    }
488
489    /// Interpolate between identity and this transform
490    pub fn interpolate(&self, t: f64) -> Transform {
491        Transform {
492            rotor: self.rotor.slerp(t),
493            translation: self.translation.lerp(t),
494        }
495    }
496
497    /// Interpolate to another transform
498    pub fn interpolate_to(&self, other: &Transform, t: f64) -> Transform {
499        Transform {
500            rotor: self.rotor.slerp_to(&other.rotor, t),
501            translation: self.translation.lerp_to(&other.translation, t),
502        }
503    }
504}
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509    use std::f64::consts::PI;
510
511    #[test]
512    fn test_rotor_identity() {
513        let r = Rotor::identity();
514        let v = Vector::<3, 0, 0>::from_components(1.0, 2.0, 3.0);
515        let mv = GA3::from_vector(&v);
516
517        let rotated = r.transform(&mv);
518
519        // Identity should not change the vector
520        assert!((rotated.get(1) - 1.0).abs() < 1e-10);
521        assert!((rotated.get(2) - 2.0).abs() < 1e-10);
522        assert!((rotated.get(4) - 3.0).abs() < 1e-10);
523    }
524
525    #[test]
526    fn test_rotor_xy_90() {
527        // 90 degree rotation in XY plane
528        let r = Rotor::xy(PI / 2.0);
529
530        // Rotate (1, 0, 0) should give (0, 1, 0)
531        let v = GA3::from_vector(&Vector::from_components(1.0, 0.0, 0.0));
532        let rotated = r.transform(&v);
533
534        assert!((rotated.get(1) - 0.0).abs() < 1e-10); // x -> 0
535        assert!((rotated.get(2) - 1.0).abs() < 1e-10); // y -> 1
536        assert!((rotated.get(4) - 0.0).abs() < 1e-10); // z -> 0
537    }
538
539    #[test]
540    fn test_rotor_preserves_magnitude() {
541        let r = Rotor::from_bivector_angle(1.23, 1.0, 2.0, 3.0);
542        let v = GA3::from_vector(&Vector::from_components(3.0, 4.0, 5.0));
543
544        let original_mag = v.magnitude();
545        let rotated = r.transform(&v);
546        let rotated_mag = rotated.magnitude();
547
548        assert!(
549            (original_mag - rotated_mag).abs() < 1e-10,
550            "Magnitude changed: {} -> {}",
551            original_mag,
552            rotated_mag
553        );
554    }
555
556    #[test]
557    fn test_rotor_composition() {
558        // Two 90-degree rotations should equal one 180-degree rotation
559        let r1 = Rotor::xy(PI / 2.0);
560        let r2 = Rotor::xy(PI / 2.0);
561        let r_combined = r1.then(&r2);
562
563        let v = GA3::from_vector(&Vector::from_components(1.0, 0.0, 0.0));
564        let rotated = r_combined.transform(&v);
565
566        // (1, 0, 0) rotated 180 degrees should give (-1, 0, 0)
567        assert!((rotated.get(1) + 1.0).abs() < 1e-10);
568        assert!((rotated.get(2) - 0.0).abs() < 1e-10);
569    }
570
571    #[test]
572    fn test_rotor_inverse() {
573        let r = Rotor::from_bivector_angle(0.7, 1.0, 1.0, 1.0);
574        let v = GA3::from_vector(&Vector::from_components(1.0, 2.0, 3.0));
575
576        let rotated = r.transform(&v);
577        let back = r.inverse().transform(&rotated);
578
579        assert!((back.get(1) - 1.0).abs() < 1e-10);
580        assert!((back.get(2) - 2.0).abs() < 1e-10);
581        assert!((back.get(4) - 3.0).abs() < 1e-10);
582    }
583
584    #[test]
585    fn test_rotor_slerp() {
586        let r = Rotor::xy(PI);
587
588        // Halfway interpolation should give PI/2 rotation
589        let half = r.slerp(0.5);
590
591        let v = GA3::from_vector(&Vector::from_components(1.0, 0.0, 0.0));
592        let rotated = half.transform(&v);
593
594        // (1, 0, 0) rotated 90 degrees should give (0, 1, 0)
595        assert!((rotated.get(1) - 0.0).abs() < 1e-10);
596        assert!((rotated.get(2) - 1.0).abs() < 1e-10);
597    }
598
599    #[test]
600    fn test_translation() {
601        let t = Translation::new(1.0, 2.0, 3.0);
602        let v = GA3::from_vector(&Vector::from_components(0.0, 0.0, 0.0));
603
604        let translated = t.transform(&v);
605
606        assert!((translated.get(1) - 1.0).abs() < 1e-10);
607        assert!((translated.get(2) - 2.0).abs() < 1e-10);
608        assert!((translated.get(4) - 3.0).abs() < 1e-10);
609    }
610
611    #[test]
612    fn test_transform_composition() {
613        let rot = Rotor::xy(PI / 2.0);
614        let trans = Translation::new(1.0, 0.0, 0.0);
615        let transform = Transform::new(rot, trans);
616
617        let v = GA3::from_vector(&Vector::from_components(1.0, 0.0, 0.0));
618        let result = transform.transform(&v);
619
620        // First rotate (1,0,0) by 90deg -> (0,1,0)
621        // Then translate by (1,0,0) -> (1,1,0)
622        assert!((result.get(1) - 1.0).abs() < 1e-10);
623        assert!((result.get(2) - 1.0).abs() < 1e-10);
624    }
625
626    #[test]
627    fn test_amari_rotor_roundtrip() {
628        let r = Rotor::xy(PI / 3.0);
629        let amari_r = r.to_amari_rotor();
630
631        // The amari rotor should have the same angle
632        let scalar = amari_r.as_multivector().get(0);
633        let angle = scalar.clamp(-1.0, 1.0).acos() * 2.0;
634        assert!((angle - PI / 3.0).abs() < 1e-10);
635
636        // Roundtrip: convert back
637        let r2 = Rotor::from_amari_rotor(&amari_r);
638        let v = GA3::from_vector(&Vector::from_components(1.0, 0.0, 0.0));
639
640        let result1 = r.transform(&v);
641        let result2 = r2.transform(&v);
642
643        assert!((result1.get(1) - result2.get(1)).abs() < 1e-10);
644        assert!((result1.get(2) - result2.get(2)).abs() < 1e-10);
645        assert!((result1.get(4) - result2.get(4)).abs() < 1e-10);
646    }
647}