1use std::{collections::HashSet, mem::size_of, rc::Rc};
2
3use cranpose_core::NodeId;
4use cranpose_foundation::PointerEvent;
5use cranpose_ui::{
6 GraphicsLayer, Point, Rect, RenderEffect, RoundedCornerShape, TextLayoutOptions, TextStyle,
7 text::AnnotatedString,
8};
9use cranpose_ui_graphics::{BlendMode, ColorFilter, DrawPrimitive, ShadowPrimitive};
10
11use crate::{raster_cache::LayerRasterCacheHashes, style_shared::DrawPlacement};
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 uniform_scale(scale: f32) -> Self {
34 Self {
35 matrix: [[scale, 0.0, 0.0], [0.0, scale, 0.0], [0.0, 0.0, 1.0]],
36 }
37 }
38
39 pub fn from_rect_to_quad(rect: Rect, quad: [[f32; 2]; 4]) -> Self {
40 if rect.width.abs() <= f32::EPSILON || rect.height.abs() <= f32::EPSILON {
41 return Self::translation(quad[0][0], quad[0][1]);
42 }
43
44 if let Some(axis_aligned) = axis_aligned_rect_from_quad(quad) {
45 let scale_x = axis_aligned.width / rect.width;
46 let scale_y = axis_aligned.height / rect.height;
47 return Self {
48 matrix: [
49 [scale_x, 0.0, axis_aligned.x - rect.x * scale_x],
50 [0.0, scale_y, axis_aligned.y - rect.y * scale_y],
51 [0.0, 0.0, 1.0],
52 ],
53 };
54 }
55
56 let source = [
57 [rect.x, rect.y],
58 [rect.x + rect.width, rect.y],
59 [rect.x, rect.y + rect.height],
60 [rect.x + rect.width, rect.y + rect.height],
61 ];
62 let Some(coefficients) = solve_homography(source, quad) else {
63 return Self::identity();
64 };
65
66 Self {
67 matrix: [
68 [coefficients[0], coefficients[1], coefficients[2]],
69 [coefficients[3], coefficients[4], coefficients[5]],
70 [coefficients[6], coefficients[7], 1.0],
71 ],
72 }
73 }
74
75 pub fn then(self, next: Self) -> Self {
77 Self {
78 matrix: multiply_matrices(next.matrix, self.matrix),
79 }
80 }
81
82 pub fn inverse(self) -> Option<Self> {
83 let m = self.matrix;
84 let a = m[0][0];
85 let b = m[0][1];
86 let c = m[0][2];
87 let d = m[1][0];
88 let e = m[1][1];
89 let f = m[1][2];
90 let g = m[2][0];
91 let h = m[2][1];
92 let i = m[2][2];
93
94 let cofactor00 = e * i - f * h;
95 let cofactor01 = -(d * i - f * g);
96 let cofactor02 = d * h - e * g;
97 let cofactor10 = -(b * i - c * h);
98 let cofactor11 = a * i - c * g;
99 let cofactor12 = -(a * h - b * g);
100 let cofactor20 = b * f - c * e;
101 let cofactor21 = -(a * f - c * d);
102 let cofactor22 = a * e - b * d;
103
104 let determinant = a * cofactor00 + b * cofactor01 + c * cofactor02;
105 if determinant.abs() <= f32::EPSILON {
106 return None;
107 }
108 let inverse_determinant = 1.0 / determinant;
109
110 Some(Self {
111 matrix: [
112 [
113 cofactor00 * inverse_determinant,
114 cofactor10 * inverse_determinant,
115 cofactor20 * inverse_determinant,
116 ],
117 [
118 cofactor01 * inverse_determinant,
119 cofactor11 * inverse_determinant,
120 cofactor21 * inverse_determinant,
121 ],
122 [
123 cofactor02 * inverse_determinant,
124 cofactor12 * inverse_determinant,
125 cofactor22 * inverse_determinant,
126 ],
127 ],
128 })
129 }
130
131 pub fn matrix(self) -> [[f32; 3]; 3] {
132 self.matrix
133 }
134
135 pub fn map_point(self, point: Point) -> Point {
136 let x = point.x;
137 let y = point.y;
138 let w = self.matrix[2][0] * x + self.matrix[2][1] * y + self.matrix[2][2];
139 let safe_w = if w.abs() <= f32::EPSILON { 1.0 } else { w };
140
141 Point {
142 x: (self.matrix[0][0] * x + self.matrix[0][1] * y + self.matrix[0][2]) / safe_w,
143 y: (self.matrix[1][0] * x + self.matrix[1][1] * y + self.matrix[1][2]) / safe_w,
144 }
145 }
146
147 pub fn map_rect(self, rect: Rect) -> [[f32; 2]; 4] {
148 [
149 self.map_point(Point {
150 x: rect.x,
151 y: rect.y,
152 }),
153 self.map_point(Point {
154 x: rect.x + rect.width,
155 y: rect.y,
156 }),
157 self.map_point(Point {
158 x: rect.x,
159 y: rect.y + rect.height,
160 }),
161 self.map_point(Point {
162 x: rect.x + rect.width,
163 y: rect.y + rect.height,
164 }),
165 ]
166 .map(|point| [point.x, point.y])
167 }
168
169 pub fn bounds_for_rect(self, rect: Rect) -> Rect {
170 quad_bounds(self.map_rect(rect))
171 }
172}
173
174fn axis_aligned_rect_from_quad(quad: [[f32; 2]; 4]) -> Option<Rect> {
175 let top_left = quad[0];
176 let top_right = quad[1];
177 let bottom_left = quad[2];
178 let bottom_right = quad[3];
179 let x_epsilon = 1e-4;
180 let y_epsilon = 1e-4;
181
182 if (top_left[1] - top_right[1]).abs() > y_epsilon
183 || (bottom_left[1] - bottom_right[1]).abs() > y_epsilon
184 || (top_left[0] - bottom_left[0]).abs() > x_epsilon
185 || (top_right[0] - bottom_right[0]).abs() > x_epsilon
186 {
187 return None;
188 }
189
190 Some(Rect {
191 x: top_left[0],
192 y: top_left[1],
193 width: top_right[0] - top_left[0],
194 height: bottom_left[1] - top_left[1],
195 })
196}
197
198impl Default for ProjectiveTransform {
199 fn default() -> Self {
200 Self::identity()
201 }
202}
203
204#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
205pub struct IsolationReasons {
206 pub explicit_offscreen: bool,
207 pub shape_clip: bool,
208 pub effect: bool,
209 pub backdrop: bool,
210 pub group_opacity: bool,
211 pub blend_mode: bool,
212}
213
214impl IsolationReasons {
215 pub fn has_any(self) -> bool {
216 self.explicit_offscreen
217 || self.shape_clip
218 || self.effect
219 || self.backdrop
220 || self.group_opacity
221 || self.blend_mode
222 }
223}
224
225#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
226pub enum CachePolicy {
227 #[default]
228 None,
229 Auto,
230}
231
232#[derive(Clone)]
233pub struct HitTestNode {
234 pub shape: Option<RoundedCornerShape>,
235 pub click_actions: Vec<Rc<dyn Fn(Point)>>,
236 pub pointer_inputs: Vec<Rc<dyn Fn(PointerEvent)>>,
237 pub clip: Option<Rect>,
238}
239
240#[derive(Clone, Debug, PartialEq)]
241pub struct DrawPrimitiveNode {
242 pub primitive: DrawPrimitive,
243 pub clip: Option<Rect>,
244}
245
246#[derive(Clone, Debug, PartialEq)]
247pub struct TextPrimitiveNode {
248 pub node_id: NodeId,
249 pub rect: Rect,
250 pub text: Rc<AnnotatedString>,
253 pub text_style: TextStyle,
254 pub font_size: f32,
255 pub layout_options: TextLayoutOptions,
256 pub clip: Option<Rect>,
257}
258
259#[derive(Clone, Copy, Debug, PartialEq, Eq)]
260pub enum PrimitivePhase {
261 BeforeChildren,
262 AfterChildren,
263}
264
265#[derive(Clone, Debug, PartialEq)]
266pub enum PrimitiveNode {
267 Draw(DrawPrimitiveNode),
268 Text(Box<TextPrimitiveNode>),
269}
270
271#[derive(Clone, Debug, PartialEq)]
272pub struct PrimitiveEntry {
273 pub phase: PrimitivePhase,
274 pub node: PrimitiveNode,
275}
276
277#[derive(Clone)]
278pub struct LayerNode {
279 pub node_id: Option<NodeId>,
280 pub wraps: Option<NodeId>,
285 pub local_bounds: Rect,
286 pub transform_to_parent: ProjectiveTransform,
287 pub content_offset: Point,
288 pub motion_context_animated: bool,
289 pub translated_content_context: bool,
290 pub translated_content_offset: Point,
291 pub scene_children_origin: Point,
298 pub scene_children_layer_translation: Point,
299 pub graphics_layer: GraphicsLayer,
300 pub clip_to_bounds: bool,
301 pub shadow_clip: Option<Rect>,
302 pub hit_test: Option<HitTestNode>,
303 pub has_hit_targets: bool,
304 pub has_origin_sinks: bool,
309 pub isolation: IsolationReasons,
310 pub cache_policy: CachePolicy,
311 pub cache_hashes: LayerRasterCacheHashes,
312 pub cache_hashes_valid: bool,
313 pub children: Vec<RenderNode>,
314}
315
316impl Default for LayerNode {
317 fn default() -> Self {
318 Self {
319 node_id: None,
320 wraps: None,
321 local_bounds: Rect {
322 x: 0.0,
323 y: 0.0,
324 width: 0.0,
325 height: 0.0,
326 },
327 transform_to_parent: ProjectiveTransform::identity(),
328 content_offset: Point::default(),
329 motion_context_animated: false,
330 translated_content_context: false,
331 translated_content_offset: Point::default(),
332 scene_children_origin: Point::default(),
333 scene_children_layer_translation: Point::default(),
334 graphics_layer: GraphicsLayer::default(),
335 clip_to_bounds: false,
336 shadow_clip: None,
337 hit_test: None,
338 has_hit_targets: false,
339 has_origin_sinks: false,
340 isolation: IsolationReasons::default(),
341 cache_policy: CachePolicy::None,
342 cache_hashes: LayerRasterCacheHashes::default(),
343 cache_hashes_valid: false,
344 children: Vec::new(),
345 }
346 }
347}
348
349impl LayerNode {
350 pub fn clip_rect(&self) -> Option<Rect> {
351 (self.clip_to_bounds || self.graphics_layer.clip).then_some(self.local_bounds)
352 }
353
354 pub fn effect(&self) -> Option<&RenderEffect> {
355 self.graphics_layer.render_effect.as_ref()
356 }
357
358 pub fn backdrop(&self) -> Option<&RenderEffect> {
359 self.graphics_layer.backdrop_effect.as_ref()
360 }
361
362 pub fn opacity(&self) -> f32 {
363 self.graphics_layer.alpha
364 }
365
366 pub fn blend_mode(&self) -> BlendMode {
367 self.graphics_layer.blend_mode
368 }
369
370 pub fn color_filter(&self) -> Option<ColorFilter> {
371 self.graphics_layer.color_filter
372 }
373
374 pub fn target_content_hash(&self) -> u64 {
375 if self.cache_hashes_valid {
376 self.cache_hashes.target_content
377 } else {
378 crate::graph_hash::layer_raster_cache_hashes(self).target_content
379 }
380 }
381
382 pub fn motion_source_content_hash(&self) -> u64 {
383 crate::graph_hash::layer_motion_source_content_hash(self)
384 }
385
386 pub fn effect_hash(&self) -> u64 {
387 if self.cache_hashes_valid {
388 self.cache_hashes.effect
389 } else {
390 crate::graph_hash::layer_raster_cache_hashes(self).effect
391 }
392 }
393
394 pub fn recompute_raster_cache_hashes(&mut self) {
395 crate::graph_hash::recompute_layer_raster_cache_hashes(self);
396 }
397}
398
399#[derive(Clone)]
400pub enum RenderNode {
401 Primitive(PrimitiveEntry),
402 DrawRun(DrawRunNode),
409 Layer(Box<LayerNode>),
410}
411
412#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
421pub struct DrawCommandId {
422 pub node_id: NodeId,
423 pub command_index: u32,
424 pub placement: DrawPlacement,
425}
426
427#[derive(Clone, Debug, PartialEq)]
428pub struct DrawRunNode {
429 pub phase: PrimitivePhase,
430 pub command: Option<DrawCommandId>,
433 pub primitives: std::rc::Rc<Vec<DrawPrimitive>>,
439 pub summary: DrawRunSummary,
445 pub replay: Option<Box<cranpose_ui_graphics::CommandReplayFrame>>,
452}
453
454impl DrawRunNode {
455 pub fn new(phase: PrimitivePhase, primitives: Vec<DrawPrimitive>) -> Self {
456 Self::for_command(phase, None, primitives)
457 }
458
459 pub fn for_command(
460 phase: PrimitivePhase,
461 command: Option<DrawCommandId>,
462 primitives: Vec<DrawPrimitive>,
463 ) -> Self {
464 Self::for_command_shared(phase, command, std::rc::Rc::new(primitives))
465 }
466
467 pub fn for_command_shared(
468 phase: PrimitivePhase,
469 command: Option<DrawCommandId>,
470 primitives: std::rc::Rc<Vec<DrawPrimitive>>,
471 ) -> Self {
472 Self::for_command_replayed(phase, command, primitives, None)
473 }
474
475 pub fn for_command_replayed(
476 phase: PrimitivePhase,
477 command: Option<DrawCommandId>,
478 primitives: std::rc::Rc<Vec<DrawPrimitive>>,
479 replay: Option<Box<cranpose_ui_graphics::CommandReplayFrame>>,
480 ) -> Self {
481 debug_assert!(
482 replay.as_ref().is_none_or(|frame| {
483 frame.fallback.is_some()
484 || !frame.spans.iter().any(|span| {
485 matches!(
486 span,
487 cranpose_ui_graphics::FrameSpan::Retained {
488 capture: false,
489 range,
490 ..
491 } if range.1 <= range.0
492 )
493 })
494 }),
495 "a replay frame with bypassed spans must own its fallback recording"
496 );
497 let mut summary = DrawRunSummary::scan(&primitives);
498 if replay.as_ref().is_some_and(|frame| {
499 frame
500 .spans
501 .iter()
502 .any(|span| matches!(span, cranpose_ui_graphics::FrameSpan::Retained { .. }))
503 }) {
504 summary.has_non_shadow = true;
505 }
506 Self {
507 phase,
508 command,
509 primitives,
510 summary,
511 replay,
512 }
513 }
514}
515
516#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
519pub struct DrawRunSummary {
520 pub has_text: bool,
523 pub has_shadow: bool,
524 pub has_non_shadow: bool,
526 pub has_pixel_sensitive: bool,
529}
530
531impl DrawRunSummary {
532 pub fn scan(primitives: &[DrawPrimitive]) -> Self {
533 fn unwrap_blend(mut primitive: &DrawPrimitive) -> &DrawPrimitive {
534 while let DrawPrimitive::Blend {
535 primitive: inner, ..
536 } = primitive
537 {
538 primitive = inner;
539 }
540 primitive
541 }
542 let mut summary = Self::default();
543 for primitive in primitives {
544 if matches!(primitive, DrawPrimitive::Shadow(_)) {
545 summary.has_shadow = true;
546 continue;
547 }
548 summary.has_non_shadow = true;
549 match unwrap_blend(primitive) {
550 DrawPrimitive::Text(_) => {
551 summary.has_text = true;
552 summary.has_pixel_sensitive = true;
553 }
554 DrawPrimitive::Image { .. } => summary.has_pixel_sensitive = true,
555 _ => {}
556 }
557 }
558 summary
559 }
560}
561
562#[derive(Clone)]
563pub struct RenderGraph {
564 pub root: LayerNode,
565}
566
567impl RenderGraph {
568 pub fn new(mut root: LayerNode) -> Self {
569 root.recompute_raster_cache_hashes();
570 Self { root }
571 }
572
573 pub fn node_count(&self) -> usize {
574 fn count_layer(layer: &LayerNode) -> usize {
575 1 + layer
576 .children
577 .iter()
578 .map(|child| match child {
579 RenderNode::Primitive(_) => 1,
580 RenderNode::DrawRun(run) => run.primitives.len(),
581 RenderNode::Layer(child_layer) => count_layer(child_layer),
582 })
583 .sum::<usize>()
584 }
585
586 count_layer(&self.root)
587 }
588
589 pub fn heap_bytes(&self) -> usize {
590 layer_heap_bytes(&self.root)
591 }
592
593 pub fn retained_visual_observation_nodes(&self) -> HashSet<NodeId> {
594 fn collect(layer: &LayerNode, nodes: &mut HashSet<NodeId>) {
595 if let Some(node_id) = layer.node_id {
596 nodes.insert(node_id);
597 }
598 for child in &layer.children {
599 match child {
600 RenderNode::DrawRun(run) => {
601 if let Some(command) = run.command {
602 nodes.insert(command.node_id);
603 }
604 }
605 RenderNode::Layer(child) => collect(child, nodes),
606 RenderNode::Primitive(_) => {}
607 }
608 }
609 }
610
611 let mut nodes = HashSet::new();
612 collect(&self.root, &mut nodes);
613 nodes
614 }
615}
616
617fn layer_heap_bytes(layer: &LayerNode) -> usize {
618 layer.hit_test.as_ref().map_or(0, hit_test_heap_bytes)
619 + size_of::<RenderNode>() * layer.children.capacity()
620 + layer
621 .children
622 .iter()
623 .map(render_node_heap_bytes)
624 .sum::<usize>()
625}
626
627fn render_node_heap_bytes(node: &RenderNode) -> usize {
628 match node {
629 RenderNode::Primitive(entry) => primitive_entry_heap_bytes(entry),
630 RenderNode::DrawRun(run) => {
631 size_of::<DrawPrimitive>() * run.primitives.capacity()
632 + run
633 .primitives
634 .iter()
635 .map(draw_primitive_heap_bytes)
636 .sum::<usize>()
637 }
638 RenderNode::Layer(layer) => size_of::<LayerNode>() + layer_heap_bytes(layer),
639 }
640}
641
642fn primitive_entry_heap_bytes(entry: &PrimitiveEntry) -> usize {
643 match &entry.node {
644 PrimitiveNode::Draw(draw) => draw_primitive_heap_bytes(&draw.primitive),
645 PrimitiveNode::Text(text) => {
646 size_of::<TextPrimitiveNode>() + annotated_string_heap_bytes(&text.text)
647 }
648 }
649}
650
651fn draw_primitive_heap_bytes(primitive: &DrawPrimitive) -> usize {
652 match primitive {
653 DrawPrimitive::Content
654 | DrawPrimitive::Rect { .. }
655 | DrawPrimitive::RoundRect { .. }
656 | DrawPrimitive::Arc { .. } => 0,
657 DrawPrimitive::Blend { primitive, .. } => {
658 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(primitive)
659 }
660 DrawPrimitive::Image { .. } => 0,
661 DrawPrimitive::Text(text) => {
662 size_of::<cranpose_ui_graphics::TextPrimitive>()
663 + text.text.len()
664 + text
665 .style
666 .font_family
667 .as_ref()
668 .map_or(0, |family| family.capacity())
669 }
670 DrawPrimitive::Shadow(shadow) => shadow_primitive_heap_bytes(shadow),
671 }
672}
673
674fn shadow_primitive_heap_bytes(shadow: &ShadowPrimitive) -> usize {
675 match shadow {
676 ShadowPrimitive::Drop { shape, .. } => {
677 size_of::<DrawPrimitive>() + draw_primitive_heap_bytes(shape)
678 }
679 ShadowPrimitive::Inner { fill, cutout, .. } => {
680 size_of::<DrawPrimitive>() * 2
681 + draw_primitive_heap_bytes(fill)
682 + draw_primitive_heap_bytes(cutout)
683 }
684 }
685}
686
687fn annotated_string_heap_bytes(text: &AnnotatedString) -> usize {
688 text.text.capacity()
689 + text.span_styles.capacity() * size_of::<usize>() * 2
690 + text.paragraph_styles.capacity() * size_of::<usize>() * 2
691 + text.string_annotations.capacity() * size_of::<usize>() * 2
692 + text.link_annotations.capacity() * size_of::<usize>() * 2
693 + text
694 .string_annotations
695 .iter()
696 .map(|annotation| {
697 annotation.item.tag.capacity() + annotation.item.annotation.capacity()
698 })
699 .sum::<usize>()
700 + text
701 .link_annotations
702 .iter()
703 .map(|annotation| match &annotation.item {
704 cranpose_ui::text::LinkAnnotation::Url(url) => url.capacity(),
705 cranpose_ui::text::LinkAnnotation::Clickable { tag, .. } => tag.capacity(),
706 })
707 .sum::<usize>()
708}
709
710fn hit_test_heap_bytes(hit_test: &HitTestNode) -> usize {
711 hit_test.click_actions.capacity() * size_of::<Rc<dyn Fn(Point)>>()
712 + hit_test.pointer_inputs.capacity() * size_of::<Rc<dyn Fn(PointerEvent)>>()
713}
714
715pub fn quad_bounds(quad: [[f32; 2]; 4]) -> Rect {
716 let mut min_x = f32::INFINITY;
717 let mut min_y = f32::INFINITY;
718 let mut max_x = f32::NEG_INFINITY;
719 let mut max_y = f32::NEG_INFINITY;
720
721 for [x, y] in quad {
722 min_x = min_x.min(x);
723 min_y = min_y.min(y);
724 max_x = max_x.max(x);
725 max_y = max_y.max(y);
726 }
727
728 Rect {
729 x: min_x,
730 y: min_y,
731 width: (max_x - min_x).max(0.0),
732 height: (max_y - min_y).max(0.0),
733 }
734}
735
736fn multiply_matrices(lhs: [[f32; 3]; 3], rhs: [[f32; 3]; 3]) -> [[f32; 3]; 3] {
737 let mut out = [[0.0; 3]; 3];
738 for row in 0..3 {
739 for col in 0..3 {
740 out[row][col] =
741 lhs[row][0] * rhs[0][col] + lhs[row][1] * rhs[1][col] + lhs[row][2] * rhs[2][col];
742 }
743 }
744 out
745}
746
747fn solve_homography(source: [[f32; 2]; 4], target: [[f32; 2]; 4]) -> Option<[f32; 8]> {
748 let mut matrix = [[0.0f32; 9]; 8];
749 for (index, (src, dst)) in source.into_iter().zip(target).enumerate() {
750 let row = index * 2;
751 let x = src[0];
752 let y = src[1];
753 let u = dst[0];
754 let v = dst[1];
755
756 matrix[row] = [x, y, 1.0, 0.0, 0.0, 0.0, -u * x, -u * y, u];
757 matrix[row + 1] = [0.0, 0.0, 0.0, x, y, 1.0, -v * x, -v * y, v];
758 }
759
760 for pivot in 0..8 {
761 let mut pivot_row = pivot;
762 let mut pivot_value = matrix[pivot][pivot].abs();
763 let mut candidate = pivot + 1;
764 while candidate < 8 {
765 let candidate_value = matrix[candidate][pivot].abs();
766 if candidate_value > pivot_value {
767 pivot_row = candidate;
768 pivot_value = candidate_value;
769 }
770 candidate += 1;
771 }
772
773 if pivot_value <= f32::EPSILON {
774 return None;
775 }
776
777 if pivot_row != pivot {
778 matrix.swap(pivot, pivot_row);
779 }
780
781 let divisor = matrix[pivot][pivot];
782 let mut col = pivot;
783 while col < 9 {
784 matrix[pivot][col] /= divisor;
785 col += 1;
786 }
787
788 for row in 0..8 {
789 if row == pivot {
790 continue;
791 }
792 let factor = matrix[row][pivot];
793 if factor.abs() <= f32::EPSILON {
794 continue;
795 }
796 let mut col = pivot;
797 while col < 9 {
798 matrix[row][col] -= factor * matrix[pivot][col];
799 col += 1;
800 }
801 }
802 }
803
804 let mut solution = [0.0f32; 8];
805 for index in 0..8 {
806 solution[index] = matrix[index][8];
807 }
808 Some(solution)
809}
810
811#[cfg(test)]
812mod tests {
813 use cranpose_ui_graphics::{Brush, Color, DrawPrimitive};
814
815 use super::*;
816
817 fn test_layer(local_bounds: Rect, children: Vec<RenderNode>) -> LayerNode {
818 LayerNode {
819 local_bounds,
820 children,
821 ..Default::default()
822 }
823 }
824
825 #[test]
826 fn projective_transform_translation_maps_points() {
827 let transform = ProjectiveTransform::translation(7.0, -3.5);
828 let mapped = transform.map_point(Point { x: 2.0, y: 4.0 });
829 assert!((mapped.x - 9.0).abs() < 1e-6);
830 assert!((mapped.y - 0.5).abs() < 1e-6);
831 }
832
833 #[test]
834 fn projective_transform_then_composes_in_parent_order() {
835 let child = ProjectiveTransform::translation(4.0, 2.0);
836 let parent = ProjectiveTransform::translation(10.0, -1.0);
837 let composed = child.then(parent);
838 let mapped = composed.map_point(Point { x: 1.0, y: 1.0 });
839 assert!((mapped.x - 15.0).abs() < 1e-6);
840 assert!((mapped.y - 2.0).abs() < 1e-6);
841 }
842
843 #[test]
844 fn homography_maps_rect_corners_to_target_quad() {
845 let rect = Rect {
846 x: 0.0,
847 y: 0.0,
848 width: 20.0,
849 height: 10.0,
850 };
851 let quad = [[5.0, 7.0], [25.0, 6.0], [7.0, 20.0], [28.0, 21.0]];
852 let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
853 let mapped = transform.map_rect(rect);
854 for (expected, actual) in quad.into_iter().zip(mapped) {
855 assert!((expected[0] - actual[0]).abs() < 1e-4);
856 assert!((expected[1] - actual[1]).abs() < 1e-4);
857 }
858 }
859
860 #[test]
861 fn axis_aligned_rect_to_quad_keeps_exact_affine_matrix() {
862 let rect = Rect {
863 x: 2.0,
864 y: 3.0,
865 width: 20.0,
866 height: 10.0,
867 };
868 let quad = [[12.0, 9.0], [32.0, 9.0], [12.0, 19.0], [32.0, 19.0]];
869 let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
870
871 assert_eq!(
872 transform.matrix(),
873 [[1.0, 0.0, 10.0], [0.0, 1.0, 6.0], [0.0, 0.0, 1.0]]
874 );
875 }
876
877 #[test]
878 fn axis_aligned_rect_to_quad_keeps_exact_axis_aligned_scale() {
879 let rect = Rect {
880 x: 4.0,
881 y: 6.0,
882 width: 10.0,
883 height: 8.0,
884 };
885 let quad = [[20.0, 18.0], [50.0, 18.0], [20.0, 42.0], [50.0, 42.0]];
886 let transform = ProjectiveTransform::from_rect_to_quad(rect, quad);
887
888 assert_eq!(
889 transform.matrix(),
890 [[3.0, 0.0, 8.0], [0.0, 3.0, 0.0], [0.0, 0.0, 1.0]]
891 );
892 }
893
894 #[test]
895 fn retained_visual_observation_nodes_collect_layers_and_command_owners() {
896 let bounds = Rect {
897 x: 0.0,
898 y: 0.0,
899 width: 20.0,
900 height: 20.0,
901 };
902 let command = |node_id| DrawCommandId {
903 node_id,
904 command_index: 0,
905 placement: DrawPlacement::Behind,
906 };
907 let mut child = test_layer(
908 bounds,
909 vec![RenderNode::DrawRun(DrawRunNode::for_command(
910 PrimitivePhase::BeforeChildren,
911 Some(command(17)),
912 Vec::new(),
913 ))],
914 );
915 child.node_id = Some(13);
916 let mut root = test_layer(
917 bounds,
918 vec![
919 RenderNode::DrawRun(DrawRunNode::for_command(
920 PrimitivePhase::BeforeChildren,
921 Some(command(9)),
922 Vec::new(),
923 )),
924 RenderNode::DrawRun(DrawRunNode::new(PrimitivePhase::BeforeChildren, Vec::new())),
925 RenderNode::Layer(Box::new(child)),
926 ],
927 );
928
929 root.node_id = Some(5);
930
931 assert_eq!(
932 RenderGraph::new(root).retained_visual_observation_nodes(),
933 HashSet::from([5, 9, 13, 17])
934 );
935 }
936
937 #[test]
938 fn render_graph_new_recomputes_manual_layer_hashes() {
939 let primitive = PrimitiveEntry {
940 phase: PrimitivePhase::BeforeChildren,
941 node: PrimitiveNode::Draw(DrawPrimitiveNode {
942 primitive: DrawPrimitive::Rect {
943 rect: Rect {
944 x: 1.0,
945 y: 2.0,
946 width: 8.0,
947 height: 6.0,
948 },
949 brush: Brush::solid(Color::WHITE),
950 stroke: None,
951 },
952 clip: None,
953 }),
954 };
955 let mut root = test_layer(
956 Rect {
957 x: 0.0,
958 y: 0.0,
959 width: 20.0,
960 height: 20.0,
961 },
962 vec![RenderNode::Primitive(primitive)],
963 );
964 root.graphics_layer.render_effect = Some(RenderEffect::blur(3.0));
965 let mut expected = root.clone();
966 expected.recompute_raster_cache_hashes();
967
968 let graph = RenderGraph::new(root);
969 assert_eq!(
970 graph.root.target_content_hash(),
971 expected.target_content_hash()
972 );
973 assert_eq!(graph.root.effect_hash(), expected.effect_hash());
974 }
975
976 #[test]
977 fn motion_source_content_hash_ignores_translated_content_offset() {
978 let primitive = PrimitiveEntry {
979 phase: PrimitivePhase::BeforeChildren,
980 node: PrimitiveNode::Draw(DrawPrimitiveNode {
981 primitive: DrawPrimitive::Rect {
982 rect: Rect {
983 x: 1.0,
984 y: 2.0,
985 width: 8.0,
986 height: 6.0,
987 },
988 brush: Brush::solid(Color::WHITE),
989 stroke: None,
990 },
991 clip: None,
992 }),
993 };
994 let mut base = test_layer(
995 Rect {
996 x: 0.0,
997 y: 0.0,
998 width: 20.0,
999 height: 20.0,
1000 },
1001 vec![RenderNode::Primitive(primitive)],
1002 );
1003 base.translated_content_context = true;
1004 base.translated_content_offset = Point::new(0.0, -24.0);
1005 base.recompute_raster_cache_hashes();
1006
1007 let mut moved = base.clone();
1008 moved.translated_content_offset = Point::new(0.0, -72.0);
1009 moved.recompute_raster_cache_hashes();
1010
1011 assert_ne!(base.target_content_hash(), moved.target_content_hash());
1012 assert_eq!(
1013 base.motion_source_content_hash(),
1014 moved.motion_source_content_hash()
1015 );
1016 }
1017}