Skip to main content

cranpose_render_common/
graph.rs

1use std::rc::Rc;
2
3use cranpose_core::NodeId;
4use cranpose_foundation::PointerEvent;
5use cranpose_ui::text::AnnotatedString;
6use cranpose_ui::{
7    GraphicsLayer, Point, Rect, RenderEffect, RoundedCornerShape, TextLayoutOptions, TextStyle,
8};
9use cranpose_ui_graphics::{BlendMode, ColorFilter, DrawPrimitive};
10
11use crate::raster_cache::LayerRasterCacheHashes;
12
13#[derive(Clone, Copy, Debug, PartialEq)]
14pub struct ProjectiveTransform {
15    matrix: [[f32; 3]; 3],
16}
17
18impl ProjectiveTransform {
19    pub const fn identity() -> Self {
20        Self {
21            matrix: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
22        }
23    }
24
25    pub fn translation(tx: f32, ty: f32) -> Self {
26        Self {
27            matrix: [[1.0, 0.0, tx], [0.0, 1.0, ty], [0.0, 0.0, 1.0]],
28        }
29    }
30
31    pub fn from_rect_to_quad(rect: Rect, quad: [[f32; 2]; 4]) -> Self {
32        if rect.width.abs() <= f32::EPSILON || rect.height.abs() <= f32::EPSILON {
33            return Self::translation(quad[0][0], quad[0][1]);
34        }
35
36        if let Some(axis_aligned) = axis_aligned_rect_from_quad(quad) {
37            let scale_x = axis_aligned.width / rect.width;
38            let scale_y = axis_aligned.height / rect.height;
39            return Self {
40                matrix: [
41                    [scale_x, 0.0, axis_aligned.x - rect.x * scale_x],
42                    [0.0, scale_y, axis_aligned.y - rect.y * scale_y],
43                    [0.0, 0.0, 1.0],
44                ],
45            };
46        }
47
48        let source = [
49            [rect.x, rect.y],
50            [rect.x + rect.width, rect.y],
51            [rect.x, rect.y + rect.height],
52            [rect.x + rect.width, rect.y + rect.height],
53        ];
54        let Some(coefficients) = solve_homography(source, quad) else {
55            return Self::identity();
56        };
57
58        Self {
59            matrix: [
60                [coefficients[0], coefficients[1], coefficients[2]],
61                [coefficients[3], coefficients[4], coefficients[5]],
62                [coefficients[6], coefficients[7], 1.0],
63            ],
64        }
65    }
66
67    /// Returns the composed transform that applies `self` first and `next` second.
68    pub fn then(self, next: Self) -> Self {
69        Self {
70            matrix: multiply_matrices(next.matrix, self.matrix),
71        }
72    }
73
74    pub fn inverse(self) -> Option<Self> {
75        let m = self.matrix;
76        let a = m[0][0];
77        let b = m[0][1];
78        let c = m[0][2];
79        let d = m[1][0];
80        let e = m[1][1];
81        let f = m[1][2];
82        let g = m[2][0];
83        let h = m[2][1];
84        let i = m[2][2];
85
86        let cofactor00 = e * i - f * h;
87        let cofactor01 = -(d * i - f * g);
88        let cofactor02 = d * h - e * g;
89        let cofactor10 = -(b * i - c * h);
90        let cofactor11 = a * i - c * g;
91        let cofactor12 = -(a * h - b * g);
92        let cofactor20 = b * f - c * e;
93        let cofactor21 = -(a * f - c * d);
94        let cofactor22 = a * e - b * d;
95
96        let determinant = a * cofactor00 + b * cofactor01 + c * cofactor02;
97        if determinant.abs() <= f32::EPSILON {
98            return None;
99        }
100        let inverse_determinant = 1.0 / determinant;
101
102        Some(Self {
103            matrix: [
104                [
105                    cofactor00 * inverse_determinant,
106                    cofactor10 * inverse_determinant,
107                    cofactor20 * inverse_determinant,
108                ],
109                [
110                    cofactor01 * inverse_determinant,
111                    cofactor11 * inverse_determinant,
112                    cofactor21 * inverse_determinant,
113                ],
114                [
115                    cofactor02 * inverse_determinant,
116                    cofactor12 * inverse_determinant,
117                    cofactor22 * inverse_determinant,
118                ],
119            ],
120        })
121    }
122
123    pub fn matrix(self) -> [[f32; 3]; 3] {
124        self.matrix
125    }
126
127    pub fn map_point(self, point: Point) -> Point {
128        let x = point.x;
129        let y = point.y;
130        let w = self.matrix[2][0] * x + self.matrix[2][1] * y + self.matrix[2][2];
131        let safe_w = if w.abs() <= f32::EPSILON { 1.0 } else { w };
132
133        Point {
134            x: (self.matrix[0][0] * x + self.matrix[0][1] * y + self.matrix[0][2]) / safe_w,
135            y: (self.matrix[1][0] * x + self.matrix[1][1] * y + self.matrix[1][2]) / safe_w,
136        }
137    }
138
139    pub fn map_rect(self, rect: Rect) -> [[f32; 2]; 4] {
140        [
141            self.map_point(Point {
142                x: rect.x,
143                y: rect.y,
144            }),
145            self.map_point(Point {
146                x: rect.x + rect.width,
147                y: rect.y,
148            }),
149            self.map_point(Point {
150                x: rect.x,
151                y: rect.y + rect.height,
152            }),
153            self.map_point(Point {
154                x: rect.x + rect.width,
155                y: rect.y + rect.height,
156            }),
157        ]
158        .map(|point| [point.x, point.y])
159    }
160
161    pub fn bounds_for_rect(self, rect: Rect) -> Rect {
162        quad_bounds(self.map_rect(rect))
163    }
164}
165
166fn axis_aligned_rect_from_quad(quad: [[f32; 2]; 4]) -> Option<Rect> {
167    let top_left = quad[0];
168    let top_right = quad[1];
169    let bottom_left = quad[2];
170    let bottom_right = quad[3];
171    let x_epsilon = 1e-4;
172    let y_epsilon = 1e-4;
173
174    if (top_left[1] - top_right[1]).abs() > y_epsilon
175        || (bottom_left[1] - bottom_right[1]).abs() > y_epsilon
176        || (top_left[0] - bottom_left[0]).abs() > x_epsilon
177        || (top_right[0] - bottom_right[0]).abs() > x_epsilon
178    {
179        return None;
180    }
181
182    Some(Rect {
183        x: top_left[0],
184        y: top_left[1],
185        width: top_right[0] - top_left[0],
186        height: bottom_left[1] - top_left[1],
187    })
188}
189
190impl Default for ProjectiveTransform {
191    fn default() -> Self {
192        Self::identity()
193    }
194}
195
196#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
197pub struct IsolationReasons {
198    pub explicit_offscreen: bool,
199    pub effect: bool,
200    pub backdrop: bool,
201    pub group_opacity: bool,
202    pub blend_mode: bool,
203}
204
205impl IsolationReasons {
206    pub fn has_any(self) -> bool {
207        self.explicit_offscreen
208            || self.effect
209            || self.backdrop
210            || self.group_opacity
211            || self.blend_mode
212    }
213}
214
215#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
216pub enum CachePolicy {
217    #[default]
218    None,
219    Auto,
220}
221
222#[derive(Clone)]
223pub struct HitTestNode {
224    pub shape: Option<RoundedCornerShape>,
225    pub click_actions: Vec<Rc<dyn Fn(Point)>>,
226    pub pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
227    pub clip: Option<Rect>,
228}
229
230#[derive(Clone, Debug, PartialEq)]
231pub struct DrawPrimitiveNode {
232    pub primitive: DrawPrimitive,
233    pub clip: Option<Rect>,
234}
235
236#[derive(Clone, Debug, PartialEq)]
237pub struct TextPrimitiveNode {
238    pub node_id: NodeId,
239    pub rect: Rect,
240    pub text: AnnotatedString,
241    pub text_style: TextStyle,
242    pub font_size: f32,
243    pub layout_options: TextLayoutOptions,
244    pub clip: Option<Rect>,
245}
246
247#[derive(Clone, Copy, Debug, PartialEq, Eq)]
248pub enum PrimitivePhase {
249    BeforeChildren,
250    AfterChildren,
251}
252
253#[derive(Clone, Debug, PartialEq)]
254pub enum PrimitiveNode {
255    Draw(DrawPrimitiveNode),
256    Text(Box<TextPrimitiveNode>),
257}
258
259#[derive(Clone, Debug, PartialEq)]
260pub struct PrimitiveEntry {
261    pub phase: PrimitivePhase,
262    pub node: PrimitiveNode,
263}
264
265#[derive(Clone)]
266pub struct LayerNode {
267    pub node_id: Option<NodeId>,
268    pub local_bounds: Rect,
269    pub transform_to_parent: ProjectiveTransform,
270    pub motion_context_animated: bool,
271    pub translated_content_context: bool,
272    pub graphics_layer: GraphicsLayer,
273    pub clip_to_bounds: bool,
274    pub shadow_clip: Option<Rect>,
275    pub hit_test: Option<HitTestNode>,
276    pub has_hit_targets: bool,
277    pub isolation: IsolationReasons,
278    pub cache_policy: CachePolicy,
279    pub cache_hashes: LayerRasterCacheHashes,
280    pub cache_hashes_valid: bool,
281    pub children: Vec<RenderNode>,
282}
283
284impl LayerNode {
285    pub fn clip_rect(&self) -> Option<Rect> {
286        (self.clip_to_bounds || self.graphics_layer.clip).then_some(self.local_bounds)
287    }
288
289    pub fn effect(&self) -> Option<&RenderEffect> {
290        self.graphics_layer.render_effect.as_ref()
291    }
292
293    pub fn backdrop(&self) -> Option<&RenderEffect> {
294        self.graphics_layer.backdrop_effect.as_ref()
295    }
296
297    pub fn opacity(&self) -> f32 {
298        self.graphics_layer.alpha
299    }
300
301    pub fn blend_mode(&self) -> BlendMode {
302        self.graphics_layer.blend_mode
303    }
304
305    pub fn color_filter(&self) -> Option<ColorFilter> {
306        self.graphics_layer.color_filter
307    }
308
309    pub fn target_content_hash(&self) -> u64 {
310        if self.cache_hashes_valid {
311            self.cache_hashes.target_content
312        } else {
313            crate::graph_hash::layer_raster_cache_hashes(self).target_content
314        }
315    }
316
317    pub fn effect_hash(&self) -> u64 {
318        if self.cache_hashes_valid {
319            self.cache_hashes.effect
320        } else {
321            crate::graph_hash::layer_raster_cache_hashes(self).effect
322        }
323    }
324
325    pub fn recompute_raster_cache_hashes(&mut self) {
326        crate::graph_hash::recompute_layer_raster_cache_hashes(self);
327    }
328}
329
330#[derive(Clone)]
331pub enum RenderNode {
332    Primitive(PrimitiveEntry),
333    Layer(Box<LayerNode>),
334}
335
336#[derive(Clone)]
337pub struct RenderGraph {
338    pub root: LayerNode,
339}
340
341impl RenderGraph {
342    pub fn new(mut root: LayerNode) -> Self {
343        root.recompute_raster_cache_hashes();
344        Self { root }
345    }
346}
347
348pub fn quad_bounds(quad: [[f32; 2]; 4]) -> Rect {
349    let mut min_x = f32::INFINITY;
350    let mut min_y = f32::INFINITY;
351    let mut max_x = f32::NEG_INFINITY;
352    let mut max_y = f32::NEG_INFINITY;
353
354    for [x, y] in quad {
355        min_x = min_x.min(x);
356        min_y = min_y.min(y);
357        max_x = max_x.max(x);
358        max_y = max_y.max(y);
359    }
360
361    Rect {
362        x: min_x,
363        y: min_y,
364        width: (max_x - min_x).max(0.0),
365        height: (max_y - min_y).max(0.0),
366    }
367}
368
369fn multiply_matrices(lhs: [[f32; 3]; 3], rhs: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
370    let mut out = [[0.0; 3]; 3];
371    for row in 0..3 {
372        for col in 0..3 {
373            out[row][col] =
374                lhs[row][0] * rhs[0][col] + lhs[row][1] * rhs[1][col] + lhs[row][2] * rhs[2][col];
375        }
376    }
377    out
378}
379
380fn solve_homography(source: [[f32; 2]; 4], target: [[f32; 2]; 4]) -> Option<[f32; 8]> {
381    let mut matrix = [[0.0f32; 9]; 8];
382    for (index, (src, dst)) in source.into_iter().zip(target).enumerate() {
383        let row = index * 2;
384        let x = src[0];
385        let y = src[1];
386        let u = dst[0];
387        let v = dst[1];
388
389        matrix[row] = [x, y, 1.0, 0.0, 0.0, 0.0, -u * x, -u * y, u];
390        matrix[row + 1] = [0.0, 0.0, 0.0, x, y, 1.0, -v * x, -v * y, v];
391    }
392
393    for pivot in 0..8 {
394        let mut pivot_row = pivot;
395        let mut pivot_value = matrix[pivot][pivot].abs();
396        let mut candidate = pivot + 1;
397        while candidate < 8 {
398            let candidate_value = matrix[candidate][pivot].abs();
399            if candidate_value > pivot_value {
400                pivot_row = candidate;
401                pivot_value = candidate_value;
402            }
403            candidate += 1;
404        }
405
406        if pivot_value <= f32::EPSILON {
407            return None;
408        }
409
410        if pivot_row != pivot {
411            matrix.swap(pivot, pivot_row);
412        }
413
414        let divisor = matrix[pivot][pivot];
415        let mut col = pivot;
416        while col < 9 {
417            matrix[pivot][col] /= divisor;
418            col += 1;
419        }
420
421        for row in 0..8 {
422            if row == pivot {
423                continue;
424            }
425            let factor = matrix[row][pivot];
426            if factor.abs() <= f32::EPSILON {
427                continue;
428            }
429            let mut col = pivot;
430            while col < 9 {
431                matrix[row][col] -= factor * matrix[pivot][col];
432                col += 1;
433            }
434        }
435    }
436
437    let mut solution = [0.0f32; 8];
438    for index in 0..8 {
439        solution[index] = matrix[index][8];
440    }
441    Some(solution)
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447    use crate::raster_cache::LayerRasterCacheHashes;
448    use cranpose_ui_graphics::{Brush, Color, DrawPrimitive};
449
450    fn test_layer(local_bounds: Rect, children: Vec<RenderNode>) -> LayerNode {
451        LayerNode {
452            node_id: None,
453            local_bounds,
454            transform_to_parent: ProjectiveTransform::identity(),
455            motion_context_animated: false,
456            translated_content_context: false,
457            graphics_layer: GraphicsLayer::default(),
458            clip_to_bounds: false,
459            shadow_clip: None,
460            hit_test: None,
461            has_hit_targets: false,
462            isolation: IsolationReasons::default(),
463            cache_policy: CachePolicy::None,
464            cache_hashes: LayerRasterCacheHashes::default(),
465            cache_hashes_valid: false,
466            children,
467        }
468    }
469
470    #[test]
471    fn projective_transform_translation_maps_points() {
472        let transform = ProjectiveTransform::translation(7.0, -3.5);
473        let mapped = transform.map_point(Point { x: 2.0, y: 4.0 });
474        assert!((mapped.x - 9.0).abs() < 1e-6);
475        assert!((mapped.y - 0.5).abs() < 1e-6);
476    }
477
478    #[test]
479    fn projective_transform_then_composes_in_parent_order() {
480        let child = ProjectiveTransform::translation(4.0, 2.0);
481        let parent = ProjectiveTransform::translation(10.0, -1.0);
482        let composed = child.then(parent);
483        let mapped = composed.map_point(Point { x: 1.0, y: 1.0 });
484        assert!((mapped.x - 15.0).abs() < 1e-6);
485        assert!((mapped.y - 2.0).abs() < 1e-6);
486    }
487
488    #[test]
489    fn homography_maps_rect_corners_to_target_quad() {
490        let rect = Rect {
491            x: 0.0,
492            y: 0.0,
493            width: 20.0,
494            height: 10.0,
495        };
496        let quad = [[5.0, 7.0], [25.0, 6.0], [7.0, 20.0], [28.0, 21.0]];
497        let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
498        let mapped = transform.map_rect(rect);
499        for (expected, actual) in quad.into_iter().zip(mapped) {
500            assert!((expected[0] - actual[0]).abs() < 1e-4);
501            assert!((expected[1] - actual[1]).abs() < 1e-4);
502        }
503    }
504
505    #[test]
506    fn axis_aligned_rect_to_quad_keeps_exact_affine_matrix() {
507        let rect = Rect {
508            x: 2.0,
509            y: 3.0,
510            width: 20.0,
511            height: 10.0,
512        };
513        let quad = [[12.0, 9.0], [32.0, 9.0], [12.0, 19.0], [32.0, 19.0]];
514        let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
515
516        assert_eq!(
517            transform.matrix(),
518            [[1.0, 0.0, 10.0], [0.0, 1.0, 6.0], [0.0, 0.0, 1.0]]
519        );
520    }
521
522    #[test]
523    fn axis_aligned_rect_to_quad_keeps_exact_axis_aligned_scale() {
524        let rect = Rect {
525            x: 4.0,
526            y: 6.0,
527            width: 10.0,
528            height: 8.0,
529        };
530        let quad = [[20.0, 18.0], [50.0, 18.0], [20.0, 42.0], [50.0, 42.0]];
531        let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
532
533        assert_eq!(
534            transform.matrix(),
535            [[3.0, 0.0, 8.0], [0.0, 3.0, 0.0], [0.0, 0.0, 1.0]]
536        );
537    }
538
539    #[test]
540    fn render_graph_new_recomputes_manual_layer_hashes() {
541        let primitive = PrimitiveEntry {
542            phase: PrimitivePhase::BeforeChildren,
543            node: PrimitiveNode::Draw(DrawPrimitiveNode {
544                primitive: DrawPrimitive::Rect {
545                    rect: Rect {
546                        x: 1.0,
547                        y: 2.0,
548                        width: 8.0,
549                        height: 6.0,
550                    },
551                    brush: Brush::solid(Color::WHITE),
552                },
553                clip: None,
554            }),
555        };
556        let mut root = test_layer(
557            Rect {
558                x: 0.0,
559                y: 0.0,
560                width: 20.0,
561                height: 20.0,
562            },
563            vec![RenderNode::Primitive(primitive)],
564        );
565        root.graphics_layer.render_effect = Some(RenderEffect::blur(3.0));
566        let mut expected = root.clone();
567        expected.recompute_raster_cache_hashes();
568
569        let graph = RenderGraph::new(root);
570        assert_eq!(
571            graph.root.target_content_hash(),
572            expected.target_content_hash()
573        );
574        assert_eq!(graph.root.effect_hash(), expected.effect_hash());
575    }
576}