polyhorn_ui/styles/
transform.rs

1use num_traits::{Float, FloatConst};
2
3use crate::geometry::{Dimension, Size};
4use crate::linalg::{Quaternion3D, Transform3D};
5use crate::physics::Angle;
6
7/// Builder that can be used to efficiently create a transform consisting of
8/// multiple individual operations.
9///
10/// The builder optimistically compresses the sequence of operations by
11/// attempting to concatenate each operation to the previous operation, which
12/// will succeed if the previous operation does not need to be resolved (i.e. is
13/// non-relative). After compression, the builder can hold at most 8 transforms.
14#[derive(Copy, Clone, Debug, Default, PartialEq)]
15pub struct TransformBuilder<T>
16where
17    T: Float,
18{
19    transforms: [Transform<T>; 8],
20    index: usize,
21}
22
23impl<T> TransformBuilder<T>
24where
25    T: Float,
26{
27    /// Returns a new builder that can be used to efficiently create a transform
28    /// consisting of multiple individual operations.
29    pub fn new() -> TransformBuilder<T>
30    where
31        T: Default,
32    {
33        Default::default()
34    }
35
36    /// Consumes the builder and returns a new builder after adding the given
37    /// transform and recompression. Returns an error if the given transform
38    /// cannot be concatenated to the last transform tracked by the builder and
39    /// there are no slots remaining in the builder.
40    pub fn push(
41        mut self,
42        transform: Transform<T>,
43    ) -> Result<TransformBuilder<T>, TransformBuilder<T>> {
44        match self.index {
45            0 => self.transforms[0] = transform,
46            n => match self.transforms[n - 1].concat(transform) {
47                Some(transform) => {
48                    self.transforms[n - 1] = transform;
49                    return Ok(self);
50                }
51                None if n == self.transforms.len() => return Err(self),
52                None => self.transforms[n] = transform,
53            },
54        }
55
56        self.index += 1;
57
58        Ok(self)
59    }
60
61    /// Returns the number of transforms currently tracked by the builder.
62    pub fn len(&self) -> usize {
63        self.index
64    }
65
66    /// Consumes the builder and returns all of its transforms.
67    pub fn into_transforms(self) -> [Transform<T>; 8] {
68        self.transforms
69    }
70}
71
72/// Decomposition of a CSS transform into a constant 3D transform and a relative
73/// 2D translation (i.e. CSS percentage).
74#[derive(Copy, Clone, Debug, PartialEq)]
75pub struct Transform<T>
76where
77    T: Float,
78{
79    /// This is the underlying, layout-independent 3D affine transformation
80    /// matrix.
81    pub matrix: Transform3D<T>,
82
83    /// This is the underlying, layout-dependent relative translation (measured
84    /// in percentage points). This translation should be applied after the
85    /// layout-independent 3D affine transformation matrix.
86    pub relative_translation: (T, T),
87}
88
89impl<T> Transform<T>
90where
91    T: Float,
92{
93    /// This function creates a new transform with the given coefficients.
94    pub fn with_transform(matrix: Transform3D<T>) -> Transform<T> {
95        Transform {
96            matrix,
97            ..Default::default()
98        }
99    }
100
101    /// This function creates a new homogeneous transform from the given
102    /// coefficients.
103    pub fn with_matrix(matrix: [T; 6]) -> Transform<T> {
104        let [a, b, c, d, tx, ty] = matrix;
105
106        let mut transform = Transform::default();
107        transform.matrix.columns[0][0] = a;
108        transform.matrix.columns[0][1] = b;
109        transform.matrix.columns[1][0] = c;
110        transform.matrix.columns[1][1] = d;
111        transform.matrix.columns[3][0] = tx;
112        transform.matrix.columns[3][1] = ty;
113
114        transform
115    }
116
117    /// This function creates a new homogeneous transform from the given
118    /// coefficients.
119    pub fn with_matrix3d(matrix: [T; 16]) -> Transform<T> {
120        let [m11, m12, m13, m14, m21, m22, m23, m24, m31, m32, m33, m34, m41, m42, m43, m44] =
121            matrix;
122
123        let mut transform = Transform::default();
124        transform.matrix.columns[0] = [m11, m12, m13, m14];
125        transform.matrix.columns[1] = [m21, m22, m23, m24];
126        transform.matrix.columns[2] = [m31, m32, m33, m34];
127        transform.matrix.columns[3] = [m41, m42, m43, m44];
128
129        transform
130    }
131
132    /// This function creates a new perspective transform with the given
133    /// parameters.
134    pub fn with_perspective(d: T) -> Transform<T> {
135        Transform {
136            matrix: Transform3D::with_perspective(d),
137            ..Default::default()
138        }
139    }
140
141    /// This function creates a new rotation transform with the given
142    /// parameters.
143    pub fn with_rotation(rx: T, ry: T, rz: T, angle: Angle<T>) -> Transform<T>
144    where
145        T: FloatConst,
146    {
147        let q = Quaternion3D::with_angle(angle.to_radians(), -rx, -ry, rz);
148        Transform {
149            matrix: Transform3D::with_rotation(q),
150            ..Default::default()
151        }
152    }
153
154    /// This function creates a new scale transform with the given parameters.
155    pub fn with_scale(sx: T, sy: T, sz: T) -> Transform<T> {
156        Transform {
157            matrix: Transform3D::with_scale(sx, sy, sz),
158            ..Default::default()
159        }
160    }
161
162    /// This function creates a new horizontal skew transform with the given
163    /// angle.
164    pub fn with_skew_x(sx: Angle<T>) -> Transform<T> {
165        Transform {
166            matrix: Transform3D::with_skew_x(sx.to_radians()),
167            ..Default::default()
168        }
169    }
170
171    /// This function creates a new vertical skew transform with the given
172    /// angle.
173    pub fn with_skew_y(sy: Angle<T>) -> Transform<T> {
174        Transform {
175            matrix: Transform3D::with_skew_y(sy.to_radians()),
176            ..Default::default()
177        }
178    }
179
180    /// This function creates a new translation transform with the given
181    /// parameters. If one of the horizontal or vertical dimensions is relative
182    /// (i.e. a percentage), this transform will need to be resolved before it
183    /// can be applied.
184    pub fn with_translation(tx: Dimension<T>, ty: Dimension<T>, tz: T) -> Transform<T> {
185        let (tx, rx) = match tx {
186            Dimension::Percentage(rx) => (T::zero(), rx),
187            Dimension::Points(tx) => (tx, T::zero()),
188            _ => (T::zero(), T::zero()),
189        };
190
191        let (ty, ry) = match ty {
192            Dimension::Percentage(ry) => (T::zero(), ry),
193            Dimension::Points(ty) => (ty, T::zero()),
194            _ => (T::zero(), T::zero()),
195        };
196
197        Transform {
198            matrix: Transform3D::with_translation(tx, ty, tz),
199            relative_translation: (rx, ry),
200        }
201    }
202
203    /// This function returns a boolean that indicates if this transform is
204    /// resolved, i.e. its relative translation is zero in both dimensions.
205    pub fn is_resolved(&self) -> bool {
206        let (tx, ty) = self.relative_translation;
207        tx.is_zero() && ty.is_zero()
208    }
209
210    /// This function resolves a transform using the given size.
211    pub fn resolve(&self, size: Size<T>) -> Transform3D<T> {
212        let (rtx, rty) = self.relative_translation;
213
214        self.matrix
215            .translate(rtx * size.width, rty * size.height, T::zero())
216    }
217
218    /// This function attempts to compress this transform and the given
219    /// transform into a single transform, which will only work if the
220    /// original transform is resolved (i.e. not relative to the unknown
221    /// container size).
222    pub fn concat(&self, other: Transform<T>) -> Option<Transform<T>> {
223        match self.is_resolved() {
224            true => Some(Transform {
225                matrix: self.matrix.concat(other.matrix),
226                relative_translation: other.relative_translation,
227            }),
228            false => None,
229        }
230    }
231
232    /// This function folds the given transforms after resolving each with the
233    /// given size.
234    pub fn squash(transforms: [Transform<T>; 8], size: Size<T>) -> Transform3D<T> {
235        transforms
236            .iter()
237            .fold(Transform3D::identity(), |current, transform| {
238                current.concat(transform.resolve(size))
239            })
240    }
241}
242
243impl<T> Default for Transform<T>
244where
245    T: Float,
246{
247    fn default() -> Self {
248        Transform {
249            matrix: Default::default(),
250            relative_translation: (T::zero(), T::zero()),
251        }
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::{Angle, Dimension, Quaternion3D, Size, Transform, Transform3D, TransformBuilder};
258
259    #[test]
260    fn test_transform() {
261        assert_eq!(
262            Transform::with_rotation(1.0, 0.0, 0.0, Angle::with_degrees(45.0)).is_resolved(),
263            true
264        );
265
266        assert_eq!(Transform::with_scale(1.0, 2.0, 3.0).is_resolved(), true);
267
268        assert_eq!(
269            Transform::with_translation(Dimension::Points(10.0), Dimension::Points(20.0), 30.0)
270                .is_resolved(),
271            true
272        );
273
274        assert_eq!(
275            Transform::with_translation(Dimension::Percentage(10.0), Dimension::Points(20.0), 30.0)
276                .is_resolved(),
277            false
278        );
279
280        assert_eq!(
281            Transform::with_translation(Dimension::Points(10.0), Dimension::Percentage(20.0), 30.0)
282                .is_resolved(),
283            false
284        );
285
286        assert_eq!(
287            Transform::with_translation(
288                Dimension::Percentage(0.0),
289                Dimension::Percentage(0.0),
290                30.0
291            )
292            .is_resolved(),
293            true
294        );
295    }
296
297    #[test]
298    fn test_transform_builder() {
299        let builder = TransformBuilder::new()
300            .push(Transform::with_scale(2.0, 3.0, 1.0))
301            .unwrap()
302            .push(Transform::with_translation(
303                Dimension::Percentage(0.5),
304                Dimension::Percentage(0.3),
305                20.0,
306            ))
307            .unwrap()
308            .push(Transform::with_rotation(
309                1.0,
310                0.0,
311                0.0,
312                Angle::with_degrees(45.0),
313            ))
314            .unwrap();
315
316        assert_eq!(builder.len(), 2);
317
318        let resolved = Transform::squash(builder.into_transforms(), Size::new(320.0, 480.0));
319
320        assert_eq!(
321            resolved,
322            Transform3D::with_scale(2.0, 3.0, 1.0)
323                .translate(160.0, 144.0, 20.0)
324                .rotate(Quaternion3D::with_angle(
325                    Angle::with_degrees(45.0).to_radians(),
326                    -1.0,
327                    0.0,
328                    0.0
329                ))
330        );
331    }
332}