1 /*
2     Copyright © 2020, Inochi2D Project
3     Distributed under the 2-Clause BSD License, see LICENSE file.
4     
5     Authors:
6         Luna Nielsen
7         Asahi Lina
8 */
9 module inochi2d.math.transform;
10 public import inochi2d.math;
11 import inochi2d.fmt.serialize;
12 
13 /**
14     A transform
15 */
16 struct Transform {
17 private:
18     @Ignore
19     mat4 trs = mat4.identity;
20 
21     @Ignore
22     mat4 translation_ = mat4.identity;
23 
24     @Ignore
25     mat4 rotation_ = mat4.identity;
26 
27     @Ignore
28     mat4 scale_ = mat4.identity;
29 
30     @Ignore
31     mat4 rotationInv = mat4.identity;
32 
33     @Ignore
34     bool rotationInvDirty = false;
35 
36     @Ignore
37     vec3 translationCache = vec3(0, 0, 0);
38 
39     @Ignore
40     vec3 rotationCache = vec3(0, 0, 0);
41 
42     @Ignore
43     vec2 scaleCache = vec2(1, 1);
44 
45     mat4 getRotationInv() {
46         if (rotationInvDirty) {
47             rotationInv = quat.euler_rotation(this.rotation.x, this.rotation.y, this.rotation.z).inverse().to_matrix!(4, 4);
48             rotationInvDirty = false;
49         }
50         return rotationInv;
51     }
52 
53 public:
54 
55     /**
56         The translation of the transform
57     */
58     vec3 translation = vec3(0, 0, 0);
59 
60     /**
61         The rotation of the transform
62     */
63     vec3 rotation = vec3(0, 0, 0);//; = quat.identity;
64 
65     /**
66         The scale of the transform
67     */
68     vec2 scale = vec2(1, 1);
69 
70     /**
71         Locks rotation on the X axis
72     */
73     bool lockRotationX = false;
74 
75     /**
76         Locks rotation on the Y axis
77     */
78     bool lockRotationY = false;
79     
80     /**
81         Locks rotation on the Z axis
82     */
83     bool lockRotationZ = false;
84 
85     /**
86         Sets the locking value for all rotation axies
87     */
88     void lockRotation(bool value) { lockRotationX = lockRotationY = lockRotationZ = value; }
89 
90     /**
91         Locks translation on the X axis
92     */
93     bool lockTranslationX = false;
94 
95     /**
96         Locks translation on the Y axis
97     */
98     bool lockTranslationY = false;
99     
100     /**
101         Locks translation on the Z axis
102     */
103     bool lockTranslationZ = false;
104 
105     /**
106         Sets the locking value for all translation axies
107     */
108     void lockTranslation(bool value) { lockTranslationX = lockTranslationY = lockTranslationZ = value; }
109 
110     /**
111         Locks scale on the X axis
112     */
113     bool lockScaleX = false;
114 
115     /**
116         Locks scale on the Y axis
117     */
118     bool lockScaleY = false;
119 
120     /**
121         Locks all scale axies
122     */
123     void lockScale(bool value) { lockScaleX = lockScaleY = value; }
124 
125     /**
126         Whether the transform should snap to pixels
127     */
128     bool pixelSnap = false;
129 
130     /**
131         Initialize a transform
132     */
133     this(vec3 translation, vec3 rotation = vec3(0), vec2 scale = vec2(1, 1)) {
134         this.translation = translation;
135         this.rotation = rotation;
136         this.scale = scale;
137     }
138 
139     /**
140         Returns the result of 2 transforms multiplied together
141     */
142     Transform opBinary(string op : "*")(Transform other) {
143         Transform tnew;
144 
145         //
146         //  ROTATION
147         //
148 
149         quat rot = quat.from_matrix(mat3(this.rotation_ * other.rotation_));
150 
151         // Handle rotation locks
152         if (!lockRotationX) tnew.rotation.x = rot.roll;
153         else tnew.rotation.x = this.rotation.x;
154         
155         if (!lockRotationY) tnew.rotation.y = rot.pitch;
156         else tnew.rotation.y = this.rotation.y;
157 
158         if (!lockRotationZ) tnew.rotation.z = rot.yaw;
159         else tnew.rotation.z = this.rotation.z;
160 
161         //
162         //  SCALE
163         //
164 
165         // Handle scale locks
166         vec2 scale = vec2(this.scale_ * vec4(other.scale, 1, 1));
167         if (!lockScaleX) tnew.scale.x = scale.x;
168         if (!lockScaleY) tnew.scale.y = scale.y;
169 
170         //
171         //  TRANSLATION
172         //
173         mat4 otherScaleInv = mat4.scaling(1 / other.scale.x, 1 / other.scale.y, 1);
174 
175         // Calculate new TRS
176         vec3 trans = vec3(
177             // We want to support parts being placed correctly even if they're rotation or scale locked
178             // therefore we need to apply the worldspace rotation and scale here
179             // That has been pre-calculated above.
180             // Do note we also multiply by its inverse, this is so that the rotations and scaling doesn't
181             // start stacking up weirdly causing cascadingly more extreme transformation.
182             other.scale_ * other.rotation_ * this.translation_ * other.getRotationInv() * otherScaleInv *
183 
184             // Also our local translation
185             vec4(other.translation, 1)
186         );
187 
188         // Handle translation
189         tnew.translation.x = pixelSnap ? round(trans.x) : trans.x;
190         tnew.translation.y = pixelSnap ? round(trans.y) : trans.y;
191         tnew.translation.z = pixelSnap ? round(trans.z) : trans.z;
192         tnew.update();
193 
194         return tnew;
195     }
196 
197     /**
198         Gets the matrix for this transform
199     */
200     @Ignore
201     mat4 matrix() {
202         return trs;
203     }
204 
205     /**
206         Updates the internal matrix of this transform
207     */
208     void update() {
209         bool recalc = false;
210 
211         if (translation != translationCache) {
212             translation_ = mat4.translation(translation);
213             recalc = true;
214             translationCache = translation;
215         }
216         if (rotation != rotationCache) {
217             rotation_ = quat.euler_rotation(this.rotation.x, this.rotation.y, this.rotation.z).to_matrix!(4, 4);
218             rotationInvDirty = true;
219             recalc = true;
220             rotationCache = rotation;
221         }
222         if (scale != scaleCache) {
223             scale_ = mat4.scaling(scale.x, scale.y, 1);
224             recalc = true;
225             scaleCache = scale;
226         }
227 
228         if (recalc)
229             trs = translation_ * rotation_ * scale_;
230     }
231 
232     void clear() {
233         translation = vec3(0);
234         rotation = vec3(0);
235         scale = vec2(1, 1);
236     }
237 
238     @Ignore
239     string toString() {
240         import std.format : format;
241         return "%s,\n%s,\n%s\n%s".format(trs.toPrettyString, translation.toString, rotation.toString, scale.toString);
242     }
243 
244     void serialize(S)(ref S serializer) {
245         auto state = serializer.objectBegin();
246             serializer.putKey("trans");
247             translation.serialize(serializer);
248 
249             serializer.putKey("rot");
250             rotation.serialize(serializer);
251 
252             if (lockRotationX || lockRotationY || lockRotationZ) {
253                 serializer.putKey("rot_lock");
254                 serializer.serializeValue([lockRotationX, lockRotationY, lockRotationZ]);
255             }
256 
257             serializer.putKey("scale");
258             scale.serialize(serializer);
259 
260             if (lockScaleX || lockScaleY) {
261                 serializer.putKey("scale_lock");
262                 serializer.serializeValue([lockScaleX, lockScaleY]);
263             }
264 
265         serializer.objectEnd(state);
266     }
267 
268     SerdeException deserializeFromFghj(Fghj data) {
269         translation.deserialize(data["trans"]);
270         rotation.deserialize(data["rot"]);
271         scale.deserialize(data["scale"]);
272         
273         if (data["rot_lock"] != Fghj.init) {
274             bool[] states;
275             data["rot_lock"].deserializeValue(states);
276 
277             this.lockRotationX = states[0];
278             this.lockRotationY = states[1];
279             this.lockRotationZ = states[2];
280         }
281         
282         if (data["scale_lock"] != Fghj.init) {
283             bool[] states;
284             data["scale_lock"].deserializeValue(states);
285             this.lockScaleX = states[0];
286             this.lockScaleY = states[1];
287         }
288         return null;
289     }
290 }
291 /**
292     A 2D transform;
293 */
294 struct Transform2D {
295 private:
296     @Ignore
297     mat3 trs;
298 
299 public:
300     /**
301         Translate
302     */
303     vec2 translation;
304     /**
305         Scale
306     */
307     vec2 scale;
308     
309     /**
310         Rotation
311     */
312     float rotation;
313 
314     /**
315         Gets the matrix for this transform
316     */
317     mat3 matrix() {
318         return trs;
319     }
320 
321     /**
322         Updates the internal matrix of this transform
323     */
324     void update() {
325         mat3 translation_ = mat3.translation(vec3(translation, 0));
326         mat3 rotation_ = mat3.zrotation(rotation);
327         mat3 scale_ = mat3.scaling(scale.x, scale.y, 1);
328         trs =  translation_ * rotation_ * scale_;
329     }
330 
331 }