Skip to main content

inset_embedder/geometry/
matrix.rs

1//! Flutter counterpart: the `package:vector_math` surface the framework uses on
2//! `Matrix4` — `transform3`, `decompose` and `compose`, with the `Vector3` and
3//! `Quaternion` those take.
4
5use std::ops::{Add, Mul};
6
7use crate::{Matrix4, Offset};
8
9/// A three-dimensional vector.
10///
11/// Flutter counterpart: `Vector3` (package:vector_math), narrowed to the `f32` the
12/// host's [`Matrix4`] stores.
13#[derive(Clone, Copy, Debug, Default, PartialEq)]
14pub struct Vector3 {
15    /// The first component.
16    pub x: f32,
17    /// The second component.
18    pub y: f32,
19    /// The third component.
20    pub z: f32,
21}
22
23impl Vector3 {
24    /// Constructs a new vector.
25    pub const fn new(x: f32, y: f32, z: f32) -> Vector3 {
26        Vector3 { x, y, z }
27    }
28
29    /// The zero vector.
30    pub const ZERO: Vector3 = Vector3::new(0.0, 0.0, 0.0);
31
32    /// The length of the vector.
33    pub fn length(&self) -> f32 {
34        (self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
35    }
36}
37
38impl Add for Vector3 {
39    type Output = Vector3;
40
41    fn add(self, other: Vector3) -> Vector3 {
42        Vector3::new(self.x + other.x, self.y + other.y, self.z + other.z)
43    }
44}
45
46impl Mul<f32> for Vector3 {
47    type Output = Vector3;
48
49    fn mul(self, scale: f32) -> Vector3 {
50        Vector3::new(self.x * scale, self.y * scale, self.z * scale)
51    }
52}
53
54/// A rotation as a unit quaternion.
55///
56/// Flutter counterpart: `Quaternion` (package:vector_math), narrowed to the `f32` the
57/// host's [`Matrix4`] stores.
58#[derive(Clone, Copy, Debug, PartialEq)]
59pub struct Quaternion {
60    /// The first imaginary component.
61    pub x: f32,
62    /// The second imaginary component.
63    pub y: f32,
64    /// The third imaginary component.
65    pub z: f32,
66    /// The real component.
67    pub w: f32,
68}
69
70impl Quaternion {
71    /// Constructs a quaternion from its four components.
72    pub const fn new(x: f32, y: f32, z: f32, w: f32) -> Quaternion {
73        Quaternion { x, y, z, w }
74    }
75
76    /// The rotation that changes nothing.
77    pub const IDENTITY: Quaternion = Quaternion::new(0.0, 0.0, 0.0, 1.0);
78
79    /// The norm of the quaternion.
80    pub fn length(&self) -> f32 {
81        self.length2().sqrt()
82    }
83
84    /// The squared norm of the quaternion.
85    pub fn length2(&self) -> f32 {
86        self.x * self.x + self.y * self.y + self.z * self.z + self.w * self.w
87    }
88
89    /// A copy of this scaled by `scale`.
90    pub fn scaled(&self, scale: f32) -> Quaternion {
91        Quaternion::new(
92            self.x * scale,
93            self.y * scale,
94            self.z * scale,
95            self.w * scale,
96        )
97    }
98
99    /// A copy of this with unit length.
100    ///
101    /// A zero-length quaternion is returned unchanged, as `Quaternion.normalize`
102    /// leaves it.
103    pub fn normalized(&self) -> Quaternion {
104        let length = self.length();
105        if length == 0.0 {
106            return *self;
107        }
108        self.scaled(1.0 / length)
109    }
110}
111
112impl Default for Quaternion {
113    fn default() -> Quaternion {
114        Quaternion::IDENTITY
115    }
116}
117
118impl Add for Quaternion {
119    type Output = Quaternion;
120
121    fn add(self, other: Quaternion) -> Quaternion {
122        Quaternion::new(
123            self.x + other.x,
124            self.y + other.y,
125            self.z + other.z,
126            self.w + other.w,
127        )
128    }
129}
130
131/// Transforms a point as a 3-vector, with no perspective division.
132///
133/// Flutter counterpart: `Matrix4.transform3` applied to `Vector3(dx, dy, 0)`.
134/// It takes the transformed x and y and ignores the resulting w.
135///
136/// This is not [`Matrix4::map_point`], which divides by w.
137pub fn transform3(matrix: Matrix4, point: Offset) -> Offset {
138    let m = matrix.to_flutter_array();
139    let (x, y) = (point.dx() as f32, point.dy() as f32);
140    Offset::new(
141        (m[0] * x + m[4] * y + m[12]) as f64,
142        (m[1] * x + m[5] * y + m[13]) as f64,
143    )
144}
145
146/// Decomposes `matrix` into its translation, rotation and scale components.
147///
148/// Flutter counterpart: `Matrix4.decompose(translation, rotation, scale)`, which
149/// fills three out parameters.
150pub fn decompose(matrix: Matrix4) -> (Vector3, Quaternion, Vector3) {
151    let m = matrix.to_flutter_array();
152    let mut sx = Vector3::new(m[0], m[1], m[2]).length();
153    let sy = Vector3::new(m[4], m[5], m[6]).length();
154    let sz = Vector3::new(m[8], m[9], m[10]).length();
155
156    if determinant(&m) < 0.0 {
157        sx = -sx;
158    }
159
160    let translation = Vector3::new(m[12], m[13], m[14]);
161
162    let inv_sx = 1.0 / sx;
163    let inv_sy = 1.0 / sy;
164    let inv_sz = 1.0 / sz;
165
166    // `Matrix4.copyRotation` over the scale-normalized matrix, into column-major
167    // 3×3 storage.
168    let rotation_matrix = [
169        m[0] * inv_sx,
170        m[1] * inv_sx,
171        m[2] * inv_sx,
172        m[4] * inv_sy,
173        m[5] * inv_sy,
174        m[6] * inv_sy,
175        m[8] * inv_sz,
176        m[9] * inv_sz,
177        m[10] * inv_sz,
178    ];
179    let rotation = quaternion_from_rotation(&rotation_matrix);
180
181    (translation, rotation, Vector3::new(sx, sy, sz))
182}
183
184/// Composes a matrix from `translation`, `rotation` and `scale`.
185///
186/// Flutter counterpart: `Matrix4.compose`, which is
187/// `setFromTranslationRotation` followed by `scaleByVector3`.
188pub fn compose(translation: Vector3, rotation: Quaternion, scale: Vector3) -> Matrix4 {
189    let (x, y, z, w) = (rotation.x, rotation.y, rotation.z, rotation.w);
190    let (x2, y2, z2) = (x + x, y + y, z + z);
191    let (xx, xy, xz) = (x * x2, x * y2, x * z2);
192    let (yy, yz, zz) = (y * y2, y * z2, z * z2);
193    let (wx, wy, wz) = (w * x2, w * y2, w * z2);
194
195    let mut m = [
196        1.0 - (yy + zz),
197        xy + wz,
198        xz - wy,
199        0.0,
200        xy - wz,
201        1.0 - (xx + zz),
202        yz + wx,
203        0.0,
204        xz + wy,
205        yz - wx,
206        1.0 - (xx + yy),
207        0.0,
208        translation.x,
209        translation.y,
210        translation.z,
211        1.0,
212    ];
213
214    // `Matrix4.scaleByVector3`, which scales the fourth column by 1.0.
215    for (column, factor) in [scale.x, scale.y, scale.z, 1.0].into_iter().enumerate() {
216        for row in 0..4 {
217            m[column * 4 + row] *= factor;
218        }
219    }
220
221    Matrix4::from_flutter_array(&m)
222}
223
224/// `Matrix4.determinant`, over column-major storage.
225fn determinant(m: &[f32; 16]) -> f32 {
226    let det2_01_01 = m[0] * m[5] - m[1] * m[4];
227    let det2_01_02 = m[0] * m[6] - m[2] * m[4];
228    let det2_01_03 = m[0] * m[7] - m[3] * m[4];
229    let det2_01_12 = m[1] * m[6] - m[2] * m[5];
230    let det2_01_13 = m[1] * m[7] - m[3] * m[5];
231    let det2_01_23 = m[2] * m[7] - m[3] * m[6];
232    let det3_201_012 = m[8] * det2_01_12 - m[9] * det2_01_02 + m[10] * det2_01_01;
233    let det3_201_013 = m[8] * det2_01_13 - m[9] * det2_01_03 + m[11] * det2_01_01;
234    let det3_201_023 = m[8] * det2_01_23 - m[10] * det2_01_03 + m[11] * det2_01_02;
235    let det3_201_123 = m[9] * det2_01_23 - m[10] * det2_01_13 + m[11] * det2_01_12;
236    -det3_201_123 * m[12] + det3_201_023 * m[13] - det3_201_013 * m[14] + det3_201_012 * m[15]
237}
238
239/// `Quaternion.setFromRotation`, over column-major 3×3 storage.
240fn quaternion_from_rotation(m: &[f32; 9]) -> Quaternion {
241    /// `Matrix3.index`.
242    fn index(row: usize, col: usize) -> usize {
243        col * 3 + row
244    }
245
246    let trace = m[0] + m[4] + m[8];
247    let mut q = [0.0f32; 4];
248    if trace > 0.0 {
249        let mut s = (trace + 1.0).sqrt();
250        q[3] = s * 0.5;
251        s = 0.5 / s;
252        q[0] = (m[5] - m[7]) * s;
253        q[1] = (m[6] - m[2]) * s;
254        q[2] = (m[1] - m[3]) * s;
255    } else {
256        let i = if m[0] < m[4] {
257            if m[4] < m[8] { 2 } else { 1 }
258        } else if m[0] < m[8] {
259            2
260        } else {
261            0
262        };
263        let j = (i + 1) % 3;
264        let k = (i + 2) % 3;
265        let mut s = (m[index(i, i)] - m[index(j, j)] - m[index(k, k)] + 1.0).sqrt();
266        q[i] = s * 0.5;
267        s = 0.5 / s;
268        q[3] = (m[index(k, j)] - m[index(j, k)]) * s;
269        q[j] = (m[index(j, i)] + m[index(i, j)]) * s;
270        q[k] = (m[index(k, i)] + m[index(i, k)]) * s;
271    }
272    Quaternion::new(q[0], q[1], q[2], q[3])
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    fn assert_close(a: Matrix4, b: Matrix4) {
280        let (a, b) = (a.to_flutter_array(), b.to_flutter_array());
281        for (index, (a, b)) in a.iter().zip(b.iter()).enumerate() {
282            assert!((a - b).abs() < 1e-5, "entry {index}: {a} vs {b}");
283        }
284    }
285
286    #[test]
287    fn decompose_reads_back_a_translation_and_a_scale() {
288        let matrix = Matrix4::translation(3.0, 4.0).then(&Matrix4::scale(2.0, 5.0));
289        let (translation, rotation, scale) = decompose(matrix);
290        assert_eq!(translation, Vector3::new(3.0, 4.0, 0.0));
291        assert_eq!(rotation, Quaternion::IDENTITY);
292        assert_eq!(scale, Vector3::new(2.0, 5.0, 1.0));
293    }
294
295    #[test]
296    fn compose_inverts_decompose() {
297        let matrix = Matrix4::translation(3.0, 4.0)
298            .then(&Matrix4::rotation(0.7))
299            .then(&Matrix4::scale(2.0, 5.0));
300        let (translation, rotation, scale) = decompose(matrix);
301        assert_close(compose(translation, rotation, scale), matrix);
302    }
303
304    #[test]
305    fn a_rotation_round_trips_through_a_quaternion() {
306        let matrix = Matrix4::rotation(std::f32::consts::FRAC_PI_3);
307        let (translation, rotation, scale) = decompose(matrix);
308        assert!((rotation.length() - 1.0).abs() < 1e-6);
309        assert_close(compose(translation, rotation, scale), matrix);
310    }
311
312    #[test]
313    fn a_mirrored_matrix_decomposes_with_a_negative_x_scale() {
314        let matrix = Matrix4::scale(-2.0, 3.0);
315        let (_, _, scale) = decompose(matrix);
316        assert_eq!(scale.x, -2.0);
317        assert_eq!(scale.y, 3.0);
318    }
319}