Skip to main content

ferroui_base/rendering/composition/
matrix_utils.rs

1//! Helpers computing the transform of a composition visual.
2
3use crate::numerics::{Matrix4x4, Quaternion, Vector3};
4use crate::{Matrix, Vector, Vector3D};
5
6/// Helpers computing the transform of a composition visual.
7pub struct MatrixUtils;
8
9impl MatrixUtils {
10    /// Computes the transform of a visual from its transform properties.
11    /// Returns `None` when the transform is the identity.
12    #[allow(clippy::too_many_arguments)]
13    pub fn compute_transform(
14        size: Vector,
15        anchor_point: Vector,
16        center_point: Vector3D,
17        transform_matrix: Matrix,
18        scale: Vector3D,
19        rotation_angle: f32,
20        orientation: Quaternion,
21        offset: Vector3D,
22    ) -> Option<Matrix> {
23        // The math here follows the *observed* UWP behavior since there are no docs on how it's supposed to work
24
25        let anchor = Vector::multiply(size, anchor_point);
26        let mut mat = Matrix::create_translation(-anchor.x, -anchor.y);
27
28        let center = Vector3D::new(center_point.x, center_point.y, center_point.z);
29        let center3: Vector3 = center.into();
30
31        if !transform_matrix.is_identity() {
32            mat = transform_matrix * mat;
33        }
34
35        if scale != Vector3D::new(1.0, 1.0, 1.0) {
36            mat *= Self::to_matrix(Matrix4x4::create_scale_at(scale.into(), center3));
37        }
38
39        //TODO: RotationAxis support
40        if rotation_angle != 0.0 {
41            mat *= Self::to_matrix(Matrix4x4::create_rotation_z_at(rotation_angle, center3));
42        }
43
44        if orientation != Quaternion::IDENTITY {
45            if center_point != Vector3D::default() {
46                mat *= Self::to_matrix(
47                    Matrix4x4::create_translation(-center3)
48                        * Matrix4x4::create_from_quaternion(orientation)
49                        * Matrix4x4::create_translation(center3),
50                );
51            } else {
52                mat *= Self::to_matrix(Matrix4x4::create_from_quaternion(orientation));
53            }
54        }
55
56        if offset != Vector3D::default() {
57            if offset.z == 0.0 {
58                mat *= Matrix::create_translation(offset.x, offset.y);
59            } else {
60                mat *= Self::to_matrix(Matrix4x4::create_translation(offset.into()));
61            }
62        }
63
64        if mat.is_identity() {
65            return None;
66        }
67        Some(mat)
68    }
69
70    /// Converts a 3x3 matrix to the 4x4 matrix that leaves Z unchanged.
71    pub fn to_matrix4x4(matrix: Matrix) -> Matrix4x4 {
72        Matrix4x4::new(
73            matrix.m11 as f32,
74            matrix.m12 as f32,
75            0.0,
76            matrix.m13 as f32,
77            matrix.m21 as f32,
78            matrix.m22 as f32,
79            0.0,
80            matrix.m23 as f32,
81            0.0,
82            0.0,
83            1.0,
84            0.0,
85            matrix.m31 as f32,
86            matrix.m32 as f32,
87            0.0,
88            matrix.m33 as f32,
89        )
90    }
91
92    /// Converts a 4x4 matrix to a 3x3 matrix, dropping the Z row and column.
93    pub fn to_matrix(matrix44: Matrix4x4) -> Matrix {
94        Matrix::new_3x3(
95            matrix44.m11 as f64,
96            matrix44.m12 as f64,
97            matrix44.m14 as f64,
98            matrix44.m21 as f64,
99            matrix44.m22 as f64,
100            matrix44.m24 as f64,
101            matrix44.m41 as f64,
102            matrix44.m42 as f64,
103            matrix44.m44 as f64,
104        )
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::Point;
112
113    const ONE: Vector3D = Vector3D::new(1.0, 1.0, 1.0);
114    const ZERO: Vector3D = Vector3D::new(0.0, 0.0, 0.0);
115
116    #[track_caller]
117    fn close(actual: Matrix, expected: Matrix) {
118        let a = [
119            actual.m11, actual.m12, actual.m13, actual.m21, actual.m22, actual.m23, actual.m31, actual.m32, actual.m33,
120        ];
121        let e = [
122            expected.m11,
123            expected.m12,
124            expected.m13,
125            expected.m21,
126            expected.m22,
127            expected.m23,
128            expected.m31,
129            expected.m32,
130            expected.m33,
131        ];
132        for (a, e) in a.iter().zip(e) {
133            assert!((a - e).abs() < 1e-5, "{actual:?} vs {expected:?}");
134        }
135    }
136
137    #[test]
138    fn identity_inputs_give_no_transform() {
139        assert_eq!(
140            MatrixUtils::compute_transform(
141                Vector::new(100.0, 50.0),
142                Vector::ZERO,
143                ZERO,
144                Matrix::IDENTITY,
145                ONE,
146                0.0,
147                Quaternion::IDENTITY,
148                ZERO
149            ),
150            None
151        );
152    }
153
154    #[test]
155    fn offset_and_anchor_translate() {
156        assert_eq!(
157            MatrixUtils::compute_transform(
158                Vector::new(100.0, 50.0),
159                Vector::new(0.5, 1.0),
160                ZERO,
161                Matrix::IDENTITY,
162                ONE,
163                0.0,
164                Quaternion::IDENTITY,
165                Vector3D::new(10.0, 20.0, 0.0)
166            ),
167            Some(Matrix::create_translation(-40.0, -30.0))
168        );
169        // An offset with a Z component goes through the 4x4 path.
170        assert_eq!(
171            MatrixUtils::compute_transform(
172                Vector::ZERO,
173                Vector::ZERO,
174                ZERO,
175                Matrix::IDENTITY,
176                ONE,
177                0.0,
178                Quaternion::IDENTITY,
179                Vector3D::new(10.0, 20.0, 5.0)
180            ),
181            Some(Matrix::create_translation(10.0, 20.0))
182        );
183    }
184
185    #[test]
186    fn scale_and_rotation_are_applied_around_the_center_point() {
187        let center = Vector3D::new(10.0, 20.0, 0.0);
188        let scaled = MatrixUtils::compute_transform(
189            Vector::ZERO,
190            Vector::ZERO,
191            center,
192            Matrix::IDENTITY,
193            Vector3D::new(2.0, 3.0, 1.0),
194            0.0,
195            Quaternion::IDENTITY,
196            ZERO,
197        )
198        .unwrap();
199        assert_eq!(scaled, Matrix::new(2.0, 0.0, 0.0, 3.0, -10.0, -40.0));
200        assert_eq!(scaled.transform(Point::new(10.0, 20.0)), Point::new(10.0, 20.0));
201
202        let angle = 0.7f32;
203        let rotated = MatrixUtils::compute_transform(
204            Vector::ZERO,
205            Vector::ZERO,
206            center,
207            Matrix::IDENTITY,
208            ONE,
209            angle,
210            Quaternion::IDENTITY,
211            ZERO,
212        )
213        .unwrap();
214        close(
215            rotated,
216            Matrix::create_rotation_at(angle as f64, Point::new(10.0, 20.0)),
217        );
218
219        // The same rotation given as an orientation.
220        let oriented = MatrixUtils::compute_transform(
221            Vector::ZERO,
222            Vector::ZERO,
223            center,
224            Matrix::IDENTITY,
225            ONE,
226            0.0,
227            Quaternion::create_from_axis_angle(Vector3::UNIT_Z, angle),
228            ZERO,
229        )
230        .unwrap();
231        close(oriented, rotated);
232
233        let oriented_at_origin = MatrixUtils::compute_transform(
234            Vector::ZERO,
235            Vector::ZERO,
236            ZERO,
237            Matrix::IDENTITY,
238            ONE,
239            0.0,
240            Quaternion::create_from_axis_angle(Vector3::UNIT_Z, angle),
241            ZERO,
242        )
243        .unwrap();
244        close(oriented_at_origin, Matrix::create_rotation(angle as f64));
245    }
246
247    #[test]
248    fn transform_matrix_is_applied_before_the_anchor_translation() {
249        let transform = Matrix::create_scale(2.0, 2.0);
250        let actual = MatrixUtils::compute_transform(
251            Vector::new(10.0, 10.0),
252            Vector::new(1.0, 1.0),
253            ZERO,
254            transform,
255            ONE,
256            0.0,
257            Quaternion::IDENTITY,
258            ZERO,
259        )
260        .unwrap();
261        assert_eq!(actual, transform * Matrix::create_translation(-10.0, -10.0));
262    }
263
264    #[test]
265    fn matrix_conversions_drop_and_restore_the_z_axis() {
266        let m = Matrix::new_3x3(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0);
267        let m44 = MatrixUtils::to_matrix4x4(m);
268        assert_eq!(
269            m44,
270            Matrix4x4::new(1.0, 2.0, 0.0, 3.0, 4.0, 5.0, 0.0, 6.0, 0.0, 0.0, 1.0, 0.0, 7.0, 8.0, 0.0, 9.0)
271        );
272        assert_eq!(MatrixUtils::to_matrix(m44), m);
273    }
274}