1use crate::{Point, Rect};
2
3#[derive(Clone, Copy, Debug, PartialEq)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[cfg_attr(feature = "serde", serde(transparent))]
10pub struct Matrix(glam::Mat4);
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum MatrixKind {
15 AxisAligned,
17 Affine,
19 General,
21}
22
23const W_EPSILON: f32 = 1e-6;
25
26impl Matrix {
27 pub const IDENTITY: Matrix = Matrix(glam::Mat4::IDENTITY);
29
30 pub fn translation(tx: f32, ty: f32) -> Self {
32 Matrix(glam::Mat4::from_translation(glam::Vec3::new(tx, ty, 0.0)))
33 }
34
35 pub fn scale(sx: f32, sy: f32) -> Self {
37 Matrix(glam::Mat4::from_scale(glam::Vec3::new(sx, sy, 1.0)))
38 }
39
40 pub fn rotation(radians: f32) -> Self {
44 Matrix(glam::Mat4::from_rotation_z(radians))
45 }
46
47 pub fn from_affine(a: f32, b: f32, c: f32, d: f32, tx: f32, ty: f32) -> Self {
51 Matrix(glam::Mat4::from_cols_array(&[
52 a, b, 0.0, 0.0, c, d, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, tx, ty, 0.0, 1.0,
56 ]))
57 }
58
59 pub fn from_flutter_array(values: &[f32; 16]) -> Self {
61 Matrix(glam::Mat4::from_cols_array(values))
62 }
63
64 pub fn to_flutter_array(&self) -> [f32; 16] {
66 self.0.to_cols_array()
67 }
68
69 pub fn to_mat4(self) -> glam::Mat4 {
71 self.0
72 }
73
74 pub fn then(&self, other: &Matrix) -> Matrix {
78 Matrix(self.0 * other.0)
79 }
80
81 pub fn is_affine(&self) -> bool {
83 let m = &self.0;
84 m.x_axis.w == 0.0 && m.y_axis.w == 0.0 && m.w_axis.w == 1.0
85 }
86
87 pub fn kind(&self) -> MatrixKind {
89 if !self.is_affine() {
90 return MatrixKind::General;
91 }
92 let m = &self.0;
93 let axis_aligned =
94 m.x_axis.y == 0.0 && m.y_axis.x == 0.0 && m.x_axis.x > 0.0 && m.y_axis.y > 0.0;
95 if axis_aligned {
96 MatrixKind::AxisAligned
97 } else {
98 MatrixKind::Affine
99 }
100 }
101
102 pub fn map_point(&self, p: Point) -> Point {
106 let v = self.0 * glam::Vec4::new(p.x, p.y, 0.0, 1.0);
107 let w = if v.w > W_EPSILON { v.w } else { W_EPSILON };
108 Point::new(v.x / w, v.y / w)
109 }
110
111 pub fn map_rect(&self, r: &Rect) -> Rect {
116 let (mut left, mut top) = (f32::MAX, f32::MAX);
117 let (mut right, mut bottom) = (f32::MIN, f32::MIN);
118 for corner in r.corners() {
119 let v = self.0 * glam::Vec4::new(corner.x, corner.y, 0.0, 1.0);
120 if v.w <= W_EPSILON {
121 return Rect::EVERYTHING;
122 }
123 let (x, y) = (v.x / v.w, v.y / v.w);
124 left = left.min(x);
125 top = top.min(y);
126 right = right.max(x);
127 bottom = bottom.max(y);
128 }
129 Rect::from_ltrb(left, top, right, bottom)
130 }
131
132 pub fn max_scale(&self) -> f32 {
136 let m = &self.0;
137 let sx = (m.x_axis.x * m.x_axis.x + m.x_axis.y * m.x_axis.y).sqrt();
138 let sy = (m.y_axis.x * m.y_axis.x + m.y_axis.y * m.y_axis.y).sqrt();
139 sx.max(sy)
140 }
141
142 pub fn to_affine(&self) -> [f32; 6] {
147 let m = &self.0;
148 [
149 m.x_axis.x, m.x_axis.y, m.y_axis.x, m.y_axis.y, m.w_axis.x, m.w_axis.y,
150 ]
151 }
152
153 pub fn determinant(&self) -> f32 {
155 let m = &self.0;
156 m.x_axis.x * m.y_axis.y - m.x_axis.y * m.y_axis.x
157 }
158
159 pub fn invert(&self) -> Option<Matrix> {
161 let det = self.0.determinant();
162 if det == 0.0 || !det.is_finite() {
163 return None;
164 }
165 let inverse = self.0.inverse();
166 inverse.is_finite().then_some(Matrix(inverse))
167 }
168}
169
170impl Default for Matrix {
171 fn default() -> Self {
172 Self::IDENTITY
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 fn close(a: Point, b: Point) -> bool {
181 (a.x - b.x).abs() < 1e-4 && (a.y - b.y).abs() < 1e-4
182 }
183
184 #[test]
185 fn near_singular_matrices_invert_to_none() {
186 assert!(Matrix::scale(1e-20, 1e-20).invert().is_none());
187 assert!(Matrix::scale(0.0, 1.0).invert().is_none());
188 }
189
190 #[test]
191 fn then_applies_local_first() {
192 let t = Matrix::translation(10.0, 0.0).then(&Matrix::scale(2.0, 2.0));
194 assert!(close(
195 t.map_point(Point::new(1.0, 1.0)),
196 Point::new(12.0, 2.0)
197 ));
198 }
199
200 #[test]
201 fn rotation_quarter_turn() {
202 let t = Matrix::rotation(std::f32::consts::FRAC_PI_2);
203 assert!(close(
205 t.map_point(Point::new(1.0, 0.0)),
206 Point::new(0.0, 1.0)
207 ));
208 }
209
210 #[test]
211 fn invert_roundtrip() {
212 let t = Matrix::translation(5.0, -3.0)
213 .then(&Matrix::rotation(0.7))
214 .then(&Matrix::scale(2.0, 0.5));
215 let inv = t.invert().unwrap();
216 let p = Point::new(3.0, 4.0);
217 assert!(close(inv.map_point(t.map_point(p)), p));
218 }
219
220 #[test]
221 fn map_rect_rotation_is_conservative_bounds() {
222 let t = Matrix::rotation(std::f32::consts::FRAC_PI_4);
223 let r = t.map_rect(&Rect::new(-1.0, -1.0, 2.0, 2.0));
224 let d = 2.0_f32.sqrt();
225 assert!((r.width - 2.0 * d).abs() < 1e-4 && (r.height - 2.0 * d).abs() < 1e-4);
226 }
227
228 #[test]
229 fn perspective_divides_by_w() {
230 let mut values = Matrix::IDENTITY.to_flutter_array();
234 values[3] = 0.001; let t = Matrix::from_flutter_array(&values);
236 assert!(close(
237 t.map_point(Point::new(100.0, 100.0)),
238 Point::new(100.0 / 1.1, 100.0 / 1.1)
239 ));
240 assert_eq!(t.kind(), MatrixKind::General);
241 }
242
243 #[test]
244 fn concatenation_stays_four_by_four() {
245 let mut tilt_values = Matrix::IDENTITY.to_flutter_array();
250 tilt_values[11] = 0.001; let tilt = Matrix::from_flutter_array(&tilt_values);
252 let mut rotate_x = Matrix::IDENTITY.to_flutter_array();
253 let (sin, cos) = 0.5_f32.sin_cos();
255 rotate_x[5] = cos;
256 rotate_x[6] = sin;
257 rotate_x[9] = -sin;
258 rotate_x[10] = cos;
259 let full = tilt.then(&Matrix::from_flutter_array(&rotate_x));
260 assert_eq!(full.kind(), MatrixKind::General);
262 let p = full.map_point(Point::new(0.0, 100.0));
263 let expected_y = 100.0 * cos / (1.0 + 0.001 * 100.0 * sin);
265 assert!((p.y - expected_y).abs() < 1e-2, "{} vs {expected_y}", p.y);
266 }
267
268 #[test]
269 fn eye_plane_bounds_are_everything() {
270 let mut values = Matrix::IDENTITY.to_flutter_array();
271 values[3] = -0.1; let t = Matrix::from_flutter_array(&values);
273 assert_eq!(
274 t.map_rect(&Rect::new(0.0, 0.0, 100.0, 10.0)),
275 Rect::EVERYTHING
276 );
277 }
278}