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 }