1 /// Affine transforms module
2 module gfx.math.transform;
3 
4 import gfx.math.mat;
5 import gfx.math.vec;
6 import std.meta : allSatisfy;
7 import std.traits : CommonType, isFloatingPoint, isNumeric;
8 
9 @safe pure:
10 
11 /// Build a translation matrix.
12 auto translation(X, Y)(in X x, in Y y)
13 {
14     alias ResMat = Mat3x3!(CommonType!(X, Y));
15     return ResMat(
16         1, 0, x,
17         0, 1, y,
18         0, 0, 1,
19     );
20 }
21 
22 /// ditto
23 auto translation(V)(in V v) if (isVec2!V)
24 {
25     return Mat3x3!(V.Component) (
26         1, 0, v.x,
27         0, 1, v.y,
28         0, 0, 1,
29     );
30 }
31 
32 /// ditto
33 auto translation(X, Y, Z)(in X x, in Y y, in Z z)
34 {
35     alias ResMat = Mat4x4!(CommonType!(X, Y, Z));
36     return ResMat(
37         1, 0, 0, x,
38         0, 1, 0, y,
39         0, 0, 1, z,
40         0, 0, 0, 1,
41     );
42 }
43 
44 /// ditto
45 auto translation(V)(in V v) if (isVec3!V)
46 {
47     return Mat4x4!(V.Component) (
48         1, 0, 0, v.x,
49         0, 1, 0, v.y,
50         0, 0, 1, v.z,
51         0, 0, 0, 1,
52     );
53 }
54 
55 unittest
56 {
57     import gfx.math.approx : approxUlp;
58 
59     immutable v2 = dvec(4, 6);
60     assert( approxUlp(translation(2, 7) * dvec(v2, 1), dvec(6, 13, 1)) );
61 
62     immutable v3 = dvec(5, 6, 7);
63     assert( approxUlp(translation(7, 4, 1) * dvec(v3, 1), dvec(12, 10, 8, 1)) );
64 }
65 
66 /// Append a translation transform inferred from arguments to the matrix m.
67 /// This is equivalent to the expression $(D_CODE translation(...) * m)
68 /// but actually save computation by knowing
69 /// where the ones and zeros are in a pure translation matrix.
70 M translate(M, X, Y)(in M m, in X x, in Y y)
71 if (isMat!(3, 3, M) && allSatisfy!(isNumeric, X, Y))
72 {
73     return M (
74         // row 1
75         m[0, 0] + m[2, 0] * x,
76         m[0, 1] + m[2, 1] * x,
77         m[0, 2] + m[2, 2] * x,
78         // row 2
79         m[1, 0] + m[2, 0] * y,
80         m[1, 1] + m[2, 1] * y,
81         m[1, 2] + m[2, 2] * y,
82         // row 3
83         m[2, 0], m[2, 1], m[2, 2]
84     );
85 }
86 
87 /// ditto
88 M translate(M, X, Y)(in M m, in X x, in Y y)
89 if (isMat!(2, 3, M) && allSatisfy!(isNumeric, X, Y))
90 {
91     return M (
92         // row 1
93         m[0, 0],
94         m[0, 1],
95         m[0, 2] + x,
96         // row 2
97         m[1, 0],
98         m[1, 1],
99         m[1, 2] + y,
100     );
101 }
102 
103 /// ditto
104 M translate(M, V)(in M m, in V v)
105 if ((isMat!(2, 3, M) || isMat!(3, 3, M)) && isVec!(2, V))
106 {
107     return translate (m, v[0] ,v[1]);
108 }
109 
110 /// ditto
111 M translate (M, X, Y, Z)(in M m, in X x, in Y y, in Z z)
112 if (isMat!(4, 4, M) && allSatisfy!(isNumeric, X, Y, Z))
113 {
114     return M (
115         // row 1
116         m[0, 0] + m[3, 0] * x,
117         m[0, 1] + m[3, 1] * x,
118         m[0, 2] + m[3, 2] * x,
119         m[0, 3] + m[3, 3] * x,
120         // row 2
121         m[1, 0] + m[3, 0] * y,
122         m[1, 1] + m[3, 1] * y,
123         m[1, 2] + m[3, 2] * y,
124         m[1, 3] + m[3, 3] * y,
125         // row 3
126         m[2, 0] + m[3, 0] * z,
127         m[2, 1] + m[3, 1] * z,
128         m[2, 2] + m[3, 2] * z,
129         m[2, 3] + m[3, 3] * z,
130         // row 4
131         m[3, 0], m[3, 1], m[3, 2], m[3, 3]
132     );
133 }
134 
135 /// ditto
136 M translate (M, X, Y, Z)(in M m, in X x, in Y y, in Z z)
137 if (isMat!(3, 4, M) && allSatisfy!(isNumeric, X, Y, Z))
138 {
139     return M (
140         // row 1
141         m[0, 0] + m[3, 0] * x,
142         m[0, 1] + m[3, 1] * x,
143         m[0, 2] + m[3, 2] * x,
144         m[0, 3] + m[3, 3] * x,
145         // row 2
146         m[1, 0] + m[3, 0] * y,
147         m[1, 1] + m[3, 1] * y,
148         m[1, 2] + m[3, 2] * y,
149         m[1, 3] + m[3, 3] * y,
150         // row 3
151         m[2, 0] + m[3, 0] * z,
152         m[2, 1] + m[3, 1] * z,
153         m[2, 2] + m[3, 2] * z,
154         m[2, 3] + m[3, 3] * z,
155     );
156 }
157 
158 /// ditto
159 M translate(M, V)(in M m, in V v)
160 if ((isMat!(3, 4, M) || isMat!(4, 4, M)) && isVec!(3, V))
161 {
162     return translate (m, v[0] ,v[1], v[2]);
163 }
164 
165 ///
166 unittest
167 {
168     import gfx.math.approx : approxUlp;
169 
170     immutable m = DMat3( 1, 2, 3, 4, 5, 6, 7, 8, 9 );
171 
172     immutable expected = translation(7, 12) * m;  // full multiplication
173     immutable result = translate(m, 7, 12);       // simplified multiplication
174 
175     assert (approxUlp(expected, result));
176 }
177 ///
178 unittest
179 {
180     import gfx.math.approx : approxUlp;
181 
182     immutable m = DMat4( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 );
183 
184     immutable expected = translation(7, 12, 18) * m;  // full multiplication
185     immutable result = translate(m, 7, 12, 18);       // simplified multiplication
186 
187     assert (approxUlp(expected, result));
188 }
189 
190 
191 /// Build a pure 3d rotation matrix with angle in radians
192 auto rotationPure(T, V) (in T angle, in V axis)
193 if (isFloatingPoint!T && isVec!(3, V))
194 {
195     static assert (
196         isFloatingPoint!(V.Component),
197         "rotationPure must be passed a floating point axis"
198     );
199     import std.math : cos, sin;
200     const u = normalize(axis);
201     const c = cos(angle);
202     const s = sin(angle);
203     const c1 = 1 - c;
204     return Mat3x3!(V.Component) (
205         // row 1
206         c1 * u.x * u.x  +  c,
207         c1 * u.x * u.y  -  s * u.z,
208         c1 * u.x * u.z  +  s * u.y,
209         // row 2
210         c1 * u.y * u.x  +  s * u.z,
211         c1 * u.y * u.y  +  c,
212         c1 * u.y * u.z  -  s * u.x,
213         // row 3
214         c1 * u.z * u.x  -  s * u.y,
215         c1 * u.z * u.y  +  s * u.x,
216         c1 * u.z * u.z  +  c,
217     );
218 }
219 
220 /// Build a rotation matrix.
221 /// angle in radians.
222 Mat3x3!T rotation(T) (in T angle) if (isFloatingPoint!T)
223 {
224     import std.math : cos, sin;
225     const c = cast(T) cos(angle);
226     const s = cast(T) sin(angle);
227     return Mat3x3!T (
228         c, -s, 0,
229         s, c, 0,
230         0, 0, 1
231     );
232 }
233 
234 /// ditto
235 auto rotation(T, V) (in T angle, in V axis)
236 if (isVec!(3, V) && isFloatingPoint!T)
237 {
238     static assert (
239         isFloatingPoint!(V.Component),
240         "rotation must be passed a floating point axis"
241     );
242     const m = rotationPure(angle, axis);
243     return mat(
244         vec(m[0], 0),
245         vec(m[1], 0),
246         vec(m[2], 0),
247         vec(0, 0,  0,  1)
248     );
249 }
250 
251 /// ditto
252 auto rotation(T) (in T angle, in T x, in T y, in T z)
253 if (isFloatingPoint!T)
254 {
255     return rotation(angle, vec(x, y, z));
256 }
257 
258 /// Append a rotation transform inferred from arguments to the matrix m.
259 /// This is equivalent to the expression $(D_CODE rotation(...) * m)
260 /// but actually save computation by knowing
261 /// where the ones and zeros are in a pure rotation matrix.
262 M rotate (M, T) (in M m, in T angle)
263 if (isMat!(3, 3, M) && isFloatingPoint!T)
264 {
265     import std.math : cos, sin;
266     immutable c = cos(angle);
267     immutable s = sin(angle);
268     return M (
269         // row 1
270         c * m[0, 0] - s * m[1, 0],
271         c * m[0, 1] - s * m[1, 1],
272         c * m[0, 2] - s * m[1, 2],
273         // row 2
274         s * m[0, 0] + c * m[1, 0],
275         s * m[0, 1] + c * m[1, 1],
276         s * m[0, 2] + c * m[1, 2],
277         // row 3
278         m[2, 0], m[2, 1], m[2, 2]
279     );
280 }
281 
282 /// ditto
283 M rotate (M, T) (in M m, in T angle)
284 if (isMat!(2, 3, M) && isFloatingPoint!T)
285 {
286     import std.math : cos, sin;
287     immutable c = cos(angle);
288     immutable s = sin(angle);
289     return M (
290         // row 1
291         c * m[0, 0] - s * m[1, 0],
292         c * m[0, 1] - s * m[1, 1],
293         c * m[0, 2] - s * m[1, 2],
294         // row 2
295         s * m[0, 0] + c * m[1, 0],
296         s * m[0, 1] + c * m[1, 1],
297         s * m[0, 2] + c * m[1, 2]
298     );
299 }
300 
301 /// ditto
302 M rotate (M, T, V) (in M m, in T angle, in V axis)
303 if (isMat!(4, 4, M) && isFloatingPoint!T && isVec!(3, V))
304 {
305     static assert (
306         isFloatingPoint!(V.Component),
307         "rotate must be passed a floating point axis"
308     );
309     immutable r = rotationPure(angle, axis);
310     return M (
311         // row 1
312         r[0, 0]*m[0, 0] + r[0, 1]*m[1, 0] + r[0, 2]*m[2, 0],
313         r[0, 0]*m[0, 1] + r[0, 1]*m[1, 1] + r[0, 2]*m[2, 1],
314         r[0, 0]*m[0, 2] + r[0, 1]*m[1, 2] + r[0, 2]*m[2, 2],
315         r[0, 0]*m[0, 3] + r[0, 1]*m[1, 3] + r[0, 2]*m[2, 3],
316         // row 2
317         r[1, 0]*m[0, 0] + r[1, 1]*m[1, 0] + r[1, 2]*m[2, 0],
318         r[1, 0]*m[0, 1] + r[1, 1]*m[1, 1] + r[1, 2]*m[2, 1],
319         r[1, 0]*m[0, 2] + r[1, 1]*m[1, 2] + r[1, 2]*m[2, 2],
320         r[1, 0]*m[0, 3] + r[1, 1]*m[1, 3] + r[1, 2]*m[2, 3],
321         // row 3
322         r[2, 0]*m[0, 0] + r[2, 1]*m[1, 0] + r[2, 2]*m[2, 0],
323         r[2, 0]*m[0, 1] + r[2, 1]*m[1, 1] + r[2, 2]*m[2, 1],
324         r[2, 0]*m[0, 2] + r[2, 1]*m[1, 2] + r[2, 2]*m[2, 2],
325         r[2, 0]*m[0, 3] + r[2, 1]*m[1, 3] + r[2, 2]*m[2, 3],
326         // row 4
327         m[3, 0], m[3, 1], m[3, 2], m[3, 3]
328     );
329 }
330 
331 /// ditto
332 M rotate (M, T, V) (in M m, in T angle, in V axis)
333 if (isMat!(3, 4, M) && isVec!(3, V) && isFloatingPoint!T)
334 {
335     static assert (
336         isFloatingPoint!(V.Component),
337         "rotate must be passed a floating point axis"
338     );
339     const r = rotationPure(angle, axis);
340     return M (
341         // row 1
342         r[0, 0]*m[0, 0] + r[0, 1]*m[1, 0] + r[0, 2]*m[2, 0],
343         r[0, 0]*m[0, 1] + r[0, 1]*m[1, 1] + r[0, 2]*m[2, 1],
344         r[0, 0]*m[0, 2] + r[0, 1]*m[1, 2] + r[0, 2]*m[2, 2],
345         r[0, 0]*m[0, 3] + r[0, 1]*m[1, 3] + r[0, 2]*m[2, 3],
346         // row 2
347         r[1, 0]*m[0, 0] + r[1, 1]*m[1, 0] + r[1, 2]*m[2, 0],
348         r[1, 0]*m[0, 1] + r[1, 1]*m[1, 1] + r[1, 2]*m[2, 1],
349         r[1, 0]*m[0, 2] + r[1, 1]*m[1, 2] + r[1, 2]*m[2, 2],
350         r[1, 0]*m[0, 3] + r[1, 1]*m[1, 3] + r[1, 2]*m[2, 3],
351         // row 3
352         r[2, 0]*m[0, 0] + r[2, 1]*m[1, 0] + r[2, 2]*m[2, 0],
353         r[2, 0]*m[0, 1] + r[2, 1]*m[1, 1] + r[2, 2]*m[2, 1],
354         r[2, 0]*m[0, 2] + r[2, 1]*m[1, 2] + r[2, 2]*m[2, 2],
355         r[2, 0]*m[0, 3] + r[2, 1]*m[1, 3] + r[2, 2]*m[2, 3],
356     );
357 }
358 
359 /// ditto
360 M rotate (M, T) (in M m, in T angle, in T x, in T y, in T z)
361 if ((isMat!(3, 4, M) || isMat!(4, 4, M)) && isFloatingPoint!T)
362 {
363     return rotate(m, angle, vec(x, y, z));
364 }
365 
366 ///
367 unittest
368 {
369     import gfx.math.approx : approxUlp;
370     import std.math : PI;
371 
372     immutable m = DMat3( 1, 2, 3, 4, 5, 6, 7, 8, 9 );
373 
374     immutable expected = rotation!double(PI) * m; // full multiplication
375     immutable result = rotate(m, PI);      // simplified multiplication
376 
377     assert (approxUlp(expected, result));
378 }
379 ///
380 unittest
381 {
382     import gfx.math.approx : approxUlp;
383     import std.math : PI;
384 
385     immutable m = DMat4( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 );
386     immutable angle = PI;
387     immutable v = fvec(3, 4, 5);
388 
389     immutable expected = rotation(angle, v) * m; // full multiplication
390     immutable result = rotate(m, angle, v);      // simplified multiplication
391 
392     assert (approxUlp(expected, result));
393 }
394 
395 
396 /// Build a scale matrix.
397 Mat3!(CommonType!(X, Y)) scale(X, Y) (in X x, in Y y)
398 if (allSatisfy!(isNumeric, X, Y))
399 {
400     return Mat3!(CommonType!(X, Y))(
401         x, 0, 0,
402         0, y, 0,
403         0, 0, 1,
404     );
405 }
406 
407 /// ditto
408 auto scale(V) (in V v) if (isVec2!V)
409 {
410     return Mat3!(V.Component) (
411         v.x, 0, 0,
412         0, v.y, 0,
413         0, 0, 1,
414     );
415 }
416 
417 /// ditto
418 Mat4!(CommonType!(X, Y, Z)) scale (X, Y, Z) (in X x, in Y y, in Z z)
419 if (allSatisfy!(isNumeric, X, Y, Z))
420 {
421     return Mat4!(CommonType!(X, Y, Z))(
422         x, 0, 0, 0,
423         0, y, 0, 0,
424         0, 0, z, 0,
425         0, 0, 0, 1,
426     );
427 }
428 
429 /// ditto
430 auto scale(V) (in V v) if (isVec3!V)
431 {
432     return Mat4!(V.Component) (
433         v.x, 0, 0, 0,
434         0, v.y, 0, 0,
435         0, 0, v.z, 0,
436         0, 0, 0, 1,
437     );
438 }
439 
440 /// Append a scale transform inferred from arguments to the matrix m.
441 /// This is equivalent to the expression $(D_CODE scale(...) * m)
442 /// but actually save computation by knowing
443 /// where the ones and zeros are in a pure scale matrix.
444 M scale (M, X, Y)(in M m, in X x, in Y y)
445 if (isMat!(3, 3, M) && allSatisfy!(isNumeric, X, Y))
446 {
447     return M (
448         // row 1
449         m[0, 0] * x,
450         m[0, 1] * x,
451         m[0, 2] * x,
452         // row 2
453         m[1, 0] * y,
454         m[1, 1] * y,
455         m[1, 2] * y,
456         // row 3
457         m[2, 0], m[2, 1], m[2, 2]
458     );
459 }
460 
461 /// ditto
462 M scale (M, X, Y)(in M m, in X x, in Y y)
463 if (isMat!(2, 3, M) && allSatisfy!(isNumeric, X, Y))
464 {
465     return M (
466         // row 1
467         m[0, 0] * x,
468         m[0, 1] * x,
469         m[0, 2] * x,
470         // row 2
471         m[1, 0] * y,
472         m[1, 1] * y,
473         m[1, 2] * y,
474     );
475 }
476 
477 /// ditto
478 M scale (M, V)(in M m, in V v)
479 if ((isMat!(2, 3, M) || isMat!(3, 3, M)) && isVec!(2, V))
480 {
481     return scale(m, v[0], v[1]);
482 }
483 
484 /// ditto
485 M scale (M, X, Y, Z)(in M m, in X x, in Y y, in Z z)
486 if (isMat!(4, 4, M) && allSatisfy!(isNumeric, X, Y, Z))
487 {
488     return M (
489         // row 1
490         m[0, 0] * x,
491         m[0, 1] * x,
492         m[0, 2] * x,
493         m[0, 3] * x,
494         // row 2
495         m[1, 0] * y,
496         m[1, 1] * y,
497         m[1, 2] * y,
498         m[1, 3] * y,
499         // row 3
500         m[2, 0] * z,
501         m[2, 1] * z,
502         m[2, 2] * z,
503         m[2, 3] * z,
504         // row 4
505         m[3, 0], m[3, 1], m[3, 2], m[3, 3]
506     );
507 }
508 
509 /// ditto
510 M scale (M, X, Y, Z)(in M m, in X x, in Y y, in Z z)
511 if (isMat!(3, 4, M) && allSatisfy!(isNumeric, X, Y, Z))
512 {
513     return M (
514         // row 1
515         m[0, 0] * x,
516         m[0, 1] * x,
517         m[0, 2] * x,
518         m[0, 3] * x,
519         // row 2
520         m[1, 0] * y,
521         m[1, 1] * y,
522         m[1, 2] * y,
523         m[1, 3] * y,
524         // row 3
525         m[2, 0] * z,
526         m[2, 1] * z,
527         m[2, 2] * z,
528         m[2, 3] * z,
529     );
530 }
531 
532 /// ditto
533 M scale (M, V)(in M m, in V v)
534 if ((isMat!(3, 4, M) || isMat!(4, 4, M)) && isVec!(3, V))
535 {
536     return scale(m, v[0], v[1], v[2]);
537 }
538 
539 
540 ///
541 unittest
542 {
543     import gfx.math.approx : approxUlp;
544 
545     immutable m = DMat3( 1, 2, 3, 4, 5, 6, 7, 8, 9 );
546 
547     immutable expected = scale(4, 5) * m; // full multiplication
548     immutable result = scale(m, 4, 5);   // simplified multiplication
549 
550     assert (approxUlp(expected, result));
551 }
552 ///
553 unittest
554 {
555     import gfx.math.approx : approxUlp;
556 
557     immutable m = DMat4( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 );
558 
559     immutable expected = scale(4, 5, 6) * m; // full multiplication
560     immutable result = scale(m, 4, 5, 6);   // simplified multiplication
561 
562     assert (approxUlp(expected, result));
563 }
564 
565 /// Affine matrix multiplication.
566 ///
567 /// Perform a multiplication of two 2x3 or two 3x4 matrices as if their last row
568 /// were [ 0, 0, 1] or [ 0, 0, 0, 1 ].
569 /// Allows manipulation of smaller matrices when only affine transformation are
570 /// required.
571 /// Note: translation, rotation, scaling, shearing and any combination of those
572 /// are affine transforms. Projection is not affine.
573 /// I.e. for 2D, an affine transform is held in 2x3 matrix, 2x2 for rotation and
574 /// scaling and an additional column for translation.
575 /// The same applies with 3D and 3x4 matrices.
576 ///
577 /// This is not implemented as an operator as it is not a mathematical
578 /// operation (ml.columnLength != mr.rowLength).
579 auto affineMult(ML, MR)(in ML ml, in MR mr)
580 if (areMat!(2, 3, ML, MR) || areMat!(3, 4, ML, MR))
581 {
582     alias Comp = CommonType!(ML.Component, MR.Component);
583     enum rowLength = ML.rowLength;
584     enum columnLength = ML.columnLength;
585     alias ResMat = Mat!(Comp, rowLength, columnLength);
586 
587     ResMat res = void;
588     static foreach(r; 0 .. rowLength)
589     {
590         static foreach (c; 0 .. columnLength)
591         {{
592             Comp resComp = 0;
593             static foreach (rc; 0 .. rowLength) // that is columnCount-1
594             {
595                 resComp += ml[r, rc] * mr[rc, c];
596             }
597             static if (c == columnLength-1)
598             {
599                 resComp += ml[r, c]; // that is the last one in the last row
600             }
601             res[r, c] = resComp;
602         }}
603     }
604     return res;
605 }
606 
607 ///
608 unittest
609 {
610     import gfx.math.approx : approxUlp;
611 
612     /// full matrices
613     immutable fm1 = FMat3x3(
614         1, 2, 3,
615         4, 5, 6,
616         0, 0, 1
617     );
618     immutable fm2 = DMat3x3(
619          7,  8,  9,
620         10, 11, 12,
621          0,  0,  1
622     );
623 
624     /// affine matrices
625     immutable am1 = FMat2x3(
626         1, 2, 3,
627         4, 5, 6,
628     );
629     immutable am2 = DMat2x3(
630          7,  8,  9,
631         10, 11, 12,
632     );
633 
634     immutable expected = (fm1 * fm2).slice!(0, 2, 0, 3);
635     immutable result = affineMult(am1, am2);
636     assert( approxUlp(expected, result) );
637 }
638 
639 
640 /// Transform a vector by a matrix in homogenous coordinates.
641 auto transform(V, M)(in V v, in M m)
642 if (isVec!(2, V) && isMat!(3, 3, M))
643 {
644     return (m * vec(v, 1)).xy;
645 }
646 /// ditto
647 auto transform(V, M)(in V v, in M m)
648 if (isVec!(2, V) && isMat!(2, 3, M))
649 {
650     return m * vec(v, 1);
651 }
652 /// ditto
653 auto transform(V, M)(in V v, in M m)
654 if (isVec!(3, V) && isMat!(4, 4, M))
655 {
656     return (m * vec(v, 1)).xyz;
657 }
658 /// ditto
659 auto transform(V, M)(in V v, in M m)
660 if (isVec!(3, V) && isMat!(3, 4, M))
661 {
662     return m * vec(v, 1);
663 }
664 /// ditto
665 auto transform(V, M)(in V v, in M m)
666 if (isVec!(4, V) && isMat!(4, 4, M))
667 {
668     return m * v;
669 }
670 
671 unittest
672 {
673     import gfx.math.approx : approxUlp;
674 
675     // 2x3 matrix can hold affine 2D transforms
676     immutable transl = DMat2x3(
677         1, 0, 3,
678         0, 1, 2,
679     );
680     assert( approxUlp(transform(dvec(3, 5), transl), dvec(6, 7)) );
681 }
682 
683 ///
684 unittest
685 {
686     import gfx.math.approx : approxUlp, approxUlpAndAbs;
687     import std.math : PI;
688 
689     immutable v = dvec(2, 0);
690     auto m = DMat2x3.identity;
691 
692     m = m.rotate(PI/2);
693     assert ( approxUlpAndAbs(transform(v, m), dvec(0, 2)) );
694 
695     m = m.translate(2, 2);
696     assert ( approxUlp(transform(v, m), dvec(2, 4)) );
697 
698     m = m.scale(2, 2);
699     assert ( approxUlp(transform(v, m), dvec(4, 8)) );
700 }
701 
702 ///
703 unittest
704 {
705     import gfx.math.approx : approxUlp;
706 
707     auto st = scale!float(2, 2).translate(3, 1);
708     assert( approxUlp(transform(fvec(0, 0), st), fvec(3, 1)) );
709     assert( approxUlp(transform(fvec(1, 1), st), fvec(5, 3)) );
710 
711     auto ts = translation!float(3, 1).scale(2, 2);
712     assert( approxUlp(transform(fvec(0, 0), ts), fvec(6, 2)) );
713     assert( approxUlp(transform(fvec(1, 1), ts), fvec(8, 4)) );
714 }