Skip to main content

ferroui_base/
rotate_3d_transform.rs

1use crate::media::{Transform, TransformImpl};
2use crate::{
3    ferro_class, ferro_property, instantiate, FerroObjectImpl, FerroObjectImplExt, FerroProperty,
4    FerroPropertyChangedEventArgs, Matrix, Ref, StyledProperty,
5};
6use std::cell::Cell;
7
8/// A row-major 4x4 single-precision matrix using the row-vector convention.
9type Matrix4x4 = [[f32; 4]; 4];
10
11const IDENTITY: Matrix4x4 =
12    [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]];
13
14fn multiply(a: &Matrix4x4, b: &Matrix4x4) -> Matrix4x4 {
15    let mut result = [[0.0f32; 4]; 4];
16    for (i, row) in result.iter_mut().enumerate() {
17        for (j, cell) in row.iter_mut().enumerate() {
18            *cell = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j] + a[i][3] * b[3][j];
19        }
20    }
21    result
22}
23
24fn create_translation(x: f32, y: f32, z: f32) -> Matrix4x4 {
25    let mut result = IDENTITY;
26    result[3][0] = x;
27    result[3][1] = y;
28    result[3][2] = z;
29    result
30}
31
32fn create_rotation_x(radians: f32) -> Matrix4x4 {
33    let (s, c) = radians.sin_cos();
34    let mut result = IDENTITY;
35    result[1][1] = c;
36    result[1][2] = s;
37    result[2][1] = -s;
38    result[2][2] = c;
39    result
40}
41
42fn create_rotation_y(radians: f32) -> Matrix4x4 {
43    let (s, c) = radians.sin_cos();
44    let mut result = IDENTITY;
45    result[0][0] = c;
46    result[0][2] = -s;
47    result[2][0] = s;
48    result[2][2] = c;
49    result
50}
51
52fn create_rotation_z(radians: f32) -> Matrix4x4 {
53    let (s, c) = radians.sin_cos();
54    let mut result = IDENTITY;
55    result[0][0] = c;
56    result[0][1] = s;
57    result[1][0] = -s;
58    result[1][1] = c;
59    result
60}
61
62/// Non-affine 3D transformation for rotating a visual around a definable
63/// axis.
64#[repr(C)]
65pub struct Rotate3DTransform {
66    base: Transform,
67    is_initializing: Cell<bool>,
68}
69
70ferro_class!(Rotate3DTransform: Transform);
71crate::ferro_class_info!(Rotate3DTransform { new: Rotate3DTransform::new });
72
73impl FerroObjectImpl for Rotate3DTransform {
74    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
75        Self::parent_on_property_changed(this, change);
76
77        if !this.is_initializing.get() {
78            this.raise_changed();
79        }
80    }
81}
82
83impl TransformImpl for Rotate3DTransform {
84    fn value(this: &Self) -> Matrix {
85        let mut matrix44 = IDENTITY;
86        // Copy values first, because it's not guaranteed that values will not
87        // change during calculation.
88        let (copy_center_x, copy_center_y, copy_center_z, copy_angle_x, copy_angle_y, copy_angle_z, copy_depth) = (
89            this.center_x(),
90            this.center_y(),
91            this.center_z(),
92            this.angle_x(),
93            this.angle_y(),
94            this.angle_z(),
95            this.depth(),
96        );
97
98        // The smallest positive subnormal double.
99        const EPSILON: f64 = f64::from_bits(1);
100
101        let center_sum = copy_center_x + copy_center_y + copy_center_z;
102
103        if center_sum.abs() > EPSILON {
104            matrix44 = multiply(
105                &matrix44,
106                &create_translation(-(copy_center_x as f32), -(copy_center_y as f32), -(copy_center_z as f32)),
107            );
108        }
109
110        if copy_angle_x != 0.0 {
111            matrix44 = multiply(&matrix44, &create_rotation_x(Matrix::to_radians(copy_angle_x) as f32));
112        }
113        if copy_angle_y != 0.0 {
114            matrix44 = multiply(&matrix44, &create_rotation_y(Matrix::to_radians(copy_angle_y) as f32));
115        }
116        if copy_angle_z != 0.0 {
117            matrix44 = multiply(&matrix44, &create_rotation_z(Matrix::to_radians(copy_angle_z) as f32));
118        }
119
120        if center_sum.abs() > EPSILON {
121            matrix44 = multiply(
122                &matrix44,
123                &create_translation(copy_center_x as f32, copy_center_y as f32, copy_center_z as f32),
124            );
125        }
126
127        if copy_depth != 0.0 {
128            let mut perspective_matrix = IDENTITY;
129            perspective_matrix[2][3] = -1.0 / copy_depth as f32;
130            matrix44 = multiply(&matrix44, &perspective_matrix);
131        }
132
133        Matrix::new_3x3(
134            matrix44[0][0] as f64,
135            matrix44[0][1] as f64,
136            matrix44[0][3] as f64,
137            matrix44[1][0] as f64,
138            matrix44[1][1] as f64,
139            matrix44[1][3] as f64,
140            matrix44[3][0] as f64,
141            matrix44[3][1] as f64,
142            matrix44[3][3] as f64,
143        )
144    }
145}
146
147crate::ferro_properties! { impl Rotate3DTransform {
148    ferro_property!(pub fn angle_x_property() -> StyledProperty<f64> {
149        FerroProperty::register::<Rotate3DTransform, _>("AngleX", 0.0)
150    });
151
152    ferro_property!(pub fn angle_y_property() -> StyledProperty<f64> {
153        FerroProperty::register::<Rotate3DTransform, _>("AngleY", 0.0)
154    });
155
156    ferro_property!(pub fn angle_z_property() -> StyledProperty<f64> {
157        FerroProperty::register::<Rotate3DTransform, _>("AngleZ", 0.0)
158    });
159
160    ferro_property!(pub fn center_x_property() -> StyledProperty<f64> {
161        FerroProperty::register::<Rotate3DTransform, _>("CenterX", 0.0)
162    });
163
164    ferro_property!(pub fn center_y_property() -> StyledProperty<f64> {
165        FerroProperty::register::<Rotate3DTransform, _>("CenterY", 0.0)
166    });
167
168    ferro_property!(pub fn center_z_property() -> StyledProperty<f64> {
169        FerroProperty::register::<Rotate3DTransform, _>("CenterZ", 0.0)
170    });
171
172    ferro_property!(pub fn depth_property() -> StyledProperty<f64> {
173        FerroProperty::register::<Rotate3DTransform, _>("Depth", 0.0)
174    });
175} }
176
177impl Rotate3DTransform {
178    /// Creates the class data.
179    pub fn construct() -> Self {
180        Self { base: Transform::construct(), is_initializing: Cell::new(false) }
181    }
182
183    pub fn new() -> Ref<Self> {
184        instantiate(Self::construct())
185    }
186
187    /// Creates a transform with the given rotation angles (in degrees),
188    /// rotation center and depth.
189    #[allow(clippy::too_many_arguments)]
190    pub fn with_values(
191        angle_x: f64,
192        angle_y: f64,
193        angle_z: f64,
194        center_x: f64,
195        center_y: f64,
196        center_z: f64,
197        depth: f64,
198    ) -> Ref<Self> {
199        let result = Self::new();
200        result.is_initializing.set(true);
201        result.set_angle_x(angle_x);
202        result.set_angle_y(angle_y);
203        result.set_angle_z(angle_z);
204        result.set_center_x(center_x);
205        result.set_center_y(center_y);
206        result.set_center_z(center_z);
207        result.set_depth(depth);
208        result.is_initializing.set(false);
209        result
210    }
211
212    /// The rotation around the X-axis, in degrees.
213    pub fn angle_x(&self) -> f64 {
214        self.get_value(Self::angle_x_property())
215    }
216
217    pub fn set_angle_x(&self, value: f64) {
218        self.set_value(Self::angle_x_property(), value)
219    }
220
221    /// The rotation around the Y-axis, in degrees.
222    pub fn angle_y(&self) -> f64 {
223        self.get_value(Self::angle_y_property())
224    }
225
226    pub fn set_angle_y(&self, value: f64) {
227        self.set_value(Self::angle_y_property(), value)
228    }
229
230    /// The rotation around the Z-axis, in degrees.
231    pub fn angle_z(&self) -> f64 {
232        self.get_value(Self::angle_z_property())
233    }
234
235    pub fn set_angle_z(&self, value: f64) {
236        self.set_value(Self::angle_z_property(), value)
237    }
238
239    /// The X-coordinate of the rotation center.
240    pub fn center_x(&self) -> f64 {
241        self.get_value(Self::center_x_property())
242    }
243
244    pub fn set_center_x(&self, value: f64) {
245        self.set_value(Self::center_x_property(), value)
246    }
247
248    /// The Y-coordinate of the rotation center.
249    pub fn center_y(&self) -> f64 {
250        self.get_value(Self::center_y_property())
251    }
252
253    pub fn set_center_y(&self, value: f64) {
254        self.set_value(Self::center_y_property(), value)
255    }
256
257    /// The Z-coordinate of the rotation center.
258    pub fn center_z(&self) -> f64 {
259        self.get_value(Self::center_z_property())
260    }
261
262    pub fn set_center_z(&self, value: f64) {
263        self.set_value(Self::center_z_property(), value)
264    }
265
266    /// The focal length of the camera.
267    pub fn depth(&self) -> f64 {
268        self.get_value(Self::depth_property())
269    }
270
271    pub fn set_depth(&self, value: f64) {
272        self.set_value(Self::depth_property(), value)
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279    use std::rc::Rc;
280
281    #[test]
282    fn default_value_is_identity() {
283        assert_eq!(Matrix::IDENTITY, Rotate3DTransform::new().value());
284    }
285
286    #[test]
287    fn rotation_around_z_matches_2d_rotation() {
288        let target = Rotate3DTransform::with_values(0.0, 0.0, 90.0, 0.0, 0.0, 0.0, 0.0);
289        let value = target.value();
290        let expected = Matrix::create_rotation(Matrix::to_radians(90.0));
291        assert!((value.m11 - expected.m11).abs() < 1e-6);
292        assert!((value.m12 - expected.m12).abs() < 1e-6);
293        assert!((value.m21 - expected.m21).abs() < 1e-6);
294        assert!((value.m22 - expected.m22).abs() < 1e-6);
295        assert!(!value.contains_perspective());
296    }
297
298    #[test]
299    fn depth_and_x_rotation_produce_perspective() {
300        let target = Rotate3DTransform::with_values(60.0, 0.0, 0.0, 0.0, 0.0, 0.0, 200.0);
301        let value = target.value();
302        // M22 = cos(60deg), M23 (perspective Y) = sin(60deg) * (-1 / depth).
303        assert!((value.m22 - 0.5).abs() < 1e-6);
304        assert!((value.m23 - (-(60f64.to_radians().sin()) / 200.0)).abs() < 1e-6);
305        assert!(value.contains_perspective());
306    }
307
308    #[test]
309    fn changed_is_raised_for_every_property_but_not_while_initializing() {
310        let target = Rotate3DTransform::with_values(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0);
311        let count = Rc::new(Cell::new(0));
312        let c = count.clone();
313        target.changed(move || c.set(c.get() + 1));
314        target.set_angle_x(10.0);
315        target.set_center_z(10.0);
316        target.set_depth(10.0);
317        assert_eq!(3, count.get());
318    }
319}