Skip to main content

projective_grid/hex/
alignment.rs

1//! Dihedral group D6 transforms for hexagonal grids in axial coordinates.
2//!
3//! The 12 symmetries of a regular hexagon: 6 rotations and 6 reflections.
4//! All transforms are expressed as 2x2 integer matrices acting on axial `(q, r)`.
5//!
6//! Reuses [`GridTransform`] — same struct, different values.
7
8use crate::GridTransform;
9
10/// The 12 dihedral transforms D6 on the hexagonal integer grid (axial coordinates).
11///
12/// Rotation generator (60° CW): `(q, r) → (-r, q + r)` = `[[0, -1], [1, 1]]`.
13/// Reflection generator: `(q, r) → (q + r, -r)` = `[[1, 1], [0, -1]]`.
14///
15/// # Order
16///
17/// Indices 0..6 are rotations (0°, 60°, 120°, 180°, 240°, 300°).
18/// Indices 6..12 are reflections (reflection composed with each rotation).
19pub const GRID_TRANSFORMS_D6: [GridTransform; 12] = [
20    // --- Rotations ---
21    // 0°: identity
22    GridTransform {
23        a: 1,
24        b: 0,
25        c: 0,
26        d: 1,
27    },
28    // 60°: (q, r) → (-r, q+r)
29    GridTransform {
30        a: 0,
31        b: -1,
32        c: 1,
33        d: 1,
34    },
35    // 120°: (q, r) → (-q-r, q)
36    GridTransform {
37        a: -1,
38        b: -1,
39        c: 1,
40        d: 0,
41    },
42    // 180°: (q, r) → (-q, -r)
43    GridTransform {
44        a: -1,
45        b: 0,
46        c: 0,
47        d: -1,
48    },
49    // 240°: (q, r) → (r, -q-r)
50    GridTransform {
51        a: 0,
52        b: 1,
53        c: -1,
54        d: -1,
55    },
56    // 300°: (q, r) → (q+r, -q)
57    GridTransform {
58        a: 1,
59        b: 1,
60        c: -1,
61        d: 0,
62    },
63    // --- Reflections (reflection generator composed with rotations) ---
64    // ref ∘ rot(0°): (q, r) → (q+r, -r)
65    GridTransform {
66        a: 1,
67        b: 1,
68        c: 0,
69        d: -1,
70    },
71    // ref ∘ rot(60°): (q, r) → (-r, -q) ... apply ref to (-r, q+r) = (-r + q+r, -(q+r)) = (q, -q-r)
72    GridTransform {
73        a: 1,
74        b: 0,
75        c: -1,
76        d: -1,
77    },
78    // ref ∘ rot(120°): apply ref to (-q-r, q) = (-q-r+q, -q) = (-r, -q)
79    GridTransform {
80        a: 0,
81        b: -1,
82        c: -1,
83        d: 0,
84    },
85    // ref ∘ rot(180°): apply ref to (-q, -r) = (-q-r, r)
86    GridTransform {
87        a: -1,
88        b: -1,
89        c: 0,
90        d: 1,
91    },
92    // ref ∘ rot(240°): apply ref to (r, -q-r) = (r-q-r, q+r) = (-q, q+r)
93    GridTransform {
94        a: -1,
95        b: 0,
96        c: 1,
97        d: 1,
98    },
99    // ref ∘ rot(300°): apply ref to (q+r, -q) = (q+r-q, q) = (r, q)
100    GridTransform {
101        a: 0,
102        b: 1,
103        c: 1,
104        d: 0,
105    },
106];
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use std::collections::HashSet;
112
113    fn compose(a: &GridTransform, b: &GridTransform) -> GridTransform {
114        GridTransform {
115            a: a.a * b.a + a.b * b.c,
116            b: a.a * b.b + a.b * b.d,
117            c: a.c * b.a + a.d * b.c,
118            d: a.c * b.b + a.d * b.d,
119        }
120    }
121
122    fn det(t: &GridTransform) -> i32 {
123        t.a * t.d - t.b * t.c
124    }
125
126    fn as_tuple(t: &GridTransform) -> (i32, i32, i32, i32) {
127        (t.a, t.b, t.c, t.d)
128    }
129
130    #[test]
131    fn all_twelve_distinct() {
132        let set: HashSet<_> = GRID_TRANSFORMS_D6.iter().map(as_tuple).collect();
133        assert_eq!(set.len(), 12);
134    }
135
136    #[test]
137    fn all_unimodular() {
138        for t in &GRID_TRANSFORMS_D6 {
139            let d = det(t);
140            assert!(d == 1 || d == -1, "det = {d} for {t:?}");
141        }
142    }
143
144    #[test]
145    fn rotations_det_plus_one() {
146        for t in &GRID_TRANSFORMS_D6[0..6] {
147            assert_eq!(det(t), 1, "rotation {t:?} should have det +1");
148        }
149    }
150
151    #[test]
152    fn reflections_det_minus_one() {
153        for t in &GRID_TRANSFORMS_D6[6..12] {
154            assert_eq!(det(t), -1, "reflection {t:?} should have det -1");
155        }
156    }
157
158    #[test]
159    fn rotation_order_six() {
160        let rot60 = &GRID_TRANSFORMS_D6[1];
161        let identity = &GRID_TRANSFORMS_D6[0];
162
163        let mut acc = *identity;
164        for k in 1..=6 {
165            acc = compose(&acc, rot60);
166            if k < 6 {
167                assert_ne!(
168                    as_tuple(&acc),
169                    as_tuple(identity),
170                    "rot60^{k} should not be identity"
171                );
172            }
173        }
174        assert_eq!(
175            as_tuple(&acc),
176            as_tuple(identity),
177            "rot60^6 must be identity"
178        );
179    }
180
181    #[test]
182    fn reflections_are_involutions() {
183        for (i, t) in GRID_TRANSFORMS_D6[6..12].iter().enumerate() {
184            let t_sq = compose(t, t);
185            assert_eq!(
186                as_tuple(&t_sq),
187                as_tuple(&GRID_TRANSFORMS_D6[0]),
188                "reflection[{i}]^2 must be identity"
189            );
190        }
191    }
192
193    #[test]
194    fn closure_under_composition() {
195        let set: HashSet<_> = GRID_TRANSFORMS_D6.iter().map(as_tuple).collect();
196        for a in &GRID_TRANSFORMS_D6 {
197            for b in &GRID_TRANSFORMS_D6 {
198                let c = compose(a, b);
199                assert!(
200                    set.contains(&as_tuple(&c)),
201                    "product of {a:?} and {b:?} = {c:?} not in D6"
202                );
203            }
204        }
205    }
206
207    #[test]
208    fn rotations_match_successive_composition() {
209        let rot60 = &GRID_TRANSFORMS_D6[1];
210        let identity = &GRID_TRANSFORMS_D6[0];
211        let mut acc = *identity;
212        for (k, expected) in GRID_TRANSFORMS_D6.iter().enumerate().take(6) {
213            assert_eq!(as_tuple(&acc), as_tuple(expected), "rot60^{k} mismatch");
214            acc = compose(&acc, rot60);
215        }
216    }
217}