Skip to main content

cranpose_ui_graphics/
transform.rs

1//! Projective geometry shared by layout coordinates and renderers.
2
3use crate::{Point, Rect, Size};
4
5/// A measured node's local size and mapping into its window's logical pixels.
6#[derive(Clone, Copy, Debug, Default, PartialEq)]
7pub struct WindowCoordinates {
8    /// Size before graphics-layer transforms.
9    pub size: Size,
10    /// Maps node-local positions through placement and ancestor graphics layers.
11    pub local_to_window: ProjectiveTransform,
12}
13
14impl WindowCoordinates {
15    /// Axis-aligned window bounds of the transformed node.
16    pub fn bounds(self) -> Rect {
17        self.local_to_window
18            .bounds_for_rect(Rect::from_size(self.size))
19    }
20}
21
22/// A projective mapping of a two-dimensional coordinate plane.
23#[derive(Clone, Copy, Debug, PartialEq)]
24pub struct ProjectiveTransform {
25    matrix: [[f32; 3]; 3],
26}
27
28impl ProjectiveTransform {
29    /// Leaves positions unchanged.
30    pub const fn identity() -> Self {
31        Self {
32            matrix: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
33        }
34    }
35
36    /// Translates positions by the supplied logical-pixel offsets.
37    pub fn translation(tx: f32, ty: f32) -> Self {
38        Self {
39            matrix: [[1.0, 0.0, tx], [0.0, 1.0, ty], [0.0, 0.0, 1.0]],
40        }
41    }
42
43    /// Uniform scale about the origin (device-scale root transform: render
44    /// graphs stay in logical dp; density applies at execution).
45    pub fn uniform_scale(scale: f32) -> Self {
46        Self {
47            matrix: [[scale, 0.0, 0.0], [0.0, scale, 0.0], [0.0, 0.0, 1.0]],
48        }
49    }
50
51    /// Maps a rectangle to corners ordered top-left, top-right, bottom-left, bottom-right.
52    pub fn from_rect_to_quad(rect: Rect, quad: [[f32; 2]; 4]) -> Self {
53        if rect.width.abs() <= f32::EPSILON || rect.height.abs() <= f32::EPSILON {
54            return Self::translation(quad[0][0], quad[0][1]);
55        }
56
57        if let Some(axis_aligned) = axis_aligned_rect_from_quad(quad) {
58            let scale_x = axis_aligned.width / rect.width;
59            let scale_y = axis_aligned.height / rect.height;
60            return Self {
61                matrix: [
62                    [scale_x, 0.0, axis_aligned.x - rect.x * scale_x],
63                    [0.0, scale_y, axis_aligned.y - rect.y * scale_y],
64                    [0.0, 0.0, 1.0],
65                ],
66            };
67        }
68
69        let source = [
70            [rect.x, rect.y],
71            [rect.x + rect.width, rect.y],
72            [rect.x, rect.y + rect.height],
73            [rect.x + rect.width, rect.y + rect.height],
74        ];
75        let Some(coefficients) = solve_homography(source, quad) else {
76            return Self::identity();
77        };
78
79        Self {
80            matrix: [
81                [coefficients[0], coefficients[1], coefficients[2]],
82                [coefficients[3], coefficients[4], coefficients[5]],
83                [coefficients[6], coefficients[7], 1.0],
84            ],
85        }
86    }
87
88    /// The transform a homogeneous matrix describes, scaled so its last entry
89    /// is one wherever that entry is not zero: its other entries then read as
90    /// the scale, turn, translation and perspective they are.
91    pub fn from_homogeneous(matrix: [[f32; 3]; 3]) -> Self {
92        let w = matrix[2][2];
93        if w == 1.0 || w.abs() <= f32::EPSILON {
94            return Self { matrix };
95        }
96        Self {
97            matrix: matrix.map(|row| row.map(|value| value / w)),
98        }
99    }
100
101    /// Returns the composed transform that applies `self` first and `next` second.
102    pub fn then(self, next: Self) -> Self {
103        Self {
104            matrix: multiply_matrices(next.matrix, self.matrix),
105        }
106    }
107
108    /// Maps destination coordinates back to the source, unless the matrix is singular.
109    pub fn inverse(self) -> Option<Self> {
110        let m = self.matrix;
111        let a = m[0][0];
112        let b = m[0][1];
113        let c = m[0][2];
114        let d = m[1][0];
115        let e = m[1][1];
116        let f = m[1][2];
117        let g = m[2][0];
118        let h = m[2][1];
119        let i = m[2][2];
120
121        let cofactor00 = e * i - f * h;
122        let cofactor01 = -(d * i - f * g);
123        let cofactor02 = d * h - e * g;
124        let cofactor10 = -(b * i - c * h);
125        let cofactor11 = a * i - c * g;
126        let cofactor12 = -(a * h - b * g);
127        let cofactor20 = b * f - c * e;
128        let cofactor21 = -(a * f - c * d);
129        let cofactor22 = a * e - b * d;
130
131        let determinant = a * cofactor00 + b * cofactor01 + c * cofactor02;
132        if determinant.abs() <= f32::EPSILON {
133            return None;
134        }
135        let inverse_determinant = 1.0 / determinant;
136
137        Some(Self {
138            matrix: [
139                [
140                    cofactor00 * inverse_determinant,
141                    cofactor10 * inverse_determinant,
142                    cofactor20 * inverse_determinant,
143                ],
144                [
145                    cofactor01 * inverse_determinant,
146                    cofactor11 * inverse_determinant,
147                    cofactor21 * inverse_determinant,
148                ],
149                [
150                    cofactor02 * inverse_determinant,
151                    cofactor12 * inverse_determinant,
152                    cofactor22 * inverse_determinant,
153                ],
154            ],
155        })
156    }
157
158    /// Returns the row-major homogeneous matrix.
159    pub fn matrix(self) -> [[f32; 3]; 3] {
160        self.matrix
161    }
162
163    /// Maps a position into destination coordinates.
164    pub fn map_point(self, point: Point) -> Point {
165        let x = point.x;
166        let y = point.y;
167        let w = self.matrix[2][0] * x + self.matrix[2][1] * y + self.matrix[2][2];
168        let safe_w = if w.abs() <= f32::EPSILON { 1.0 } else { w };
169
170        Point {
171            x: (self.matrix[0][0] * x + self.matrix[0][1] * y + self.matrix[0][2]) / safe_w,
172            y: (self.matrix[1][0] * x + self.matrix[1][1] * y + self.matrix[1][2]) / safe_w,
173        }
174    }
175
176    /// Maps a rectangle's top-left, top-right, bottom-left and bottom-right corners.
177    pub fn map_rect(self, rect: Rect) -> [[f32; 2]; 4] {
178        [
179            self.map_point(Point {
180                x: rect.x,
181                y: rect.y,
182            }),
183            self.map_point(Point {
184                x: rect.x + rect.width,
185                y: rect.y,
186            }),
187            self.map_point(Point {
188                x: rect.x,
189                y: rect.y + rect.height,
190            }),
191            self.map_point(Point {
192                x: rect.x + rect.width,
193                y: rect.y + rect.height,
194            }),
195        ]
196        .map(|point| [point.x, point.y])
197    }
198
199    /// Returns the axis-aligned bounds of the transformed rectangle.
200    pub fn bounds_for_rect(self, rect: Rect) -> Rect {
201        quad_bounds(self.map_rect(rect))
202    }
203}
204
205fn axis_aligned_rect_from_quad(quad: [[f32; 2]; 4]) -> Option<Rect> {
206    let top_left = quad[0];
207    let top_right = quad[1];
208    let bottom_left = quad[2];
209    let bottom_right = quad[3];
210    let x_epsilon = 1e-4;
211    let y_epsilon = 1e-4;
212
213    if (top_left[1] - top_right[1]).abs() > y_epsilon
214        || (bottom_left[1] - bottom_right[1]).abs() > y_epsilon
215        || (top_left[0] - bottom_left[0]).abs() > x_epsilon
216        || (top_right[0] - bottom_right[0]).abs() > x_epsilon
217    {
218        return None;
219    }
220
221    Some(Rect {
222        x: top_left[0],
223        y: top_left[1],
224        width: top_right[0] - top_left[0],
225        height: bottom_left[1] - top_left[1],
226    })
227}
228
229impl Default for ProjectiveTransform {
230    fn default() -> Self {
231        Self::identity()
232    }
233}
234
235/// Returns the axis-aligned bounds containing all four points.
236pub fn quad_bounds(quad: [[f32; 2]; 4]) -> Rect {
237    let mut min_x = f32::INFINITY;
238    let mut min_y = f32::INFINITY;
239    let mut max_x = f32::NEG_INFINITY;
240    let mut max_y = f32::NEG_INFINITY;
241
242    for [x, y] in quad {
243        min_x = min_x.min(x);
244        min_y = min_y.min(y);
245        max_x = max_x.max(x);
246        max_y = max_y.max(y);
247    }
248
249    Rect {
250        x: min_x,
251        y: min_y,
252        width: (max_x - min_x).max(0.0),
253        height: (max_y - min_y).max(0.0),
254    }
255}
256
257fn multiply_matrices(lhs: [[f32; 3]; 3], rhs: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
258    let mut out = [[0.0; 3]; 3];
259    for row in 0..3 {
260        for col in 0..3 {
261            out[row][col] =
262                lhs[row][0] * rhs[0][col] + lhs[row][1] * rhs[1][col] + lhs[row][2] * rhs[2][col];
263        }
264    }
265    out
266}
267
268fn solve_homography(source: [[f32; 2]; 4], target: [[f32; 2]; 4]) -> Option<[f32; 8]> {
269    let mut matrix = [[0.0f32; 9]; 8];
270    for (index, (src, dst)) in source.into_iter().zip(target).enumerate() {
271        let row = index * 2;
272        let x = src[0];
273        let y = src[1];
274        let u = dst[0];
275        let v = dst[1];
276
277        matrix[row] = [x, y, 1.0, 0.0, 0.0, 0.0, -u * x, -u * y, u];
278        matrix[row + 1] = [0.0, 0.0, 0.0, x, y, 1.0, -v * x, -v * y, v];
279    }
280
281    for pivot in 0..8 {
282        let mut pivot_row = pivot;
283        let mut pivot_value = matrix[pivot][pivot].abs();
284        let mut candidate = pivot + 1;
285        while candidate < 8 {
286            let candidate_value = matrix[candidate][pivot].abs();
287            if candidate_value > pivot_value {
288                pivot_row = candidate;
289                pivot_value = candidate_value;
290            }
291            candidate += 1;
292        }
293
294        if pivot_value <= f32::EPSILON {
295            return None;
296        }
297
298        if pivot_row != pivot {
299            matrix.swap(pivot, pivot_row);
300        }
301
302        let divisor = matrix[pivot][pivot];
303        let mut col = pivot;
304        while col < 9 {
305            matrix[pivot][col] /= divisor;
306            col += 1;
307        }
308
309        for row in 0..8 {
310            if row == pivot {
311                continue;
312            }
313            let factor = matrix[row][pivot];
314            if factor.abs() <= f32::EPSILON {
315                continue;
316            }
317            let mut col = pivot;
318            while col < 9 {
319                matrix[row][col] -= factor * matrix[pivot][col];
320                col += 1;
321            }
322        }
323    }
324
325    let mut solution = [0.0f32; 8];
326    for index in 0..8 {
327        solution[index] = matrix[index][8];
328    }
329    Some(solution)
330}