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/// The smallest homogeneous `w` at which a point still maps: a point with a
23/// smaller `w` lies at or behind the eye of a perspective and lands on no
24/// position of the destination plane. A layer's own plane maps at a `w` of
25/// one.
26pub const NEAR_W: f32 = 1.0 / 64.0;
27
28/// A projective mapping of a two-dimensional coordinate plane.
29#[derive(Clone, Copy, Debug)]
30pub struct ProjectiveTransform {
31    matrix: [[f32; 3]; 3],
32}
33
34/// The part of a rectangle in front of a perspective's eye, mapped: a convex
35/// polygon of up to five corners, in order around it. Empty when the whole
36/// rectangle lies behind the eye.
37#[derive(Clone, Copy, Debug, PartialEq)]
38pub struct FrontPolygon {
39    corners: [[f32; 2]; 5],
40    len: u8,
41}
42
43impl FrontPolygon {
44    /// The whole of `rect`, which lies in a plane with no perspective:
45    /// corners top-left, top-right, bottom-right, bottom-left.
46    pub fn of_rect(rect: Rect) -> Self {
47        let (right, bottom) = (rect.x + rect.width, rect.y + rect.height);
48        Self {
49            corners: [
50                [rect.x, rect.y],
51                [right, rect.y],
52                [right, bottom],
53                [rect.x, bottom],
54                [0.0; 2],
55            ],
56            len: 4,
57        }
58    }
59
60    fn push(&mut self, corner: [f32; 2]) {
61        self.corners[usize::from(self.len)] = corner;
62        self.len += 1;
63    }
64
65    /// The corners in order around the polygon.
66    pub fn corners(&self) -> &[[f32; 2]] {
67        &self.corners[..usize::from(self.len)]
68    }
69
70    /// Whether no part of the rectangle lies in front of the eye.
71    pub fn is_empty(&self) -> bool {
72        self.len < 3
73    }
74
75    /// The corners in triangle-strip order: the strip covers the polygon,
76    /// which is convex.
77    pub fn strip(&self) -> [[f32; 2]; 5] {
78        let c = self.corners;
79        match self.len {
80            5 => [c[0], c[1], c[4], c[2], c[3]],
81            4 => [c[0], c[1], c[3], c[2], c[2]],
82            _ => c,
83        }
84    }
85
86    /// Axis-aligned bounds of the polygon; empty at the origin when the
87    /// polygon is.
88    pub fn bounds(&self) -> Rect {
89        if self.is_empty() {
90            return Rect::from_size(Size::ZERO);
91        }
92        let (mut min, mut max) = ([f32::INFINITY; 2], [f32::NEG_INFINITY; 2]);
93        for corner in self.corners() {
94            min = [min[0].min(corner[0]), min[1].min(corner[1])];
95            max = [max[0].max(corner[0]), max[1].max(corner[1])];
96        }
97        Rect {
98            x: min[0],
99            y: min[1],
100            width: (max[0] - min[0]).max(0.0),
101            height: (max[1] - min[1]).max(0.0),
102        }
103    }
104}
105
106impl PartialEq for ProjectiveTransform {
107    fn eq(&self, other: &Self) -> bool {
108        // Entry by entry, as a derived comparison: written out, it compiles
109        // to nine comparisons where the derived one loops over each row.
110        let [[a0, a1, a2], [a3, a4, a5], [a6, a7, a8]] = self.matrix;
111        let [[b0, b1, b2], [b3, b4, b5], [b6, b7, b8]] = other.matrix;
112        a0 == b0
113            && a1 == b1
114            && a2 == b2
115            && a3 == b3
116            && a4 == b4
117            && a5 == b5
118            && a6 == b6
119            && a7 == b7
120            && a8 == b8
121    }
122}
123
124impl ProjectiveTransform {
125    /// Leaves positions unchanged.
126    pub const fn identity() -> Self {
127        Self {
128            matrix: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
129        }
130    }
131
132    /// Translates positions by the supplied logical-pixel offsets.
133    pub fn translation(tx: f32, ty: f32) -> Self {
134        Self {
135            matrix: [[1.0, 0.0, tx], [0.0, 1.0, ty], [0.0, 0.0, 1.0]],
136        }
137    }
138
139    /// Uniform scale about the origin (device-scale root transform: render
140    /// graphs stay in logical dp; density applies at execution).
141    pub fn uniform_scale(scale: f32) -> Self {
142        Self {
143            matrix: [[scale, 0.0, 0.0], [0.0, scale, 0.0], [0.0, 0.0, 1.0]],
144        }
145    }
146
147    /// Maps a rectangle to corners ordered top-left, top-right, bottom-left,
148    /// bottom-right, with the whole rectangle in front of the eye.
149    pub fn from_rect_to_quad(rect: Rect, quad: [[f32; 2]; 4]) -> Self {
150        if rect.width.abs() <= f32::EPSILON || rect.height.abs() <= f32::EPSILON {
151            return Self::translation(quad[0][0], quad[0][1]);
152        }
153
154        if let Some(axis_aligned) = axis_aligned_rect_from_quad(quad) {
155            let scale_x = axis_aligned.width / rect.width;
156            let scale_y = axis_aligned.height / rect.height;
157            return Self {
158                matrix: [
159                    [scale_x, 0.0, axis_aligned.x - rect.x * scale_x],
160                    [0.0, scale_y, axis_aligned.y - rect.y * scale_y],
161                    [0.0, 0.0, 1.0],
162                ],
163            };
164        }
165
166        let source = [
167            [rect.x, rect.y],
168            [rect.x + rect.width, rect.y],
169            [rect.x, rect.y + rect.height],
170            [rect.x + rect.width, rect.y + rect.height],
171        ];
172        let Some(coefficients) = solve_homography(source, quad) else {
173            return Self::identity();
174        };
175
176        let solved = Self {
177            matrix: [
178                [coefficients[0], coefficients[1], coefficients[2]],
179                [coefficients[3], coefficients[4], coefficients[5]],
180                [coefficients[6], coefficients[7], 1.0],
181            ],
182        };
183        if solved.w_at(rect.x + rect.width * 0.5, rect.y + rect.height * 0.5) >= 0.0 {
184            return solved;
185        }
186        Self {
187            matrix: solved.matrix.map(|row| row.map(|value| -value)),
188        }
189    }
190
191    /// The transform of a row-major homogeneous matrix, kept as it is: its
192    /// bottom row gives each point's `w`, which [`NEAR_W`] cuts.
193    pub const fn from_rows(matrix: [[f32; 3]; 3]) -> Self {
194        Self { matrix }
195    }
196
197    /// The transform a homogeneous matrix describes, scaled so its last entry
198    /// is one or minus one wherever that entry is not zero: its other entries
199    /// then read as the scale, turn, translation and perspective they are.
200    /// The scale is positive, so every point keeps the side of the eye its
201    /// homogeneous `w` puts it on.
202    pub fn from_homogeneous(matrix: [[f32; 3]; 3]) -> Self {
203        let w = matrix[2][2].abs();
204        if w == 1.0 || w <= f32::EPSILON {
205            return Self { matrix };
206        }
207        Self {
208            matrix: matrix.map(|row| row.map(|value| value / w)),
209        }
210    }
211
212    /// Returns the composed transform that applies `self` first and `next` second.
213    pub fn then(self, next: Self) -> Self {
214        Self {
215            matrix: multiply_matrices(next.matrix, self.matrix),
216        }
217    }
218
219    /// Maps destination coordinates back to the source, unless the matrix is singular.
220    pub fn inverse(self) -> Option<Self> {
221        let m = self.matrix;
222        let a = m[0][0];
223        let b = m[0][1];
224        let c = m[0][2];
225        let d = m[1][0];
226        let e = m[1][1];
227        let f = m[1][2];
228        let g = m[2][0];
229        let h = m[2][1];
230        let i = m[2][2];
231
232        let cofactor00 = e * i - f * h;
233        let cofactor01 = -(d * i - f * g);
234        let cofactor02 = d * h - e * g;
235        let cofactor10 = -(b * i - c * h);
236        let cofactor11 = a * i - c * g;
237        let cofactor12 = -(a * h - b * g);
238        let cofactor20 = b * f - c * e;
239        let cofactor21 = -(a * f - c * d);
240        let cofactor22 = a * e - b * d;
241
242        let determinant = a * cofactor00 + b * cofactor01 + c * cofactor02;
243        if determinant.abs() <= f32::EPSILON {
244            return None;
245        }
246        let inverse_determinant = 1.0 / determinant;
247
248        Some(Self {
249            matrix: [
250                [
251                    cofactor00 * inverse_determinant,
252                    cofactor10 * inverse_determinant,
253                    cofactor20 * inverse_determinant,
254                ],
255                [
256                    cofactor01 * inverse_determinant,
257                    cofactor11 * inverse_determinant,
258                    cofactor21 * inverse_determinant,
259                ],
260                [
261                    cofactor02 * inverse_determinant,
262                    cofactor12 * inverse_determinant,
263                    cofactor22 * inverse_determinant,
264                ],
265            ],
266        })
267    }
268
269    /// Returns the row-major homogeneous matrix.
270    pub fn matrix(self) -> [[f32; 3]; 3] {
271        self.matrix
272    }
273
274    /// Maps a position into destination coordinates.
275    pub fn map_point(&self, point: Point) -> Point {
276        let x = point.x;
277        let y = point.y;
278        let w = self.matrix[2][0] * x + self.matrix[2][1] * y + self.matrix[2][2];
279        let mapped_x = self.matrix[0][0] * x + self.matrix[0][1] * y + self.matrix[0][2];
280        let mapped_y = self.matrix[1][0] * x + self.matrix[1][1] * y + self.matrix[1][2];
281        // Without perspective `w` is exactly 1, and dividing by it changes no
282        // bit. Targets without a float divider pay a call per division.
283        if w == 1.0 {
284            return Point {
285                x: mapped_x,
286                y: mapped_y,
287            };
288        }
289        let safe_w = if w.abs() <= f32::EPSILON { 1.0 } else { w };
290
291        Point {
292            x: mapped_x / safe_w,
293            y: mapped_y / safe_w,
294        }
295    }
296
297    /// Maps a rectangle's top-left, top-right, bottom-left and bottom-right corners.
298    pub fn map_rect(&self, rect: Rect) -> [[f32; 2]; 4] {
299        let right = rect.x + rect.width;
300        let bottom = rect.y + rect.height;
301        let corner = |x, y| {
302            let point = self.map_point(Point { x, y });
303            [point.x, point.y]
304        };
305        [
306            corner(rect.x, rect.y),
307            corner(right, rect.y),
308            corner(rect.x, bottom),
309            corner(right, bottom),
310        ]
311    }
312
313    fn w_at(&self, x: f32, y: f32) -> f32 {
314        self.matrix[2][0] * x + self.matrix[2][1] * y + self.matrix[2][2]
315    }
316
317    /// Whether `point` lies in front of the eye of the transform's
318    /// perspective, so that it maps to a position: its homogeneous `w` is at
319    /// least [`NEAR_W`]. Always true without perspective.
320    pub fn in_front(&self, point: Point) -> bool {
321        self.w_at(point.x, point.y) >= NEAR_W
322    }
323
324    /// Whether all of `rect` lies in front of the eye.
325    pub fn rect_in_front(&self, rect: Rect) -> bool {
326        let right = rect.x + rect.width;
327        let bottom = rect.y + rect.height;
328        [
329            (rect.x, rect.y),
330            (right, rect.y),
331            (rect.x, bottom),
332            (right, bottom),
333        ]
334        .into_iter()
335        .all(|(x, y)| self.w_at(x, y) >= NEAR_W)
336    }
337
338    /// Maps `point` through this transform, the [`inverse`](Self::inverse) of
339    /// a perspective, back onto the plane the perspective shows; `None` when
340    /// the eye's ray through `point` meets the plane behind the eye or nearer
341    /// than [`NEAR_W`]. The perspective's own `w` at the mapped point is one
342    /// over this transform's `w` at `point`.
343    pub fn map_point_in_front(&self, point: Point) -> Option<Point> {
344        let w = self.w_at(point.x, point.y);
345        (w > 0.0 && w * NEAR_W <= 1.0).then(|| self.map_point(point))
346    }
347
348    /// Maps the part of `rect` in front of the eye: the rectangle cut where
349    /// its homogeneous `w` falls below [`NEAR_W`], corners in order top-left,
350    /// top-right, bottom-right, bottom-left with the cut's corners between.
351    pub fn front_polygon(&self, rect: Rect) -> FrontPolygon {
352        let right = rect.x + rect.width;
353        let bottom = rect.y + rect.height;
354        let around = [
355            [rect.x, rect.y],
356            [right, rect.y],
357            [right, bottom],
358            [rect.x, bottom],
359        ];
360        let mut polygon = FrontPolygon {
361            corners: [[0.0; 2]; 5],
362            len: 0,
363        };
364        let mapped = |[x, y]: [f32; 2]| {
365            let point = self.map_point(Point { x, y });
366            [point.x, point.y]
367        };
368        for (index, &from) in around.iter().enumerate() {
369            let to = around[(index + 1) % around.len()];
370            let (w_from, w_to) = (self.w_at(from[0], from[1]), self.w_at(to[0], to[1]));
371            if w_from >= NEAR_W {
372                polygon.push(mapped(from));
373            }
374            if (w_from >= NEAR_W) != (w_to >= NEAR_W) {
375                let t = (NEAR_W - w_from) / (w_to - w_from);
376                polygon.push(mapped([
377                    from[0] + (to[0] - from[0]) * t,
378                    from[1] + (to[1] - from[1]) * t,
379                ]));
380            }
381        }
382        polygon
383    }
384
385    /// Returns the axis-aligned bounds of the transformed rectangle's part in
386    /// front of the eye; empty when no part is.
387    pub fn bounds_for_rect(&self, rect: Rect) -> Rect {
388        let [[sx, kx, tx], [ky, sy, ty], [px, py, w]] = self.matrix;
389        if kx != 0.0 || ky != 0.0 || px != 0.0 || py != 0.0 || w != 1.0 {
390            return self.turned_bounds_for_rect(rect);
391        }
392        // Scale and translation alone keep a rectangle a rectangle: two
393        // corners bound it.
394        let x0 = sx * rect.x + tx;
395        let x1 = sx * (rect.x + rect.width) + tx;
396        let y0 = sy * rect.y + ty;
397        let y1 = sy * (rect.y + rect.height) + ty;
398        Rect {
399            x: x0.min(x1),
400            y: y0.min(y1),
401            width: (x1 - x0).abs(),
402            height: (y1 - y0).abs(),
403        }
404    }
405
406    /// [`Self::bounds_for_rect`] for a transform that skews, turns or has a
407    /// perspective, kept out of line: a rectangle that stays a rectangle
408    /// pays for one check.
409    #[inline(never)]
410    fn turned_bounds_for_rect(&self, rect: Rect) -> Rect {
411        let [_, _, [px, py, w]] = self.matrix;
412        if px != 0.0 || py != 0.0 || w != 1.0 {
413            return self.front_polygon(rect).bounds();
414        }
415        quad_bounds(self.map_rect(rect))
416    }
417}
418
419fn axis_aligned_rect_from_quad(quad: [[f32; 2]; 4]) -> Option<Rect> {
420    let top_left = quad[0];
421    let top_right = quad[1];
422    let bottom_left = quad[2];
423    let bottom_right = quad[3];
424    let x_epsilon = 1e-4;
425    let y_epsilon = 1e-4;
426
427    if (top_left[1] - top_right[1]).abs() > y_epsilon
428        || (bottom_left[1] - bottom_right[1]).abs() > y_epsilon
429        || (top_left[0] - bottom_left[0]).abs() > x_epsilon
430        || (top_right[0] - bottom_right[0]).abs() > x_epsilon
431    {
432        return None;
433    }
434
435    Some(Rect {
436        x: top_left[0],
437        y: top_left[1],
438        width: top_right[0] - top_left[0],
439        height: bottom_left[1] - top_left[1],
440    })
441}
442
443impl Default for ProjectiveTransform {
444    fn default() -> Self {
445        Self::identity()
446    }
447}
448
449/// Returns the axis-aligned bounds containing all four points.
450pub fn quad_bounds(quad: [[f32; 2]; 4]) -> Rect {
451    // Unrolled: iterating the array by value copies it and steps an
452    // iterator where the compiler optimizes for size.
453    let [[x0, y0], [x1, y1], [x2, y2], [x3, y3]] = quad;
454    let min_x = f32::INFINITY.min(x0).min(x1).min(x2).min(x3);
455    let min_y = f32::INFINITY.min(y0).min(y1).min(y2).min(y3);
456    let max_x = f32::NEG_INFINITY.max(x0).max(x1).max(x2).max(x3);
457    let max_y = f32::NEG_INFINITY.max(y0).max(y1).max(y2).max(y3);
458
459    Rect {
460        x: min_x,
461        y: min_y,
462        width: (max_x - min_x).max(0.0),
463        height: (max_y - min_y).max(0.0),
464    }
465}
466
467fn multiply_matrices(lhs: [[f32; 3]; 3], rhs: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
468    // Built in place: a zeroed matrix filled by loops costs a fill and a
469    // copy where the compiler optimizes for size.
470    let cell = |row: usize, col: usize| {
471        lhs[row][0] * rhs[0][col] + lhs[row][1] * rhs[1][col] + lhs[row][2] * rhs[2][col]
472    };
473    [
474        [cell(0, 0), cell(0, 1), cell(0, 2)],
475        [cell(1, 0), cell(1, 1), cell(1, 2)],
476        [cell(2, 0), cell(2, 1), cell(2, 2)],
477    ]
478}
479
480fn solve_homography(source: [[f32; 2]; 4], target: [[f32; 2]; 4]) -> Option<[f32; 8]> {
481    let mut matrix = [[0.0f32; 9]; 8];
482    for (index, (src, dst)) in source.into_iter().zip(target).enumerate() {
483        let row = index * 2;
484        let x = src[0];
485        let y = src[1];
486        let u = dst[0];
487        let v = dst[1];
488
489        matrix[row] = [x, y, 1.0, 0.0, 0.0, 0.0, -u * x, -u * y, u];
490        matrix[row + 1] = [0.0, 0.0, 0.0, x, y, 1.0, -v * x, -v * y, v];
491    }
492
493    for pivot in 0..8 {
494        let mut pivot_row = pivot;
495        let mut pivot_value = matrix[pivot][pivot].abs();
496        let mut candidate = pivot + 1;
497        while candidate < 8 {
498            let candidate_value = matrix[candidate][pivot].abs();
499            if candidate_value > pivot_value {
500                pivot_row = candidate;
501                pivot_value = candidate_value;
502            }
503            candidate += 1;
504        }
505
506        if pivot_value <= f32::EPSILON {
507            return None;
508        }
509
510        if pivot_row != pivot {
511            matrix.swap(pivot, pivot_row);
512        }
513
514        let divisor = matrix[pivot][pivot];
515        let mut col = pivot;
516        while col < 9 {
517            matrix[pivot][col] /= divisor;
518            col += 1;
519        }
520
521        for row in 0..8 {
522            if row == pivot {
523                continue;
524            }
525            let factor = matrix[row][pivot];
526            if factor.abs() <= f32::EPSILON {
527                continue;
528            }
529            let mut col = pivot;
530            while col < 9 {
531                matrix[row][col] -= factor * matrix[pivot][col];
532                col += 1;
533            }
534        }
535    }
536
537    let mut solution = [0.0f32; 8];
538    for index in 0..8 {
539        solution[index] = matrix[index][8];
540    }
541    Some(solution)
542}