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
8type 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#[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 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 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 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 #[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 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 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 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 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 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 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 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 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}