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 }