1 /** 2 * Type and dimension generic Affine Transformations backed by possibly compacted Matrices. 3 */ 4 module bettercmath.transform; 5 6 /// Options for the Transform template. 7 enum TransformOptions 8 { 9 /// Default options. 10 none = 0, 11 /// Use a compact Matrix type, as Affine Transformation matrices always 12 /// have the last row [0 ... 0 1] 13 compact = 1 << 0, 14 } 15 16 @nogc @safe pure nothrow: 17 18 /** 19 * Affine Transformation matrix. 20 * 21 * Params: 22 * T = Value types, should be numeric 23 * Dim = Space dimensions, pass 2 for 2D, 3 for 3D, etc... 24 * options = Additional options 25 */ 26 struct Transform(T, uint Dim, TransformOptions options = TransformOptions.none) 27 if (Dim > 0) 28 { 29 import std.algorithm : min; 30 31 import bettercmath.cmath : cos, sin; 32 import bettercmath.matrix : Matrix; 33 import bettercmath.misc : FloatType, degreesToRadians; 34 35 alias ElementType = T; 36 /// Transform dimension. 37 enum dimension = Dim; 38 /// Whether Transform is compact. 39 enum bool isCompact = options & TransformOptions.compact; 40 static if (!isCompact) 41 { 42 /// The underlying matrix type 43 alias MatrixType = Matrix!(T, Dim + 1, Dim + 1); 44 private alias CompactTransform = Transform!(T, Dim, TransformOptions.compact); 45 46 private static bool isAffineTransformMatrix(const MatrixType matrix) 47 { 48 import std.algorithm : equal; 49 import std.range : chain, only, repeat; 50 return matrix.rows[Dim].equal(repeat(Dim, 0).chain(only(1))); 51 } 52 } 53 else 54 { 55 /// The underlying matrix type 56 alias MatrixType = Matrix!(T, Dim + 1, Dim); 57 private alias CompactTransform = typeof(this); 58 59 private static bool isAffineTransformMatrix(const MatrixType _) 60 { 61 return true; 62 } 63 } 64 private alias FT = FloatType!T; 65 66 /// Cast between Transform types of any dimension. 67 U opCast(U : Transform!(T, Args), Args...)() const 68 { 69 typeof(return) result; 70 return copyInto(result); 71 } 72 73 /// Copy Transform contents into `target` transform of any dimension and options. 74 auto ref copyInto(Args...)(ref return Transform!(T, Args) target) const 75 { 76 copyInto(target.matrix); 77 return target; 78 } 79 80 /// Copy Transform contents into a matrix of Transform-like dimensions. 81 auto ref copyInto(uint C, uint R)(ref return Matrix!(T, C, R) target) const 82 if (C == R || C == R + 1) 83 { 84 static if (target.rowSize == this.matrix.rowSize) 85 { 86 matrix.copyInto(target); 87 } 88 else 89 { 90 enum minDimension = min(C - 1, this.dimension); 91 foreach (i; 0 .. minDimension) 92 { 93 target[i][0 .. minDimension] = this.matrix[i][0 .. minDimension]; 94 } 95 // translations must be in the last column, so copy them separately 96 target[$-1][0 .. minDimension] = this.matrix[$-1][0 .. minDimension]; 97 } 98 return target; 99 } 100 101 102 /// The underlying matrix. 103 MatrixType matrix = MatrixType.fromDiagonal(1); 104 alias matrix this; 105 106 /// The identity Transform. 107 enum identity = Transform.init; 108 109 /// Construct a Transform from matrix. 110 this()(const auto ref MatrixType mat) 111 in { assert(isAffineTransformMatrix(mat), "Matrix is not suitable for affine transformations"); } 112 do 113 { 114 this.matrix = mat; 115 } 116 117 /// Reset a Transform to identity. 118 ref Transform setIdentity() return 119 { 120 this = identity; 121 return this; 122 } 123 124 /// Transform an array of values of any dimension. 125 T[N] transform(uint N)(const auto ref T[N] values) const 126 { 127 enum minDimension = min(N, Dim + 1); 128 typeof(return) result; 129 foreach (i; 0 .. Dim) 130 { 131 T sum = 0; 132 foreach (j; 0 .. minDimension) 133 { 134 sum += matrix[j, i] * values[j]; 135 } 136 static if (N < Dim + 1) 137 { 138 sum += matrix[$-1, i]; 139 } 140 result[i] = sum; 141 } 142 return result; 143 } 144 /// Transform an array of values of any dimension. 145 auto opBinary(string op : "*", uint N)(const auto ref T[N] values) const 146 { 147 return transform(values); 148 } 149 150 /// Constructs a new Transform representing a translation. 151 static Transform fromTranslation(uint N)(const auto ref T[N] values) 152 { 153 enum minDimension = min(N, Dim); 154 Transform t; 155 t[$-1][0 .. minDimension] = values[0 .. minDimension]; 156 return t; 157 } 158 /// Apply translation in-place. 159 /// Returns: this 160 ref Transform translate(uint N)(const auto ref T[N] values) return 161 { 162 enum minDimension = min(N, Dim); 163 this[$-1][0 .. minDimension] += values[0 .. minDimension]; 164 return this; 165 } 166 /// Returns a translated copy of Transform. 167 Transform translated(uint N)(const auto ref T[N] values) const 168 { 169 Transform t = this; 170 return t.translate(values); 171 } 172 173 /// Constructs a new Transform representing a scaling. 174 static Transform fromScaling(uint N)(const auto ref T[N] values) 175 { 176 enum minDimension = min(N, Dim); 177 Transform t; 178 foreach (i; 0 .. minDimension) 179 { 180 t[i, i] = values[i]; 181 } 182 return t; 183 } 184 /// Apply scaling in-place. 185 /// Returns: this 186 ref Transform scale(uint N)(const auto ref T[N] values) return 187 { 188 return this.combine(CompactTransform.fromScaling(values)); 189 } 190 /// Returns a scaled copy of Transform. 191 Transform scaled(uint N)(const auto ref T[N] values) const 192 { 193 Transform t = this; 194 return t.scale(values); 195 } 196 197 // 2D transforms 198 static if (Dim >= 2) 199 { 200 /// Constructs a new Transform representing a shearing. 201 static Transform fromShearing(uint N)(const auto ref T[N] values) 202 { 203 enum minDimension = min(N, Dim); 204 Transform t; 205 foreach (i; 0 .. minDimension) 206 { 207 foreach (j; 0 .. Dim) 208 { 209 if (j != i) 210 { 211 t[j, i] = values[i]; 212 } 213 } 214 } 215 return t; 216 } 217 /// Apply shearing in-place. 218 /// Returns: this 219 ref Transform shear(uint N)(const auto ref T[N] values) return 220 { 221 return this.combine(CompactTransform.fromShearing(values)); 222 } 223 /// Returns a sheared copy of Transform. 224 Transform sheared(uint N)(const auto ref T[N] values) const 225 { 226 Transform t = this; 227 return t.shear(values); 228 } 229 230 /// Constructs a new Transform representing a 2D rotation. 231 /// Params: 232 /// angle = Rotation angle in radians 233 static Transform fromRotation(const FT angle) 234 { 235 Transform t; 236 immutable auto c = cos(angle), s = sin(angle); 237 t[0, 0] = c; t[0, 1] = -s; 238 t[1, 0] = s; t[1, 1] = c; 239 return t; 240 } 241 /// Constructs a new Transform representing a 2D rotation. 242 /// Params: 243 /// angle = Rotation angle in degrees 244 static auto fromRotationDegrees(const FT degrees) 245 { 246 return fromRotation(degreesToRadians(degrees)); 247 } 248 /// Apply 2D rotation in-place. 249 /// Params: 250 /// angle = Rotation angle in radians 251 /// Returns: this 252 ref Transform rotate(const FT angle) return 253 { 254 return this.combine(CompactTransform.fromRotation(angle)); 255 } 256 /// Apply 2D rotation in-place. 257 /// Params: 258 /// angle = Rotation angle in degrees 259 auto rotateDegrees(const FT degrees) 260 { 261 return rotate(degreesToRadians(degrees)); 262 } 263 /// Returns a rotated copy of Transform. 264 /// Params: 265 /// angle = Rotation angle in radians 266 Transform rotated(const FT angle) const 267 { 268 Transform t = this; 269 return t.rotate(angle); 270 } 271 /// Returns a rotated copy of Transform. 272 /// Params: 273 /// angle = Rotation angle in degrees 274 auto rotatedDegrees(const FT degrees) const 275 { 276 return rotated(degreesToRadians(degrees)); 277 } 278 } 279 // 3D transforms 280 static if (Dim >= 3) 281 { 282 /// Constructs a new Transform representing a 3D rotation aroud the X axis. 283 /// Params: 284 /// angle = Rotation angle in radians 285 static Transform fromXRotation(const FT angle) 286 { 287 Transform t; 288 immutable auto c = cos(angle), s = sin(angle); 289 t[1, 1] = c; t[2, 1] = -s; 290 t[1, 2] = s; t[2, 2] = c; 291 return t; 292 } 293 /// Constructs a new Transform representing a 3D rotation aroud the X axis. 294 /// Params: 295 /// angle = Rotation angle in degrees 296 static auto fromXRotationDegrees(const FT degrees) 297 { 298 return fromXRotation(degreesToRadians(degrees)); 299 } 300 /// Apply 3D rotation around the X axis in-place. 301 /// Params: 302 /// angle = Rotation angle in radians 303 ref Transform rotateX(const FT angle) return 304 { 305 return this.combine(CompactTransform.fromXRotation(angle)); 306 } 307 /// Apply 3D rotation around the X axis in-place. 308 /// Params: 309 /// angle = Rotation angle in degrees 310 auto rotateXDegrees(const FT degrees) 311 { 312 return rotateX(degreesToRadians(degrees)); 313 } 314 /// Returns a copy of Transform rotated around the X axis. 315 /// Params: 316 /// angle = Rotation angle in radians 317 Transform rotatedX(const FT angle) const 318 { 319 Transform t = this; 320 return t.rotateX(angle); 321 } 322 /// Returns a copy of Transform rotated around the X axis. 323 /// Params: 324 /// angle = Rotation angle in degrees 325 auto rotatedXDegrees(const FT degrees) 326 { 327 return rotatedX(degreesToRadians(degrees)); 328 } 329 330 331 /// Constructs a new Transform representing a 3D rotation aroud the Y axis. 332 /// Params: 333 /// angle = Rotation angle in radians 334 static Transform fromYRotation(const FT angle) 335 { 336 Transform t; 337 immutable auto c = cos(angle), s = sin(angle); 338 t[0, 0] = c; t[2, 0] = s; 339 t[0, 2] = -s; t[2, 2] = c; 340 return t; 341 } 342 /// Constructs a new Transform representing a 3D rotation aroud the Y axis. 343 /// Params: 344 /// angle = Rotation angle in degrees 345 static auto fromYRotationDegrees(const FT degrees) 346 { 347 return fromYRotation(degreesToRadians(degrees)); 348 } 349 /// Apply 3D rotation around the Y axis in-place. 350 /// Params: 351 /// angle = Rotation angle in radians 352 ref Transform rotateY(const FT angle) return 353 { 354 return this.combine(CompactTransform.fromYRotation(angle)); 355 } 356 /// Apply 3D rotation around the Y axis in-place. 357 /// Params: 358 /// angle = Rotation angle in degrees 359 auto rotateYDegrees(const FT degrees) 360 { 361 return rotateY(degreesToRadians(degrees)); 362 } 363 /// Returns a copy of Transform rotated around the Y axis. 364 /// Params: 365 /// angle = Rotation angle in radians 366 Transform rotatedY(const FT angle) const 367 { 368 Transform t = this; 369 return t.rotateY(angle); 370 } 371 /// Returns a copy of Transform rotated around the Y axis. 372 /// Params: 373 /// angle = Rotation angle in degrees 374 auto rotatedYDegrees(const FT degrees) 375 { 376 return rotatedY(degreesToRadians(degrees)); 377 } 378 379 // Rotating in Z is the same as rotating in 2D 380 alias fromZRotation = fromRotation; 381 alias fromZRotationDegrees = fromRotationDegrees; 382 alias rotateZ = rotate; 383 alias rotateZDegrees = rotateDegrees; 384 alias rotatedZ = rotated; 385 alias rotatedZDegrees = rotatedDegrees; 386 } 387 } 388 389 /// Pre-multiply `transformation` into `target`, returning a reference to `target` 390 auto ref combine(T, uint Dim, TransformOptions O1, TransformOptions O2)( 391 ref return Transform!(T, Dim, O1) target, 392 const auto ref Transform!(T, Dim, O2) transformation 393 ) 394 { 395 target = target.combined(transformation); 396 return target; 397 } 398 /// Returns the result of pre-multiplying `transformation` and `target` 399 Transform!(T, Dim, O1) combined(T, uint Dim, TransformOptions O1, TransformOptions O2)( 400 const auto ref Transform!(T, Dim, O1) target, 401 const auto ref Transform!(T, Dim, O2) transformation 402 ) 403 { 404 // Just about matrix multiplication, but assuming last row is [0...0 1] 405 typeof(return) result; 406 foreach (i; 0 .. Dim) 407 { 408 foreach (j; 0 .. Dim + 1) 409 { 410 T sum = 0; 411 foreach (k; 0 .. Dim) 412 { 413 sum += transformation[k, i] * target[j, k]; 414 } 415 result[j, i] = sum; 416 } 417 // Last column has to take input's last row's 1 418 result[Dim, i] += transformation[Dim, i]; 419 } 420 return result; 421 } 422 423 unittest 424 { 425 alias Transform2D = Transform!(float, 2); 426 alias Transform2DCompact = Transform!(float, 2, TransformOptions.compact); 427 alias Transform3D = Transform!(float, 3); 428 alias Transform3DCompact = Transform!(float, 3, TransformOptions.compact); 429 }