1 /** 2 2D Transformation. 3x2 matrix. 3 4 Copyright: 5 Copyright (c) 2007 Juan Linietsky, Ariel Manzur. 6 Copyright (c) 2014 Godot Engine contributors (cf. AUTHORS.md) 7 Copyright (c) 2017 Godot-D contributors 8 Copyright (c) 2022 Godot-DLang contributors 9 10 License: $(LINK2 https://opensource.org/licenses/MIT, MIT License) 11 12 13 */ 14 module godot.transform2d; 15 16 import godot.api.types; 17 import godot.vector2; 18 import godot.rect2; 19 20 import std.math; 21 import std.algorithm.comparison; 22 import std.algorithm.mutation : swap; 23 24 /** 25 Represents one or many transformations in 2D space such as translation, rotation, or scaling. It is similar to a 3x2 matrix. 26 */ 27 struct Transform2D { 28 @nogc nothrow: 29 30 union { 31 Vector2[3] columns = [Vector2(1, 0), Vector2(0, 1), Vector2(0, 0)]; 32 struct { 33 Vector2 x_axis; /// 34 Vector2 y_axis; /// 35 Vector2 origin; /// 36 } 37 } 38 39 real_t tdotx(in Vector2 v) const { 40 return columns[0][0] * v.x + columns[1][0] * v.y; 41 } 42 43 real_t tdoty(in Vector2 v) const { 44 return columns[0][1] * v.x + columns[1][1] * v.y; 45 } 46 47 this(real_t xx, real_t xy, real_t yx, real_t yy, real_t ox, real_t oy) { 48 columns[0][0] = xx; 49 columns[0][1] = xy; 50 columns[1][0] = yx; 51 columns[1][1] = yy; 52 columns[2][0] = ox; 53 columns[2][1] = oy; 54 } 55 56 const(Vector2) opIndex(int axis) const { 57 return columns[axis]; 58 } 59 60 ref Vector2 opIndex(int axis) return { 61 return columns[axis]; 62 } 63 64 Vector2 basisXform(in Vector2 v) const { 65 return Vector2( 66 tdotx(v), 67 tdoty(v) 68 ); 69 } 70 71 Vector2 basisXformInv(in Vector2 v) const { 72 return Vector2( 73 columns[0].dot(v), 74 columns[1].dot(v) 75 ); 76 } 77 78 Vector2 xform(in Vector2 v) const { 79 return Vector2( 80 tdotx(v), 81 tdoty(v) 82 ) + columns[2]; 83 } 84 85 Vector2 xformInv(in Vector2 p_vec) const { 86 Vector2 v = p_vec - columns[2]; 87 88 return Vector2( 89 columns[0].dot(v), 90 columns[1].dot(v) 91 ); 92 } 93 94 Rect2 xform(in Rect2 p_rect) const { 95 Vector2 x = columns[0] * p_rect.size.x; 96 Vector2 y = columns[1] * p_rect.size.y; 97 Vector2 pos = xform(p_rect.position); 98 99 Rect2 new_rect; 100 new_rect.position = pos; 101 new_rect.expandTo(pos + x); 102 new_rect.expandTo(pos + y); 103 new_rect.expandTo(pos + x + y); 104 return new_rect; 105 } 106 107 void setRotationAndScale(real_t p_rot, in Vector2 p_scale) { 108 columns[0][0] = cos(p_rot) * p_scale.x; 109 columns[1][1] = cos(p_rot) * p_scale.y; 110 columns[1][0] = -sin(p_rot) * p_scale.y; 111 columns[0][1] = sin(p_rot) * p_scale.x; 112 113 } 114 115 Rect2 xformInv(in Rect2 p_rect) const { 116 Vector2[4] ends = [ 117 xformInv(p_rect.position), 118 xformInv(Vector2(p_rect.position.x, p_rect.position.y + p_rect.size.y)), 119 xformInv(Vector2(p_rect.position.x + p_rect.size.x, p_rect.position.y + p_rect.size.y)), 120 xformInv(Vector2(p_rect.position.x + p_rect.size.x, p_rect.position.y)) 121 ]; 122 123 Rect2 new_rect; 124 new_rect.position = ends[0]; 125 new_rect.expandTo(ends[1]); 126 new_rect.expandTo(ends[2]); 127 new_rect.expandTo(ends[3]); 128 129 return new_rect; 130 } 131 132 void invert() { 133 // FIXME: this function assumes the basis is a rotation matrix, with no scaling. 134 // affine_inverse can handle matrices with scaling, so GDScript should eventually use that. 135 swap(columns[0][1], columns[1][0]); 136 columns[2] = basisXform(-columns[2]); 137 } 138 139 Transform2D inverse() const { 140 Transform2D inv = this; 141 inv.invert(); 142 return inv; 143 144 } 145 146 void affineInvert() { 147 real_t det = basisDeterminant(); 148 ///ERR_FAIL_COND(det==0); 149 real_t idet = 1.0 / det; 150 151 swap(columns[0][0], columns[1][1]); 152 columns[0] *= Vector2(idet, -idet); 153 columns[1] *= Vector2(-idet, idet); 154 155 columns[2] = basisXform(-columns[2]); 156 157 } 158 159 Transform2D affineInverse() const { 160 Transform2D inv = this; 161 inv.affineInvert(); 162 return inv; 163 } 164 165 void rotate(real_t p_phi) { 166 this = Transform2D(p_phi, Vector2()) * (this); 167 } 168 169 real_t getRotation() const { 170 real_t det = basisDeterminant(); 171 Transform2D m = orthonormalized(); 172 if (det < 0) { 173 m.scaleBasis(Vector2(-1, -1)); 174 } 175 return atan2(m[0].y, m[0].x); 176 } 177 178 void setRotation(real_t p_rot) { 179 real_t cr = cos(p_rot); 180 real_t sr = sin(p_rot); 181 columns[0][0] = cr; 182 columns[0][1] = sr; 183 columns[1][0] = -sr; 184 columns[1][1] = cr; 185 } 186 187 this(real_t p_rot, in Vector2 p_pos) { 188 real_t cr = cos(p_rot); 189 real_t sr = sin(p_rot); 190 columns[0][0] = cr; 191 columns[0][1] = sr; 192 columns[1][0] = -sr; 193 columns[1][1] = cr; 194 columns[2] = p_pos; 195 } 196 197 Vector2 getScale() const { 198 real_t det_sign = basisDeterminant() > 0 ? 1 : -1; 199 return det_sign * Vector2(columns[0].length(), columns[1].length()); 200 } 201 202 void scale(in Vector2 p_scale) { 203 scaleBasis(p_scale); 204 columns[2] *= p_scale; 205 } 206 207 void scaleBasis(in Vector2 p_scale) { 208 columns[0][0] *= p_scale.x; 209 columns[0][1] *= p_scale.y; 210 columns[1][0] *= p_scale.x; 211 columns[1][1] *= p_scale.y; 212 213 } 214 215 void translate(real_t p_tx, real_t p_ty) { 216 translate(Vector2(p_tx, p_ty)); 217 } 218 219 void translate(in Vector2 p_translation) { 220 columns[2] += basisXform(p_translation); 221 } 222 223 void orthonormalize() { 224 // Gram-Schmidt Process 225 226 Vector2 x = columns[0]; 227 Vector2 y = columns[1]; 228 229 x.normalize(); 230 y = (y - x * (x.dot(y))); 231 y.normalize(); 232 233 columns[0] = x; 234 columns[1] = y; 235 } 236 237 Transform2D orthonormalized() const { 238 Transform2D on = this; 239 on.orthonormalize(); 240 return on; 241 242 } 243 244 void opOpAssign(string op : "*")(in Transform2D p_transform) { 245 columns[2] = xform(p_transform.columns[2]); 246 247 real_t x0, x1, y0, y1; 248 249 x0 = tdotx(p_transform.columns[0]); 250 x1 = tdoty(p_transform.columns[0]); 251 y0 = tdotx(p_transform.columns[1]); 252 y1 = tdoty(p_transform.columns[1]); 253 254 columns[0][0] = x0; 255 columns[0][1] = x1; 256 columns[1][0] = y0; 257 columns[1][1] = y1; 258 } 259 260 Transform2D opBinary(string op : "*")(in Transform2D p_transform) const { 261 Transform2D t = this; 262 t *= p_transform; 263 return t; 264 265 } 266 267 Transform2D scaled(in Vector2 p_scale) const { 268 Transform2D copy = this; 269 copy.scale(p_scale); 270 return copy; 271 272 } 273 274 Transform2D basisScaled(in Vector2 p_scale) const { 275 Transform2D copy = this; 276 copy.scaleBasis(p_scale); 277 return copy; 278 279 } 280 281 Transform2D untranslated() const { 282 Transform2D copy = this; 283 copy.columns[2] = Vector2(); 284 return copy; 285 } 286 287 Transform2D translated(in Vector2 p_offset) const { 288 Transform2D copy = this; 289 copy.translate(p_offset); 290 return copy; 291 } 292 293 Transform2D rotated(real_t p_phi) const { 294 Transform2D copy = this; 295 copy.rotate(p_phi); 296 return copy; 297 298 } 299 300 real_t basisDeterminant() const { 301 return columns[0].x * columns[1].y - columns[0].y * columns[1].x; 302 } 303 304 Transform2D interpolateWith(in Transform2D p_transform, real_t p_c) const { 305 //extract parameters 306 Vector2 p1 = origin; 307 Vector2 p2 = p_transform.origin; 308 309 real_t r1 = getRotation(); 310 real_t r2 = p_transform.getRotation(); 311 312 Vector2 s1 = getScale(); 313 Vector2 s2 = p_transform.getScale(); 314 315 //slerp rotation 316 Vector2 v1 = Vector2(cos(r1), sin(r1)); 317 Vector2 v2 = Vector2(cos(r2), sin(r2)); 318 319 real_t dot = v1.dot(v2); 320 321 dot = (dot < -1.0) ? -1.0 : ((dot > 1.0) ? 1.0 : dot); //clamp dot to [-1,1] 322 323 Vector2 v; 324 325 if (dot > 0.9995) { 326 v = Vector2.linearInterpolate(v1, v2, p_c).normalized(); //linearly interpolate to avoid numerical precision issues 327 } else { 328 real_t angle = p_c * acos(dot); 329 Vector2 v3 = (v2 - v1 * dot).normalized(); 330 v = v1 * cos(angle) + v3 * sin(angle); 331 } 332 333 //construct matrix 334 Transform2D res = Transform2D(atan2(v.y, v.x), Vector2.linearInterpolate(p1, p2, p_c)); 335 res.scaleBasis(Vector2.linearInterpolate(s1, s2, p_c)); 336 return res; 337 } 338 }