pax_runtime/
layout.rs

1use std::ops::Mul;
2
3#[allow(unused)]
4use pax_runtime_api::math::Generic;
5use pax_runtime_api::math::{Point2, Space, TransformParts};
6use pax_runtime_api::{Interpolatable, Percent, Property, Rotation, Window};
7
8use crate::api::math::{Transform2, Vector2};
9use crate::api::{Axis, Size, Transform2D};
10use crate::node_interface::NodeLocal;
11
12/// For the `current_expanded_node` attached to `ptc`, calculates and returns a new [`crate::rendering::TransformAndBounds`] a.k.a. "tab".
13/// Intended as a helper method to be called during properties computation, for creating a new tab to attach to `ptc` for downstream calculations.
14pub fn compute_tab(
15    layout_properties: Property<LayoutProperties>,
16    extra_transform: Property<Option<Transform2D>>,
17    container_transform_and_bounds: Property<TransformAndBounds<NodeLocal, Window>>,
18) -> Property<TransformAndBounds<NodeLocal, Window>> {
19    //get the size of this node (calc'd or otherwise) and use
20    //it as the new accumulated bounds: both for this node's children (their parent container bounds)
21    //and for this node itself (e.g. for specifying the size of a Rectangle node)
22
23    let deps = [
24        layout_properties.untyped(),
25        container_transform_and_bounds.untyped(),
26        extra_transform.untyped(),
27    ];
28
29    Property::computed(
30        move || {
31            let container_t_and_b = container_transform_and_bounds.get();
32            layout_properties.read(|layout_properties| {
33                let transform_and_bounds =
34                    calculate_transform_and_bounds(&layout_properties, container_t_and_b.clone());
35                let extra_transform = extra_transform.get();
36                if let Some(transform) = extra_transform {
37                    transform.apply(transform_and_bounds)
38                } else {
39                    transform_and_bounds
40                }
41            })
42        },
43        &deps,
44    )
45}
46
47pub fn calculate_transform_and_bounds(
48    LayoutProperties {
49        width,
50        height,
51        anchor_x,
52        anchor_y,
53        x,
54        y,
55        rotate,
56        scale_x,
57        scale_y,
58        skew_x,
59        skew_y,
60    }: &LayoutProperties,
61    TransformAndBounds {
62        transform: container_transform,
63        bounds: container_bounds,
64    }: TransformAndBounds<NodeLocal, Window>,
65) -> TransformAndBounds<NodeLocal, Window> {
66    let x = x.unwrap_or(Size::ZERO());
67    let y = y.unwrap_or(Size::ZERO());
68    let width = width
69        .map(|v| v.evaluate(container_bounds, Axis::X))
70        .unwrap_or(container_bounds.0);
71    let height = height
72        .map(|v| v.evaluate(container_bounds, Axis::Y))
73        .unwrap_or(container_bounds.1);
74    let origin = Vector2::new(
75        x.evaluate(container_bounds, Axis::X),
76        y.evaluate(container_bounds, Axis::Y),
77    );
78
79    let bounds = (width, height);
80
81    // Anchor behavior:  if no anchor is specified and if x/y values are present
82    //and have an explicit percent value or component, use those percent values (clamp 100% and 0%)
83    //otherwise, default to 0
84    let anchor_x = anchor_x.unwrap_or_else(|| match x {
85        Size::Pixels(_) => Size::ZERO(),
86        Size::Combined(_, per) | Size::Percent(per) => {
87            if per.to_float() > 100.0 {
88                Size::default()
89            } else if per.to_float() < 0.0 {
90                Size::ZERO()
91            } else {
92                Size::Percent(per)
93            }
94        }
95    });
96
97    let anchor_y = anchor_y.unwrap_or_else(|| match y {
98        Size::Pixels(_) => Size::ZERO(),
99        Size::Combined(_, per) | Size::Percent(per) => {
100            if per.to_float() > 100.0 {
101                Size::default()
102            } else if per.to_float() < 0.0 {
103                Size::ZERO()
104            } else {
105                Size::Percent(per)
106            }
107        }
108    });
109
110    let anchor_transform = Transform2::translate(Vector2::new(
111        -anchor_x.evaluate(bounds, Axis::X),
112        -anchor_y.evaluate(bounds, Axis::Y),
113    ));
114
115    let scale = Vector2::new(
116        scale_x
117            .as_ref()
118            .map(|s| s.0.to_float() / 100.0)
119            .unwrap_or(1.0),
120        scale_y
121            .as_ref()
122            .map(|s| s.0.to_float() / 100.0)
123            .unwrap_or(1.0),
124    );
125
126    let skew = Vector2::new(
127        skew_x.map(|s| s.get_as_radians()).unwrap_or(0.0),
128        skew_y.map(|s| s.get_as_radians()).unwrap_or(0.0),
129    );
130
131    let rotation = rotate.map(|s| s.get_as_radians()).unwrap_or(0.0);
132
133    let parts = TransformParts {
134        origin,
135        scale,
136        skew,
137        rotation,
138    };
139
140    let combined_transform: Transform2<NodeLocal, NodeLocal> = parts.into();
141
142    TransformAndBounds {
143        transform: container_transform * combined_transform * anchor_transform,
144        bounds,
145    }
146}
147
148/// Properties that are currently re-computed each frame before rendering.
149
150impl<F: Space, T: Space> Interpolatable for TransformAndBounds<F, T> {
151    fn interpolate(&self, other: &Self, t: f64) -> Self {
152        TransformAndBounds {
153            transform: self.transform.interpolate(&other.transform, t),
154            bounds: (
155                self.bounds.0 + (other.bounds.0 - self.bounds.0) * t,
156                self.bounds.1 + (other.bounds.1 - self.bounds.1) * t,
157            ),
158        }
159    }
160}
161
162#[derive(PartialEq)]
163pub struct TransformAndBounds<F, T = F> {
164    pub transform: Transform2<F, T>,
165    pub bounds: (f64, f64),
166}
167
168impl<F: Space, T: Space> std::fmt::Debug for TransformAndBounds<F, T> {
169    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170        f.debug_struct("TransformAndBounds")
171            .field("transform", &self.transform)
172            .field("bounds", &self.bounds)
173            .finish()
174    }
175}
176
177impl PartialEq for TransformAndBounds<NodeLocal, Window> {
178    fn eq(&self, other: &Self) -> bool {
179        self.transform == other.transform && self.bounds == other.bounds
180    }
181}
182
183impl<F: Space, T: Space> Default for TransformAndBounds<F, T> {
184    fn default() -> Self {
185        Self {
186            transform: Default::default(),
187            bounds: (100.0, 100.0),
188        }
189    }
190}
191
192impl<F, T> Clone for TransformAndBounds<F, T> {
193    fn clone(&self) -> Self {
194        Self {
195            transform: self.transform.clone(),
196            bounds: self.bounds.clone(),
197        }
198    }
199}
200
201impl<F, T> Copy for TransformAndBounds<F, T> {}
202
203impl<W1: Space, W2: Space, W3: Space> Mul<TransformAndBounds<W1, W2>>
204    for TransformAndBounds<W2, W3>
205{
206    type Output = TransformAndBounds<W1, W3>;
207
208    // - if T(M) = M.transform * Transform::scale_sep(M.bounds.0, M.bounds.1),
209    //   then T(A) * T(B) = T(A*B).
210    // (
211    // - some other rule regarding multiplying scaling and width/height values being constant,
212    //   related to how width/height scale x/y change for A or B, how that affects A*B.
213    // TODO figure this rule out, this is that would fix resize skew introducing un-needed scaling
214    // (compare with figma)
215    //)
216    fn mul(self, rhs: TransformAndBounds<W1, W2>) -> Self::Output {
217        let s_s = Transform2::scale_sep(Vector2::new(self.bounds.0, self.bounds.1));
218        let r_s = Transform2::scale_sep(Vector2::new(rhs.bounds.0, rhs.bounds.1));
219
220        let s_t = self.transform * s_s;
221        let r_t = rhs.transform * r_s;
222        let res = s_t * r_t * s_s.inverse() * r_s.inverse();
223
224        TransformAndBounds {
225            transform: res,
226            bounds: (self.bounds.0 * rhs.bounds.0, self.bounds.1 * rhs.bounds.1),
227        }
228    }
229}
230
231impl<F: Space, T: Space> TransformAndBounds<F, T> {
232    pub fn center(&self) -> Point2<T> {
233        let (o, u, v) = self.transform.decompose();
234        let u = u * self.bounds.0;
235        let v = v * self.bounds.1;
236        o + v / 2.0 + u / 2.0
237    }
238
239    pub fn corners(&self) -> [Point2<T>; 4] {
240        let (o, u, v) = self.transform.decompose();
241        let u = u * self.bounds.0;
242        let v = v * self.bounds.1;
243        [o, o + v, o + u + v, o + u]
244    }
245
246    pub fn contains_point(&self, point: Point2<T>) -> bool {
247        self.as_transform().contains_point(point)
248    }
249
250    pub fn as_pure_size(self) -> Self {
251        let mut parts: TransformParts = self.transform.into();
252        let bounds_x = std::mem::replace(&mut parts.scale.x, 1.0);
253        let bounds_y = std::mem::replace(&mut parts.scale.y, 1.0);
254        TransformAndBounds {
255            transform: parts.into(),
256            bounds: (self.bounds.0 * bounds_x, self.bounds.1 * bounds_y),
257        }
258    }
259    pub fn as_pure_scale(self) -> Self {
260        TransformAndBounds {
261            transform: self.transform
262                * Transform2::scale_sep(Vector2::new(self.bounds.0, self.bounds.1)),
263            bounds: (1.0, 1.0),
264        }
265    }
266
267    pub fn cast_spaces<A: Space, B: Space>(self) -> TransformAndBounds<A, B> {
268        TransformAndBounds {
269            transform: self.transform.cast_spaces(),
270            bounds: self.bounds,
271        }
272    }
273
274    pub fn as_transform(&self) -> Transform2<F, T> {
275        self.transform * Transform2::scale_sep(Vector2::new(self.bounds.0, self.bounds.1))
276    }
277}
278
279#[test]
280fn test_transform_and_bounds_mult() {
281    let dvx = 0.6;
282    let dvy = 0.3;
283
284    let t_and_b_with_scale = TransformAndBounds::<Generic> {
285        transform: Transform2::new([1.5, 1.1, 2.3, 3.2, 1.2, 1.0])
286            * Transform2::scale_sep(Vector2::<Generic>::new(dvx, dvy)),
287        bounds: (2.1, 1.2),
288    };
289
290    let t_and_b_with_size = TransformAndBounds::<Generic> {
291        transform: Transform2::new([1.5, 1.1, 2.3, 3.2, 1.2, 1.0]),
292        bounds: (2.1 * dvx, 1.2 * dvy),
293    };
294    let some_other_transform = TransformAndBounds::<Generic> {
295        transform: Transform2::new([1.1, 1.2, 5.3, 9.2, 1.0, 2.0]),
296        bounds: (1.9, 4.5),
297    };
298
299    let res_scale = t_and_b_with_scale * some_other_transform;
300    let res_size = t_and_b_with_size * some_other_transform;
301
302    let t_scale = res_scale.transform
303        * Transform2::<Generic>::scale_sep(Vector2::new(res_scale.bounds.0, res_scale.bounds.1));
304    let t_scale_c = t_scale.coeffs();
305    let t_size = res_size.transform
306        * Transform2::<Generic>::scale_sep(Vector2::new(res_size.bounds.0, res_size.bounds.1));
307    let t_size_c = t_size.coeffs();
308    let diff_sum = t_scale_c
309        .iter()
310        .zip(t_size_c)
311        .map(|(a, b)| (a - b).abs())
312        .sum::<f64>();
313    assert!(diff_sum < 1e-4);
314
315    let res_scale_other_way = some_other_transform * t_and_b_with_scale;
316    let res_size_other_way = some_other_transform * t_and_b_with_size;
317
318    let t_scale = res_scale_other_way.transform
319        * Transform2::<Generic>::scale_sep(Vector2::new(
320            res_scale_other_way.bounds.0,
321            res_scale_other_way.bounds.1,
322        ));
323    let t_scale_c = t_scale.coeffs();
324    let t_size = res_size_other_way.transform
325        * Transform2::<Generic>::scale_sep(Vector2::new(
326            res_size_other_way.bounds.0,
327            res_size_other_way.bounds.1,
328        ));
329    let t_size_c = t_size.coeffs();
330    let diff_sum = t_scale_c
331        .iter()
332        .zip(t_size_c)
333        .map(|(a, b)| (a - b).abs())
334        .sum::<f64>();
335    assert!(diff_sum < 1e-4);
336}
337
338impl Interpolatable for LayoutProperties {}
339
340#[derive(Debug, Default, Clone)]
341pub struct LayoutProperties {
342    pub x: Option<Size>,
343    pub y: Option<Size>,
344    pub width: Option<Size>,
345    pub height: Option<Size>,
346    pub rotate: Option<Rotation>,
347    pub scale_x: Option<Percent>,
348    pub scale_y: Option<Percent>,
349    pub anchor_x: Option<Size>,
350    pub anchor_y: Option<Size>,
351    pub skew_x: Option<Rotation>,
352    pub skew_y: Option<Rotation>,
353}
354
355impl LayoutProperties {
356    pub fn fill() -> Self {
357        Self {
358            x: Some(Size::ZERO()),
359            y: Some(Size::ZERO()),
360            width: Some(Size::default()),
361            height: Some(Size::default()),
362            rotate: Some(Rotation::ZERO()),
363            scale_x: Some(Percent(100.into())),
364            scale_y: Some(Percent(100.into())),
365            anchor_x: None,
366            anchor_y: None,
367            skew_x: Some(Rotation::ZERO()),
368            skew_y: Some(Rotation::ZERO()),
369        }
370    }
371}
372
373impl<F: Space, T: Space> TransformAndBounds<F, T> {
374    pub fn inverse(&self) -> TransformAndBounds<T, F> {
375        let t_inv = self.transform.inverse();
376        let b_inv = (1.0 / self.bounds.0, 1.0 / self.bounds.1);
377        TransformAndBounds {
378            transform: t_inv,
379            bounds: b_inv,
380        }
381    }
382
383    //Applies the separating axis theorem to determine whether two `TransformAndBounds` intersect.
384    pub fn intersects(&self, other: &Self) -> bool {
385        let corners_self = self.corners();
386        let corners_other = other.corners();
387
388        for i in 0..2 {
389            let axis = (corners_self[i] - corners_self[(i + 1) % 4]).normal();
390
391            let self_projections: Vec<_> = corners_self
392                .iter()
393                .map(|&p| p.to_vector().project_onto(axis).length())
394                .collect();
395            let other_projections: Vec<_> = corners_other
396                .iter()
397                .map(|&p| p.to_vector().project_onto(axis).length())
398                .collect();
399
400            let (min_self, max_self) = min_max_projections(&self_projections);
401            let (min_other, max_other) = min_max_projections(&other_projections);
402
403            // Check for non-overlapping projections
404            if max_self < min_other || max_other < min_self {
405                // By the separating axis theorem, non-overlap of projections on _any one_ of the axis-normals proves that these polygons do not intersect.
406                return false;
407            }
408        }
409        true
410    }
411}
412
413fn min_max_projections(projections: &[f64]) -> (f64, f64) {
414    let min_projection = *projections
415        .iter()
416        .min_by(|a, b| a.partial_cmp(b).unwrap())
417        .unwrap();
418    let max_projection = *projections
419        .iter()
420        .max_by(|a, b| a.partial_cmp(b).unwrap())
421        .unwrap();
422    (min_projection, max_projection)
423}
424
425pub trait ComputableTransform<F, T> {
426    fn apply(&self, bounds: TransformAndBounds<F, T>) -> TransformAndBounds<F, T>;
427}
428
429impl ComputableTransform<NodeLocal, Window> for Transform2D {
430    fn apply(
431        &self,
432        bounds: TransformAndBounds<NodeLocal, Window>,
433    ) -> TransformAndBounds<NodeLocal, Window> {
434        let layout_properties = LayoutProperties {
435            x: self.translate.map(|v| v[0]),
436            y: self.translate.map(|v| v[1]),
437            width: Some(Size::Pixels(bounds.bounds.0.into())),
438            height: Some(Size::Pixels(bounds.bounds.1.into())),
439            rotate: self.rotate.clone(),
440            scale_x: self
441                .scale
442                .as_ref()
443                .map(|v| Percent((100.0 * v[0].clone().expect_percent()).into())),
444            scale_y: self
445                .scale
446                .as_ref()
447                .map(|v| Percent((100.0 * v[1].clone().expect_percent()).into())),
448            anchor_x: self.anchor.map(|v| v[0]),
449            anchor_y: self.anchor.map(|v| v[1]),
450            skew_x: self.skew.as_ref().map(|v| v[0].clone()),
451            skew_y: self.skew.as_ref().map(|v| v[1].clone()),
452        };
453
454        let curr = calculate_transform_and_bounds(&layout_properties, bounds.clone());
455        match &self.previous {
456            Some(previous) => (*previous).apply(curr),
457            None => curr,
458        }
459    }
460}