|
1 | 1 | use crate::math::bbox::AxisAlignedBbox; |
2 | 2 | use core::f64; |
| 3 | +use dyn_any::DynAny; |
3 | 4 | use glam::{DAffine2, DMat2, DVec2, UVec2}; |
4 | 5 |
|
| 6 | +/// Controls whether the Decompose Scale node returns axis-length magnitudes or pure scale factors. |
| 7 | +#[repr(C)] |
| 8 | +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] |
| 9 | +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] |
| 10 | +#[widget(Radio)] |
| 11 | +pub enum ScaleType { |
| 12 | + /// The visual length of each axis (always positive, includes any skew contribution). |
| 13 | + #[default] |
| 14 | + Magnitude, |
| 15 | + /// The isolated scale factors with rotation and skew stripped away (can be negative for flipped axes). |
| 16 | + Pure, |
| 17 | +} |
| 18 | + |
5 | 19 | pub trait Transform { |
6 | 20 | fn transform(&self) -> DAffine2; |
7 | 21 |
|
8 | 22 | fn local_pivot(&self, pivot: DVec2) -> DVec2 { |
9 | 23 | pivot |
10 | 24 | } |
11 | 25 |
|
12 | | - fn decompose_scale(&self) -> DVec2 { |
13 | | - DVec2::new(self.transform().transform_vector2(DVec2::X).length(), self.transform().transform_vector2(DVec2::Y).length()) |
| 26 | + /// Decomposes the full transform into `(rotation, signed_scale, skew)` using a TRS+Skew factorization. |
| 27 | + /// |
| 28 | + /// - `rotation`: angle in radians |
| 29 | + /// - `signed_scale`: the algebraic scale factors (can be negative for reflections, excludes skew) |
| 30 | + /// - `skew`: the horizontal shear coefficient (the raw matrix value, not an angle) |
| 31 | + /// |
| 32 | + /// The original transform can be reconstructed as: |
| 33 | + /// ``` |
| 34 | + /// DAffine2::from_scale_angle_translation(scale, rotation, translation) * DAffine2::from_cols_array(&[1., 0., skew, 1., 0., 0.]) |
| 35 | + /// ``` |
| 36 | + #[inline(always)] |
| 37 | + fn decompose_rotation_scale_skew(&self) -> (f64, DVec2, f64) { |
| 38 | + let t = self.transform(); |
| 39 | + let x_axis = t.matrix2.x_axis; |
| 40 | + let y_axis = t.matrix2.y_axis; |
| 41 | + |
| 42 | + let angle = x_axis.y.atan2(x_axis.x); |
| 43 | + let (sin, cos) = angle.sin_cos(); |
| 44 | + |
| 45 | + let scale_x = if cos.abs() > 1e-10 { x_axis.x / cos } else { x_axis.y / sin }; |
| 46 | + |
| 47 | + let mut skew = (sin * y_axis.y + cos * y_axis.x) / (sin * sin * scale_x + cos * cos * scale_x); |
| 48 | + if !skew.is_finite() { |
| 49 | + skew = 0.; |
| 50 | + } |
| 51 | + |
| 52 | + let scale_y = if cos.abs() > 1e-10 { |
| 53 | + (y_axis.y - scale_x * sin * skew) / cos |
| 54 | + } else { |
| 55 | + (scale_x * cos * skew - y_axis.x) / sin |
| 56 | + }; |
| 57 | + |
| 58 | + (angle, DVec2::new(scale_x, scale_y), skew) |
14 | 59 | } |
15 | 60 |
|
16 | | - /// Requires that the transform does not contain any skew. |
| 61 | + /// Extracts the rotation angle (in radians) from the transform. |
| 62 | + /// This is the angle of the x-axis and is correct regardless of skew, negative scale, or non-uniform scale. |
17 | 63 | fn decompose_rotation(&self) -> f64 { |
18 | | - let rotation_matrix = (self.transform() * DAffine2::from_scale(self.decompose_scale().recip())).matrix2; |
19 | | - let rotation = -rotation_matrix.mul_vec2(DVec2::X).angle_to(DVec2::X); |
| 64 | + let x_axis = self.transform().matrix2.x_axis; |
| 65 | + let rotation = x_axis.y.atan2(x_axis.x); |
20 | 66 | if rotation == -0. { 0. } else { rotation } |
21 | 67 | } |
22 | 68 |
|
| 69 | + /// Returns the signed scale components from the TRS+Skew decomposition. |
| 70 | + /// Unlike [`Self::scale_magnitudes`] which returns positive axis-length magnitudes, |
| 71 | + /// this returns the algebraic scale factors which can be negative for reflections and exclude skew. |
| 72 | + fn decompose_scale(&self) -> DVec2 { |
| 73 | + self.decompose_rotation_scale_skew().1 |
| 74 | + } |
| 75 | + |
| 76 | + /// Returns the unsigned scale as the lengths of each axis (always positive, includes skew contribution). |
| 77 | + /// Use this for magnitude-based queries like stroke width scaling, zoom level, or bounding box inflation. |
| 78 | + fn scale_magnitudes(&self) -> DVec2 { |
| 79 | + DVec2::new(self.transform().transform_vector2(DVec2::X).length(), self.transform().transform_vector2(DVec2::Y).length()) |
| 80 | + } |
| 81 | + |
| 82 | + /// Returns the horizontal skew (shear) coefficient from the TRS+Skew decomposition. |
| 83 | + /// This is the raw matrix coefficient. To convert to degrees: `skew.atan().to_degrees()`. |
| 84 | + fn decompose_skew(&self) -> f64 { |
| 85 | + self.decompose_rotation_scale_skew().2 |
| 86 | + } |
| 87 | + |
23 | 88 | /// Detects if the transform contains skew by checking if the transformation matrix |
24 | 89 | /// deviates from a pure rotation + uniform scale + translation. |
25 | 90 | /// |
@@ -135,7 +200,7 @@ impl Footprint { |
135 | 200 | } |
136 | 201 |
|
137 | 202 | pub fn scale(&self) -> DVec2 { |
138 | | - self.transform.decompose_scale() |
| 203 | + self.transform.scale_magnitudes() |
139 | 204 | } |
140 | 205 |
|
141 | 206 | pub fn offset(&self) -> DVec2 { |
|
0 commit comments