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 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}