Skip to main content

cranpose_render_common/
graph.rs

1use std::{mem::size_of, ops::Range, rc::Rc};
2
3use cranpose_core::{NodeId, collections::map::HashSet};
4use cranpose_foundation::PointerEvent;
5use cranpose_ui::{
6    GraphicsLayer, Point, Rect, RenderEffect, RoundedCornerShape, TextLayoutOptions, TextStyle,
7    text::AnnotatedString,
8};
9use cranpose_ui_graphics::{
10    BlendMode, ColorFilter, CommandRecording, DrawPrimitive, PointerIcon, RecordingSummary,
11    ShadowPrimitive,
12};
13
14use crate::{raster_cache::LayerRasterCacheHashes, style_shared::DrawPlacement};
15
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub struct ProjectiveTransform {
18    matrix: [[f32; 3]; 3],
19}
20
21impl ProjectiveTransform {
22    pub const fn identity() -> Self {
23        Self {
24            matrix: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
25        }
26    }
27
28    pub fn translation(tx: f32, ty: f32) -> Self {
29        Self {
30            matrix: [[1.0, 0.0, tx], [0.0, 1.0, ty], [0.0, 0.0, 1.0]],
31        }
32    }
33
34    /// Uniform scale about the origin (device-scale root transform: render
35    /// graphs stay in logical dp; density applies at execution).
36    pub fn uniform_scale(scale: f32) -> Self {
37        Self {
38            matrix: [[scale, 0.0, 0.0], [0.0, scale, 0.0], [0.0, 0.0, 1.0]],
39        }
40    }
41
42    pub fn from_rect_to_quad(rect: Rect, quad: [[f32; 2]; 4]) -> Self {
43        if rect.width.abs() <= f32::EPSILON || rect.height.abs() <= f32::EPSILON {
44            return Self::translation(quad[0][0], quad[0][1]);
45        }
46
47        if let Some(axis_aligned) = axis_aligned_rect_from_quad(quad) {
48            let scale_x = axis_aligned.width / rect.width;
49            let scale_y = axis_aligned.height / rect.height;
50            return Self {
51                matrix: [
52                    [scale_x, 0.0, axis_aligned.x - rect.x * scale_x],
53                    [0.0, scale_y, axis_aligned.y - rect.y * scale_y],
54                    [0.0, 0.0, 1.0],
55                ],
56            };
57        }
58
59        let source = [
60            [rect.x, rect.y],
61            [rect.x + rect.width, rect.y],
62            [rect.x, rect.y + rect.height],
63            [rect.x + rect.width, rect.y + rect.height],
64        ];
65        let Some(coefficients) = solve_homography(source, quad) else {
66            return Self::identity();
67        };
68
69        Self {
70            matrix: [
71                [coefficients[0], coefficients[1], coefficients[2]],
72                [coefficients[3], coefficients[4], coefficients[5]],
73                [coefficients[6], coefficients[7], 1.0],
74            ],
75        }
76    }
77
78    /// Returns the composed transform that applies `self` first and `next` second.
79    pub fn then(self, next: Self) -> Self {
80        Self {
81            matrix: multiply_matrices(next.matrix, self.matrix),
82        }
83    }
84
85    pub fn inverse(self) -> Option<Self> {
86        let m = self.matrix;
87        let a = m[0][0];
88        let b = m[0][1];
89        let c = m[0][2];
90        let d = m[1][0];
91        let e = m[1][1];
92        let f = m[1][2];
93        let g = m[2][0];
94        let h = m[2][1];
95        let i = m[2][2];
96
97        let cofactor00 = e * i - f * h;
98        let cofactor01 = -(d * i - f * g);
99        let cofactor02 = d * h - e * g;
100        let cofactor10 = -(b * i - c * h);
101        let cofactor11 = a * i - c * g;
102        let cofactor12 = -(a * h - b * g);
103        let cofactor20 = b * f - c * e;
104        let cofactor21 = -(a * f - c * d);
105        let cofactor22 = a * e - b * d;
106
107        let determinant = a * cofactor00 + b * cofactor01 + c * cofactor02;
108        if determinant.abs() <= f32::EPSILON {
109            return None;
110        }
111        let inverse_determinant = 1.0 / determinant;
112
113        Some(Self {
114            matrix: [
115                [
116                    cofactor00 * inverse_determinant,
117                    cofactor10 * inverse_determinant,
118                    cofactor20 * inverse_determinant,
119                ],
120                [
121                    cofactor01 * inverse_determinant,
122                    cofactor11 * inverse_determinant,
123                    cofactor21 * inverse_determinant,
124                ],
125                [
126                    cofactor02 * inverse_determinant,
127                    cofactor12 * inverse_determinant,
128                    cofactor22 * inverse_determinant,
129                ],
130            ],
131        })
132    }
133
134    pub fn matrix(self) -> [[f32; 3]; 3] {
135        self.matrix
136    }
137
138    pub fn map_point(self, point: Point) -> Point {
139        let x = point.x;
140        let y = point.y;
141        let w = self.matrix[2][0] * x + self.matrix[2][1] * y + self.matrix[2][2];
142        let safe_w = if w.abs() <= f32::EPSILON { 1.0 } else { w };
143
144        Point {
145            x: (self.matrix[0][0] * x + self.matrix[0][1] * y + self.matrix[0][2]) / safe_w,
146            y: (self.matrix[1][0] * x + self.matrix[1][1] * y + self.matrix[1][2]) / safe_w,
147        }
148    }
149
150    pub fn map_rect(self, rect: Rect) -> [[f32; 2]; 4] {
151        [
152            self.map_point(Point {
153                x: rect.x,
154                y: rect.y,
155            }),
156            self.map_point(Point {
157                x: rect.x + rect.width,
158                y: rect.y,
159            }),
160            self.map_point(Point {
161                x: rect.x,
162                y: rect.y + rect.height,
163            }),
164            self.map_point(Point {
165                x: rect.x + rect.width,
166                y: rect.y + rect.height,
167            }),
168        ]
169        .map(|point| [point.x, point.y])
170    }
171
172    pub fn bounds_for_rect(self, rect: Rect) -> Rect {
173        quad_bounds(self.map_rect(rect))
174    }
175}
176
177fn axis_aligned_rect_from_quad(quad: [[f32; 2]; 4]) -> Option<Rect> {
178    let top_left = quad[0];
179    let top_right = quad[1];
180    let bottom_left = quad[2];
181    let bottom_right = quad[3];
182    let x_epsilon = 1e-4;
183    let y_epsilon = 1e-4;
184
185    if (top_left[1] - top_right[1]).abs() > y_epsilon
186        || (bottom_left[1] - bottom_right[1]).abs() > y_epsilon
187        || (top_left[0] - bottom_left[0]).abs() > x_epsilon
188        || (top_right[0] - bottom_right[0]).abs() > x_epsilon
189    {
190        return None;
191    }
192
193    Some(Rect {
194        x: top_left[0],
195        y: top_left[1],
196        width: top_right[0] - top_left[0],
197        height: bottom_left[1] - top_left[1],
198    })
199}
200
201impl Default for ProjectiveTransform {
202    fn default() -> Self {
203        Self::identity()
204    }
205}
206
207#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
208pub struct IsolationReasons {
209    pub explicit_offscreen: bool,
210    pub shape_clip: bool,
211    pub effect: bool,
212    pub backdrop: bool,
213    pub group_opacity: bool,
214    pub blend_mode: bool,
215}
216
217impl IsolationReasons {
218    pub fn has_any(self) -> bool {
219        self.explicit_offscreen
220            || self.shape_clip
221            || self.effect
222            || self.backdrop
223            || self.group_opacity
224            || self.blend_mode
225    }
226}
227
228#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
229pub enum CachePolicy {
230    #[default]
231    None,
232    Auto,
233}
234
235#[derive(Clone)]
236pub struct HitTestNode {
237    pub shape: Option<RoundedCornerShape>,
238    pub click_actions: Vec<Rc<dyn Fn(Point)>>,
239    pub pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
240    /// The pointer's appearance while it hovers this target. A region that only
241    /// names an icon still becomes a hit target, which is how a decorative
242    /// panel carries a cursor without handling clicks.
243    pub pointer_icon: Option<PointerIcon>,
244    pub clip: Option<Rect>,
245}
246
247#[derive(Clone, Debug, PartialEq)]
248pub struct DrawPrimitiveNode {
249    pub primitive: DrawPrimitive,
250    pub clip: Option<Rect>,
251}
252
253#[derive(Clone, Debug, PartialEq)]
254pub struct TextPrimitiveNode {
255    pub node_id: NodeId,
256    pub rect: Rect,
257    /// Shared so the renderer can hand the same allocation to every draw it
258    /// emits for this node instead of deep-copying the string once per emit.
259    pub text: Rc<AnnotatedString>,
260    pub text_style: TextStyle,
261    pub font_size: f32,
262    pub layout_options: TextLayoutOptions,
263    pub clip: Option<Rect>,
264}
265
266#[derive(Clone, Copy, Debug, PartialEq, Eq)]
267pub enum PrimitivePhase {
268    BeforeChildren,
269    AfterChildren,
270}
271
272#[derive(Clone, Debug, PartialEq)]
273pub enum PrimitiveNode {
274    Draw(DrawPrimitiveNode),
275    Text(Box<TextPrimitiveNode>),
276}
277
278#[derive(Clone, Debug, PartialEq)]
279pub struct PrimitiveEntry {
280    pub phase: PrimitivePhase,
281    pub node: PrimitiveNode,
282}
283
284#[derive(Clone)]
285pub struct LayerNode {
286    pub node_id: Option<NodeId>,
287    /// Set on the synthetic layer that holds a node's outer draws, those chained
288    /// before its graphics layer, around that node's own layer. Scene updates
289    /// address the node through this id because the node's layer has none of the
290    /// outer draws and the wrapper has no node id of its own.
291    pub wraps: Option<NodeId>,
292    pub local_bounds: Rect,
293    pub transform_to_parent: ProjectiveTransform,
294    pub content_offset: Point,
295    pub motion_context_animated: bool,
296    pub translated_content_context: bool,
297    pub translated_content_offset: Point,
298    /// Window-space origin this layer's children are placed from, and the
299    /// accumulated ancestor graphics-layer translation, captured during the
300    /// full per-frame scene build. Read back when a dirty subtree is rebuilt in
301    /// isolation (`update_scene_from_applier`) so a scrolling field's window
302    /// origin stays live during a fling even though the ancestor chain is not
303    /// re-walked from the root. Defaults to the identity origin.
304    pub scene_children_origin: Point,
305    pub scene_children_layer_translation: Point,
306    pub graphics_layer: GraphicsLayer,
307    pub clip_to_bounds: bool,
308    pub shadow_clip: Option<Rect>,
309    pub hit_test: Option<HitTestNode>,
310    pub has_hit_targets: bool,
311    /// Whether this subtree publishes live window origins (a text field's
312    /// popup anchor, a scroll container's viewport rect). Those sinks are
313    /// written during a full lowering, so the scroll fast path may translate
314    /// a retained subtree in place only when this is false.
315    pub has_origin_sinks: bool,
316    pub isolation: IsolationReasons,
317    pub cache_policy: CachePolicy,
318    pub cache_hashes: LayerRasterCacheHashes,
319    pub cache_hashes_valid: bool,
320    pub children: Vec<RenderNode>,
321}
322
323impl Default for LayerNode {
324    fn default() -> Self {
325        Self {
326            node_id: None,
327            wraps: None,
328            local_bounds: Rect {
329                x: 0.0,
330                y: 0.0,
331                width: 0.0,
332                height: 0.0,
333            },
334            transform_to_parent: ProjectiveTransform::identity(),
335            content_offset: Point::default(),
336            motion_context_animated: false,
337            translated_content_context: false,
338            translated_content_offset: Point::default(),
339            scene_children_origin: Point::default(),
340            scene_children_layer_translation: Point::default(),
341            graphics_layer: GraphicsLayer::default(),
342            clip_to_bounds: false,
343            shadow_clip: None,
344            hit_test: None,
345            has_hit_targets: false,
346            has_origin_sinks: false,
347            isolation: IsolationReasons::default(),
348            cache_policy: CachePolicy::None,
349            cache_hashes: LayerRasterCacheHashes::default(),
350            cache_hashes_valid: false,
351            children: Vec::new(),
352        }
353    }
354}
355
356impl LayerNode {
357    pub fn clip_rect(&self) -> Option<Rect> {
358        (self.clip_to_bounds || self.graphics_layer.clip).then_some(self.local_bounds)
359    }
360
361    pub fn effect(&self) -> Option<&RenderEffect> {
362        self.graphics_layer.render_effect.as_ref()
363    }
364
365    pub fn backdrop(&self) -> Option<&RenderEffect> {
366        self.graphics_layer.backdrop_effect.as_ref()
367    }
368
369    pub fn opacity(&self) -> f32 {
370        self.graphics_layer.alpha
371    }
372
373    pub fn blend_mode(&self) -> BlendMode {
374        self.graphics_layer.blend_mode
375    }
376
377    pub fn color_filter(&self) -> Option<ColorFilter> {
378        self.graphics_layer.color_filter
379    }
380
381    pub fn target_content_hash(&self) -> u64 {
382        if self.cache_hashes_valid {
383            self.cache_hashes.target_content
384        } else {
385            crate::graph_hash::layer_raster_cache_hashes(self).target_content
386        }
387    }
388
389    pub fn motion_source_content_hash(&self) -> u64 {
390        crate::graph_hash::layer_motion_source_content_hash(self)
391    }
392
393    pub fn effect_hash(&self) -> u64 {
394        if self.cache_hashes_valid {
395            self.cache_hashes.effect
396        } else {
397            crate::graph_hash::layer_raster_cache_hashes(self).effect
398        }
399    }
400
401    pub fn recompute_raster_cache_hashes(&mut self) {
402        crate::graph_hash::recompute_layer_raster_cache_hashes(self);
403    }
404}
405
406#[derive(Clone)]
407pub enum RenderNode {
408    Primitive(PrimitiveEntry),
409    /// A whole draw command's primitives as one node. A heavy canvas records
410    /// thousands of primitives per frame; wrapping each in its own
411    /// [`RenderNode`] made the graph rebuild move every one of them twice and
412    /// free seventeen thousand nodes per frame on a stress scene. The run
413    /// keeps the recorded vector intact — semantically it is exactly that
414    /// many consecutive `Primitive` draw entries with no per-primitive clip.
415    DrawRun(DrawRunNode),
416    Layer(Box<LayerNode>),
417}
418
419/// Stable identity of the draw command a run was recorded from: the layout
420/// node owning the command, the command's index in that node's command list,
421/// and which placement pass produced this run (a `WithContent` command emits
422/// one run per placement, so the pair alone is not unique). Rendering does
423/// not read it yet; it is the key under which retained recording state lives
424/// as retention moves up to the draw-command recorder, and it must survive
425/// recording, graph construction, normalized-scene creation, and renderer
426/// cache lookup unchanged.
427#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
428pub struct DrawCommandId {
429    pub node_id: NodeId,
430    pub command_index: u32,
431    pub placement: DrawPlacement,
432}
433
434#[derive(Clone, Debug, PartialEq)]
435pub struct DrawRunNode {
436    pub phase: PrimitivePhase,
437    /// Which draw command recorded these primitives. `None` only for runs
438    /// with no per-command provenance (hand-built tests).
439    pub command: Option<DrawCommandId>,
440    /// Shared, not owned: the recording registry keyed by [`DrawCommandId`]
441    /// keeps a handle to the same recording, so its buffers survive this
442    /// node being dropped on the next rebuild and the command re-records
443    /// into them. Nothing mutates a recording after construction, which is
444    /// what makes sharing sound.
445    pub recording: Rc<CommandRecording>,
446    /// The segments of the recording this run draws: one placement's part
447    /// of a with-content command, or the whole recording.
448    pub segments: Range<u32>,
449    /// Content facts consumers keep asking per frame, answered while
450    /// recording and never by rescanning.
451    pub summary: DrawRunSummary,
452}
453
454/// What a run contains, answered while recording.
455pub type DrawRunSummary = RecordingSummary;
456
457impl DrawRunNode {
458    pub fn new(phase: PrimitivePhase, primitives: Vec<DrawPrimitive>) -> Self {
459        Self::for_command(phase, None, primitives)
460    }
461
462    pub fn for_command(
463        phase: PrimitivePhase,
464        command: Option<DrawCommandId>,
465        primitives: Vec<DrawPrimitive>,
466    ) -> Self {
467        let recording = CommandRecording::from_primitives(primitives);
468        let segments = recording.all_segments();
469        Self::for_command_shared(phase, command, Rc::new(recording), segments)
470    }
471
472    pub fn for_command_shared(
473        phase: PrimitivePhase,
474        command: Option<DrawCommandId>,
475        recording: Rc<CommandRecording>,
476        segments: Range<u32>,
477    ) -> Self {
478        let summary = recording.summary_in(&segments);
479        Self {
480            phase,
481            command,
482            recording,
483            segments,
484            summary,
485        }
486    }
487
488    /// The run's primitives, materialised from the recording in order.
489    pub fn primitives(&self) -> impl Iterator<Item = DrawPrimitive> + '_ {
490        self.recording.primitives(self.segments.clone())
491    }
492
493    /// The rect every entry of the run can reach.
494    pub fn coverage_rects(&self) -> impl Iterator<Item = Rect> + '_ {
495        self.recording.coverage_rects(self.segments.clone())
496    }
497
498    pub fn len(&self) -> usize {
499        self.recording.len_in(&self.segments)
500    }
501
502    pub fn is_empty(&self) -> bool {
503        self.recording.is_empty_in(&self.segments)
504    }
505}
506
507#[derive(Clone)]
508pub struct RenderGraph {
509    pub root: LayerNode,
510}
511
512impl RenderGraph {
513    pub fn new(mut root: LayerNode) -> Self {
514        root.recompute_raster_cache_hashes();
515        Self { root }
516    }
517
518    pub fn node_count(&self) -> usize {
519        fn count_layer(layer: &LayerNode) -> usize {
520            1 + layer
521                .children
522                .iter()
523                .map(|child| match child {
524                    RenderNode::Primitive(_) => 1,
525                    RenderNode::DrawRun(run) => run.len(),
526                    RenderNode::Layer(child_layer) => count_layer(child_layer),
527                })
528                .sum::<usize>()
529        }
530
531        count_layer(&self.root)
532    }
533
534    pub fn heap_bytes(&self) -> usize {
535        layer_heap_bytes(&self.root)
536    }
537
538    /// Replaces the set with the graph's visual observation owners, retaining its capacity.
539    pub fn collect_retained_visual_observation_nodes(&self, nodes: &mut HashSet<NodeId>) {
540        fn collect(layer: &LayerNode, nodes: &mut HashSet<NodeId>) {
541            if let Some(node_id) = layer.node_id {
542                nodes.insert(node_id);
543            }
544            for child in &layer.children {
545                match child {
546                    RenderNode::DrawRun(run) => {
547                        if let Some(command) = run.command {
548                            nodes.insert(command.node_id);
549                        }
550                    }
551                    RenderNode::Layer(child) => collect(child, nodes),
552                    RenderNode::Primitive(_) => {}
553                }
554            }
555        }
556
557        nodes.clear();
558        collect(&self.root, nodes);
559    }
560}
561
562fn layer_heap_bytes(layer: &LayerNode) -> usize {
563    layer.hit_test.as_ref().map_or(0, hit_test_heap_bytes)
564        + size_of::<RenderNode>() * layer.children.capacity()
565        + layer
566            .children
567            .iter()
568            .map(render_node_heap_bytes)
569            .sum::<usize>()
570}
571
572fn render_node_heap_bytes(node: &RenderNode) -> usize {
573    match node {
574        RenderNode::Primitive(entry) => primitive_entry_heap_bytes(entry),
575        RenderNode::DrawRun(run) => {
576            run.recording.pod_heap_bytes()
577                + std::mem::size_of_val(run.recording.others())
578                + run
579                    .recording
580                    .others()
581                    .iter()
582                    .map(draw_primitive_heap_bytes)
583                    .sum::<usize>()
584        }
585        RenderNode::Layer(layer) => size_of::<LayerNode>() + layer_heap_bytes(layer),
586    }
587}
588
589fn primitive_entry_heap_bytes(entry: &PrimitiveEntry) -> usize {
590    match &entry.node {
591        PrimitiveNode::Draw(draw) => draw_primitive_heap_bytes(&draw.primitive),
592        PrimitiveNode::Text(text) => {
593            size_of::<TextPrimitiveNode>() + annotated_string_heap_bytes(&text.text)
594        }
595    }
596}
597
598fn draw_primitive_heap_bytes(primitive: &DrawPrimitive) -> usize {
599    match primitive {
600        DrawPrimitive::Content
601        | DrawPrimitive::Rect { .. }
602        | DrawPrimitive::RoundRect { .. }
603        | DrawPrimitive::Arc { .. } => 0,
604        DrawPrimitive::Blend { primitive, .. } => {
605            size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(primitive)
606        }
607        DrawPrimitive::Image { .. } => 0,
608        DrawPrimitive::Text(text) => {
609            size_of::<cranpose_ui_graphics::TextPrimitive>()
610                + text.text.len()
611                + text
612                    .style
613                    .font_family
614                    .as_ref()
615                    .map_or(0, |family| family.capacity())
616        }
617        DrawPrimitive::Shadow(shadow) => shadow_primitive_heap_bytes(shadow),
618    }
619}
620
621fn shadow_primitive_heap_bytes(shadow: &ShadowPrimitive) -> usize {
622    match shadow {
623        ShadowPrimitive::Drop { shape, .. } => {
624            size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(shape)
625        }
626        ShadowPrimitive::Inner { fill, cutout, .. } => {
627            size_of::<DrawPrimitive>() * 2
628                + draw_primitive_heap_bytes(fill)
629                + draw_primitive_heap_bytes(cutout)
630        }
631    }
632}
633
634fn annotated_string_heap_bytes(text: &AnnotatedString) -> usize {
635    text.text.capacity()
636        + text.span_styles.capacity() * size_of::<usize>() * 2
637        + text.paragraph_styles.capacity() * size_of::<usize>() * 2
638        + text.string_annotations.capacity() * size_of::<usize>() * 2
639        + text.link_annotations.capacity() * size_of::<usize>() * 2
640        + text
641            .string_annotations
642            .iter()
643            .map(|annotation| {
644                annotation.item.tag.capacity() + annotation.item.annotation.capacity()
645            })
646            .sum::<usize>()
647        + text
648            .link_annotations
649            .iter()
650            .map(|annotation| match &annotation.item {
651                cranpose_ui::text::LinkAnnotation::Url(url) => url.capacity(),
652                cranpose_ui::text::LinkAnnotation::Clickable { tag, .. } => tag.capacity(),
653            })
654            .sum::<usize>()
655}
656
657fn hit_test_heap_bytes(hit_test: &HitTestNode) -> usize {
658    hit_test.click_actions.capacity() * size_of::<Rc<dyn Fn(Point)>>()
659        + hit_test.pointer_inputs.capacity() * size_of::<Rc<dyn Fn(PointerEvent)>>()
660}
661
662pub fn quad_bounds(quad: [[f32; 2]; 4]) -> Rect {
663    let mut min_x = f32::INFINITY;
664    let mut min_y = f32::INFINITY;
665    let mut max_x = f32::NEG_INFINITY;
666    let mut max_y = f32::NEG_INFINITY;
667
668    for [x, y] in quad {
669        min_x = min_x.min(x);
670        min_y = min_y.min(y);
671        max_x = max_x.max(x);
672        max_y = max_y.max(y);
673    }
674
675    Rect {
676        x: min_x,
677        y: min_y,
678        width: (max_x - min_x).max(0.0),
679        height: (max_y - min_y).max(0.0),
680    }
681}
682
683fn multiply_matrices(lhs: [[f32; 3]; 3], rhs: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
684    let mut out = [[0.0; 3]; 3];
685    for row in 0..3 {
686        for col in 0..3 {
687            out[row][col] =
688                lhs[row][0] * rhs[0][col] + lhs[row][1] * rhs[1][col] + lhs[row][2] * rhs[2][col];
689        }
690    }
691    out
692}
693
694fn solve_homography(source: [[f32; 2]; 4], target: [[f32; 2]; 4]) -> Option<[f32; 8]> {
695    let mut matrix = [[0.0f32; 9]; 8];
696    for (index, (src, dst)) in source.into_iter().zip(target).enumerate() {
697        let row = index * 2;
698        let x = src[0];
699        let y = src[1];
700        let u = dst[0];
701        let v = dst[1];
702
703        matrix[row] = [x, y, 1.0, 0.0, 0.0, 0.0, -u * x, -u * y, u];
704        matrix[row + 1] = [0.0, 0.0, 0.0, x, y, 1.0, -v * x, -v * y, v];
705    }
706
707    for pivot in 0..8 {
708        let mut pivot_row = pivot;
709        let mut pivot_value = matrix[pivot][pivot].abs();
710        let mut candidate = pivot + 1;
711        while candidate < 8 {
712            let candidate_value = matrix[candidate][pivot].abs();
713            if candidate_value > pivot_value {
714                pivot_row = candidate;
715                pivot_value = candidate_value;
716            }
717            candidate += 1;
718        }
719
720        if pivot_value <= f32::EPSILON {
721            return None;
722        }
723
724        if pivot_row != pivot {
725            matrix.swap(pivot, pivot_row);
726        }
727
728        let divisor = matrix[pivot][pivot];
729        let mut col = pivot;
730        while col < 9 {
731            matrix[pivot][col] /= divisor;
732            col += 1;
733        }
734
735        for row in 0..8 {
736            if row == pivot {
737                continue;
738            }
739            let factor = matrix[row][pivot];
740            if factor.abs() <= f32::EPSILON {
741                continue;
742            }
743            let mut col = pivot;
744            while col < 9 {
745                matrix[row][col] -= factor * matrix[pivot][col];
746                col += 1;
747            }
748        }
749    }
750
751    let mut solution = [0.0f32; 8];
752    for index in 0..8 {
753        solution[index] = matrix[index][8];
754    }
755    Some(solution)
756}
757
758#[cfg(test)]
759mod tests {
760    use cranpose_ui_graphics::{Brush, Color, DrawPrimitive};
761
762    use super::*;
763
764    fn test_layer(local_bounds: Rect, children: Vec<RenderNode>) -> LayerNode {
765        LayerNode {
766            local_bounds,
767            children,
768            ..Default::default()
769        }
770    }
771
772    #[test]
773    fn projective_transform_translation_maps_points() {
774        let transform = ProjectiveTransform::translation(7.0, -3.5);
775        let mapped = transform.map_point(Point { x: 2.0, y: 4.0 });
776        assert!((mapped.x - 9.0).abs() < 1e-6);
777        assert!((mapped.y - 0.5).abs() < 1e-6);
778    }
779
780    #[test]
781    fn projective_transform_then_composes_in_parent_order() {
782        let child = ProjectiveTransform::translation(4.0, 2.0);
783        let parent = ProjectiveTransform::translation(10.0, -1.0);
784        let composed = child.then(parent);
785        let mapped = composed.map_point(Point { x: 1.0, y: 1.0 });
786        assert!((mapped.x - 15.0).abs() < 1e-6);
787        assert!((mapped.y - 2.0).abs() < 1e-6);
788    }
789
790    #[test]
791    fn homography_maps_rect_corners_to_target_quad() {
792        let rect = Rect {
793            x: 0.0,
794            y: 0.0,
795            width: 20.0,
796            height: 10.0,
797        };
798        let quad = [[5.0, 7.0], [25.0, 6.0], [7.0, 20.0], [28.0, 21.0]];
799        let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
800        let mapped = transform.map_rect(rect);
801        for (expected, actual) in quad.into_iter().zip(mapped) {
802            assert!((expected[0] - actual[0]).abs() < 1e-4);
803            assert!((expected[1] - actual[1]).abs() < 1e-4);
804        }
805    }
806
807    #[test]
808    fn axis_aligned_rect_to_quad_keeps_exact_affine_matrix() {
809        let rect = Rect {
810            x: 2.0,
811            y: 3.0,
812            width: 20.0,
813            height: 10.0,
814        };
815        let quad = [[12.0, 9.0], [32.0, 9.0], [12.0, 19.0], [32.0, 19.0]];
816        let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
817
818        assert_eq!(
819            transform.matrix(),
820            [[1.0, 0.0, 10.0], [0.0, 1.0, 6.0], [0.0, 0.0, 1.0]]
821        );
822    }
823
824    #[test]
825    fn axis_aligned_rect_to_quad_keeps_exact_axis_aligned_scale() {
826        let rect = Rect {
827            x: 4.0,
828            y: 6.0,
829            width: 10.0,
830            height: 8.0,
831        };
832        let quad = [[20.0, 18.0], [50.0, 18.0], [20.0, 42.0], [50.0, 42.0]];
833        let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
834
835        assert_eq!(
836            transform.matrix(),
837            [[3.0, 0.0, 8.0], [0.0, 3.0, 0.0], [0.0, 0.0, 1.0]]
838        );
839    }
840
841    #[test]
842    fn retained_visual_observation_nodes_collect_layers_and_command_owners() {
843        let bounds = Rect {
844            x: 0.0,
845            y: 0.0,
846            width: 20.0,
847            height: 20.0,
848        };
849        let command = |node_id| DrawCommandId {
850            node_id,
851            command_index: 0,
852            placement: DrawPlacement::Behind,
853        };
854        let mut child = test_layer(
855            bounds,
856            vec![RenderNode::DrawRun(DrawRunNode::for_command(
857                PrimitivePhase::BeforeChildren,
858                Some(command(17)),
859                Vec::new(),
860            ))],
861        );
862        child.node_id = Some(13);
863        let mut root = test_layer(
864            bounds,
865            vec![
866                RenderNode::DrawRun(DrawRunNode::for_command(
867                    PrimitivePhase::BeforeChildren,
868                    Some(command(9)),
869                    Vec::new(),
870                )),
871                RenderNode::DrawRun(DrawRunNode::new(PrimitivePhase::BeforeChildren, Vec::new())),
872                RenderNode::Layer(Box::new(child)),
873            ],
874        );
875
876        root.node_id = Some(5);
877
878        let mut graph = RenderGraph::new(root);
879        let mut nodes = HashSet::from([999]);
880        graph.collect_retained_visual_observation_nodes(&mut nodes);
881        assert_eq!(nodes, HashSet::from([5, 9, 13, 17]));
882        let capacity = nodes.capacity();
883        graph.root.children.clear();
884        graph.root.node_id = Some(23);
885        graph.collect_retained_visual_observation_nodes(&mut nodes);
886        assert_eq!(nodes, HashSet::from([23]));
887        assert_eq!(nodes.capacity(), capacity);
888    }
889
890    #[test]
891    fn render_graph_new_recomputes_manual_layer_hashes() {
892        let primitive = PrimitiveEntry {
893            phase: PrimitivePhase::BeforeChildren,
894            node: PrimitiveNode::Draw(DrawPrimitiveNode {
895                primitive: DrawPrimitive::Rect {
896                    rect: Rect {
897                        x: 1.0,
898                        y: 2.0,
899                        width: 8.0,
900                        height: 6.0,
901                    },
902                    brush: Brush::solid(Color::WHITE),
903                    stroke: None,
904                },
905                clip: None,
906            }),
907        };
908        let mut root = test_layer(
909            Rect {
910                x: 0.0,
911                y: 0.0,
912                width: 20.0,
913                height: 20.0,
914            },
915            vec![RenderNode::Primitive(primitive)],
916        );
917        root.graphics_layer.render_effect = Some(RenderEffect::blur(3.0));
918        let mut expected = root.clone();
919        expected.recompute_raster_cache_hashes();
920
921        let graph = RenderGraph::new(root);
922        assert_eq!(
923            graph.root.target_content_hash(),
924            expected.target_content_hash()
925        );
926        assert_eq!(graph.root.effect_hash(), expected.effect_hash());
927    }
928
929    #[test]
930    fn motion_source_content_hash_ignores_translated_content_offset() {
931        let primitive = PrimitiveEntry {
932            phase: PrimitivePhase::BeforeChildren,
933            node: PrimitiveNode::Draw(DrawPrimitiveNode {
934                primitive: DrawPrimitive::Rect {
935                    rect: Rect {
936                        x: 1.0,
937                        y: 2.0,
938                        width: 8.0,
939                        height: 6.0,
940                    },
941                    brush: Brush::solid(Color::WHITE),
942                    stroke: None,
943                },
944                clip: None,
945            }),
946        };
947        let mut base = test_layer(
948            Rect {
949                x: 0.0,
950                y: 0.0,
951                width: 20.0,
952                height: 20.0,
953            },
954            vec![RenderNode::Primitive(primitive)],
955        );
956        base.translated_content_context = true;
957        base.translated_content_offset = Point::new(0.0, -24.0);
958        base.recompute_raster_cache_hashes();
959
960        let mut moved = base.clone();
961        moved.translated_content_offset = Point::new(0.0, -72.0);
962        moved.recompute_raster_cache_hashes();
963
964        assert_ne!(base.target_content_hash(), moved.target_content_hash());
965        assert_eq!(
966            base.motion_source_content_hash(),
967            moved.motion_source_content_hash()
968        );
969    }
970}