View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.geometry.euclidean.twod;
18  
19  import java.util.function.UnaryOperator;
20  
21  import org.apache.commons.geometry.core.GeometryTestUtils;
22  import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
23  import org.apache.commons.geometry.euclidean.twod.rotation.Rotation2D;
24  import org.apache.commons.numbers.angle.Angle;
25  import org.apache.commons.numbers.core.Precision;
26  import org.junit.jupiter.api.Assertions;
27  import org.junit.jupiter.api.Test;
28  
29  class AffineTransformMatrix2DTest {
30  
31      private static final double EPS = 1e-12;
32  
33      private static final Precision.DoubleEquivalence TEST_PRECISION =
34              Precision.doubleEquivalenceOfEpsilon(EPS);
35  
36      private static final double THREE_PI_OVER_TWO = 3 * Math.PI / 2;
37  
38      @Test
39      void testOf() {
40          // arrange
41          final double[] arr = {
42              1, 2, 3,
43              4, 5, 6
44          };
45  
46          // act
47          final AffineTransformMatrix2D transform = AffineTransformMatrix2D.of(arr);
48  
49          // assert
50          final double[] result = transform.toArray();
51          Assertions.assertNotSame(arr, result);
52          Assertions.assertArrayEquals(arr, result, 0.0);
53      }
54  
55      @Test
56      void testOf_invalidDimensions() {
57          // act/assert
58          GeometryTestUtils.assertThrowsWithMessage(() -> AffineTransformMatrix2D.of(1, 2),
59                  IllegalArgumentException.class, "Dimension mismatch: 2 != 6");
60      }
61  
62      @Test
63      void testFromColumnVectors_twoVector() {
64          // arrange
65          final Vector2D u = Vector2D.of(1, 2);
66          final Vector2D v = Vector2D.of(3, 4);
67  
68          // act
69          final AffineTransformMatrix2D transform = AffineTransformMatrix2D.fromColumnVectors(u, v);
70  
71          // assert
72          Assertions.assertArrayEquals(new double[] {
73              1, 3, 0,
74              2, 4, 0
75          }, transform.toArray(), 0.0);
76      }
77  
78      @Test
79      void testFromColumnVectors_threeVectors() {
80          // arrange
81          final Vector2D u = Vector2D.of(1, 2);
82          final Vector2D v = Vector2D.of(3, 4);
83          final Vector2D t = Vector2D.of(5, 6);
84  
85          // act
86          final AffineTransformMatrix2D transform = AffineTransformMatrix2D.fromColumnVectors(u, v, t);
87  
88          // assert
89          Assertions.assertArrayEquals(new double[] {
90              1, 3, 5,
91              2, 4, 6
92          }, transform.toArray(), 0.0);
93      }
94  
95      @Test
96      void testIdentity() {
97          // act
98          final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity();
99  
100         // assert
101         final double[] expected = {
102             1, 0, 0,
103             0, 1, 0
104         };
105         Assertions.assertArrayEquals(expected, transform.toArray(), 0.0);
106     }
107 
108     @Test
109     void testFrom() {
110         // act/assert
111         Assertions.assertArrayEquals(new double[] {
112             1, 0, 0,
113             0, 1, 0
114         }, AffineTransformMatrix2D.from(UnaryOperator.identity()).toArray(), EPS);
115         Assertions.assertArrayEquals(new double[] {
116             1, 0, 2,
117             0, 1, 3
118         }, AffineTransformMatrix2D.from(v -> v.add(Vector2D.of(2, 3))).toArray(), EPS);
119         Assertions.assertArrayEquals(new double[] {
120             3, 0, 0,
121             0, 3, 0
122         }, AffineTransformMatrix2D.from(v -> v.multiply(3)).toArray(), EPS);
123         Assertions.assertArrayEquals(new double[] {
124             3, 0, 6,
125             0, 3, 9
126         }, AffineTransformMatrix2D.from(v -> v.add(Vector2D.of(2, 3)).multiply(3)).toArray(), EPS);
127     }
128 
129     @Test
130     void testFrom_invalidFunction() {
131         // act/assert
132         Assertions.assertThrows(IllegalArgumentException.class, () -> AffineTransformMatrix2D.from(v -> v.multiply(0)));
133     }
134 
135     @Test
136     void testCreateTranslation_xy() {
137         // act
138         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createTranslation(2, 3);
139 
140         // assert
141         final double[] expected = {
142             1, 0, 2,
143             0, 1, 3
144         };
145         Assertions.assertArrayEquals(expected, transform.toArray(), 0.0);
146     }
147 
148     @Test
149     void testCreateTranslation_vector() {
150         // act
151         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createTranslation(Vector2D.of(5, 6));
152 
153         // assert
154         final double[] expected = {
155             1, 0, 5,
156             0, 1, 6
157         };
158         Assertions.assertArrayEquals(expected, transform.toArray(), 0.0);
159     }
160 
161     @Test
162     void testCreateScale_xy() {
163         // act
164         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createScale(2, 3);
165 
166         // assert
167         final double[] expected = {
168             2, 0, 0,
169             0, 3, 0
170         };
171         Assertions.assertArrayEquals(expected, transform.toArray(), 0.0);
172     }
173 
174     @Test
175     void testTranslate_xy() {
176         // arrange
177         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
178                     2, 0, 10,
179                     0, 3, 11
180                 );
181 
182         // act
183         final AffineTransformMatrix2D result = a.translate(4, 5);
184 
185         // assert
186         final double[] expected = {
187             2, 0, 14,
188             0, 3, 16
189         };
190         Assertions.assertArrayEquals(expected, result.toArray(), 0.0);
191     }
192 
193     @Test
194     void testTranslate_vector() {
195         // arrange
196         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
197                     2, 0, 10,
198                     0, 3, 11
199                 );
200 
201         // act
202         final AffineTransformMatrix2D result = a.translate(Vector2D.of(7, 8));
203 
204         // assert
205         final double[] expected = {
206             2, 0, 17,
207             0, 3, 19
208         };
209         Assertions.assertArrayEquals(expected, result.toArray(), 0.0);
210     }
211 
212     @Test
213     void testCreateScale_vector() {
214         // act
215         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createScale(Vector2D.of(4, 5));
216 
217         // assert
218         final double[] expected = {
219             4, 0, 0,
220             0, 5, 0
221         };
222         Assertions.assertArrayEquals(expected, transform.toArray(), 0.0);
223     }
224 
225     @Test
226     void testCreateScale_singleValue() {
227         // act
228         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createScale(7);
229 
230         // assert
231         final double[] expected = {
232             7, 0, 0,
233             0, 7, 0
234         };
235         Assertions.assertArrayEquals(expected, transform.toArray(), 0.0);
236     }
237 
238     @Test
239     void testScale_xy() {
240         // arrange
241         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
242                     2, 0, 10,
243                     0, 3, 11
244                 );
245 
246         // act
247         final AffineTransformMatrix2D result = a.scale(4, 5);
248 
249         // assert
250         final double[] expected = {
251             8, 0, 40,
252             0, 15, 55
253         };
254         Assertions.assertArrayEquals(expected, result.toArray(), 0.0);
255     }
256 
257     @Test
258     void testScale_vector() {
259         // arrange
260         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
261                     2, 0, 10,
262                     0, 3, 11
263                 );
264 
265         // act
266         final AffineTransformMatrix2D result = a.scale(Vector2D.of(7, 8));
267 
268         // assert
269         final double[] expected = {
270             14, 0, 70,
271             0, 24, 88
272         };
273         Assertions.assertArrayEquals(expected, result.toArray(), 0.0);
274     }
275 
276     @Test
277     void testScale_singleValue() {
278         // arrange
279         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
280                     2, 0, 10,
281                     0, 3, 11
282                 );
283 
284         // act
285         final AffineTransformMatrix2D result = a.scale(10);
286 
287         // assert
288         final double[] expected = {
289             20, 0, 100,
290             0, 30, 110
291         };
292         Assertions.assertArrayEquals(expected, result.toArray(), 0.0);
293     }
294 
295     @Test
296     void testCreateRotation() {
297         // act
298         final double angle = Math.PI * 2.0 / 3.0;
299         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createRotation(angle);
300 
301         // assert
302         final double sin = Math.sin(angle);
303         final double cos = Math.cos(angle);
304 
305         final double[] expected = {
306             cos, -sin, 0,
307             sin, cos, 0
308         };
309         Assertions.assertArrayEquals(expected, transform.toArray(), EPS);
310     }
311 
312     @Test
313     void testCreateRotation_aroundCenter_rawAngle() {
314         // act
315         final Vector2D center = Vector2D.of(1, 2);
316         final double angle = Math.PI * 2.0 / 3.0;
317         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createRotation(center, angle);
318 
319         // assert
320         final double sin = Math.sin(angle);
321         final double cos = Math.cos(angle);
322 
323         final double[] expected = {
324             cos, -sin, -cos + (2 * sin) + 1,
325             sin, cos, -sin - (2 * cos) + 2
326         };
327         Assertions.assertArrayEquals(expected, transform.toArray(), EPS);
328     }
329 
330     @Test
331     void testCreateRotation_aroundCenter_rotationInstance() {
332         // act
333         final Vector2D center = Vector2D.of(1, 2);
334         final double angle = Math.PI * 4.0 / 3.0;
335         final Rotation2D rotation = Rotation2D.of(angle);
336         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createRotation(center, rotation);
337 
338         // assert
339         final double sin = Math.sin(angle);
340         final double cos = Math.cos(angle);
341 
342         final double[] expected = {
343             cos, -sin, -cos + (2 * sin) + 1,
344             sin, cos, -sin - (2 * cos) + 2
345         };
346         Assertions.assertArrayEquals(expected, transform.toArray(), EPS);
347     }
348 
349     @Test
350     void testRotate_rawAngle() {
351         // arrange
352         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
353                     1, 2, 3,
354                     4, 5, 6
355                 );
356 
357         // act
358         final AffineTransformMatrix2D result = a.rotate(Angle.PI_OVER_TWO);
359 
360         // assert
361         final double[] expected = {
362             -4, -5, -6,
363             1, 2, 3
364         };
365         Assertions.assertArrayEquals(expected, result.toArray(), EPS);
366     }
367 
368     @Test
369     void testRotate_rotationInstance() {
370         // arrange
371         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
372                     1, 2, 3,
373                     4, 5, 6
374                 );
375 
376         // act
377         final AffineTransformMatrix2D result = a.rotate(Rotation2D.of(Angle.PI_OVER_TWO));
378 
379         // assert
380         final double[] expected = {
381             -4, -5, -6,
382             1, 2, 3
383         };
384         Assertions.assertArrayEquals(expected, result.toArray(), EPS);
385     }
386 
387     @Test
388     void testRotate_aroundCenter_rawAngle() {
389         // arrange
390         final Vector2D center = Vector2D.of(1, 2);
391 
392         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
393                     1, 2, 3,
394                     4, 5, 6
395                 );
396 
397         // act
398         final AffineTransformMatrix2D result = a.rotate(center, Angle.PI_OVER_TWO);
399 
400         // assert
401         final double[] expected = {
402             -4, -5, -3,
403             1, 2, 4
404         };
405         Assertions.assertArrayEquals(expected, result.toArray(), EPS);
406     }
407 
408     @Test
409     void testRotate_aroundCenter_rotationInstance() {
410         // arrange
411         final Vector2D center = Vector2D.of(1, 2);
412 
413         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
414                     1, 2, 3,
415                     4, 5, 6
416                 );
417 
418         // act
419         final AffineTransformMatrix2D result = a.rotate(center, Rotation2D.of(Angle.PI_OVER_TWO));
420 
421         // assert
422         final double[] expected = {
423             -4, -5, -3,
424             1, 2, 4
425         };
426         Assertions.assertArrayEquals(expected, result.toArray(), EPS);
427     }
428 
429     @Test
430     void testCreateShear() {
431         // act
432         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createShear(2, 3);
433 
434         // assert
435         final double[] expected = {
436             1, 2, 0,
437             3, 1, 0
438         };
439         Assertions.assertArrayEquals(expected, transform.toArray(), 0.0);
440     }
441 
442     @Test
443     void testShear() {
444         // arrange
445         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
446                 1, 2, 3,
447                 4, 5, 6
448             );
449 
450         // act
451         final AffineTransformMatrix2D result = a.shear(-2, 3);
452 
453         // assert
454         final double[] expected = {
455             -7, -8, -9,
456             7, 11, 15
457         };
458         Assertions.assertArrayEquals(expected, result.toArray(), EPS);
459     }
460 
461     @Test
462     void testShear_noShear() {
463         // arrange
464         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
465                 1, 2, 3,
466                 4, 5, 6
467             );
468 
469         // act
470         final AffineTransformMatrix2D result = a.shear(0, 0);
471 
472         // assert
473         final double[] expected = {
474             1, 2, 3,
475             4, 5, 6
476         };
477         Assertions.assertArrayEquals(expected, result.toArray(), EPS);
478     }
479 
480     @Test
481     void testApply_identity() {
482         // arrange
483         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity();
484 
485         // act/assert
486         runWithCoordinates((x, y) -> {
487             final Vector2D v = Vector2D.of(x, y);
488 
489             EuclideanTestUtils.assertCoordinatesEqual(v, transform.apply(v), EPS);
490         });
491     }
492 
493     @Test
494     void testApply_translate() {
495         // arrange
496         final Vector2D translation = Vector2D.of(1.1, -Math.PI);
497 
498         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
499                 .translate(translation);
500 
501         // act/assert
502         runWithCoordinates((x, y) -> {
503             final Vector2D vec = Vector2D.of(x, y);
504 
505             final Vector2D expectedVec = vec.add(translation);
506 
507             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
508         });
509     }
510 
511     @Test
512     void testApply_scale() {
513         // arrange
514         final Vector2D factors = Vector2D.of(2.0, -3.0);
515 
516         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
517                 .scale(factors);
518 
519         // act/assert
520         runWithCoordinates((x, y) -> {
521             final Vector2D vec = Vector2D.of(x, y);
522 
523             final Vector2D expectedVec = Vector2D.of(factors.getX() * x, factors.getY() * y);
524 
525             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
526         });
527     }
528 
529     @Test
530     void testApply_rotate() {
531         // arrange
532         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
533                 .rotate(-Angle.PI_OVER_TWO);
534 
535         // act/assert
536         runWithCoordinates((x, y) -> {
537             final Vector2D vec = Vector2D.of(x, y);
538 
539             final Vector2D expectedVec = Vector2D.of(y, -x);
540 
541             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
542         });
543     }
544 
545     @Test
546     void testApply_rotate_aroundCenter_minusHalfPi() {
547         // arrange
548         final Vector2D center = Vector2D.of(1, 2);
549         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
550                 .rotate(center, -Angle.PI_OVER_TWO);
551 
552         // act/assert
553         runWithCoordinates((x, y) -> {
554             final Vector2D vec = Vector2D.of(x, y);
555 
556             final Vector2D centered = vec.subtract(center);
557             final Vector2D expectedVec = Vector2D.of(centered.getY(), -centered.getX()).add(center);
558 
559             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
560         });
561     }
562 
563     @Test
564     void testApply_rotate_aroundCenter_pi() {
565         // arrange
566         final Vector2D center = Vector2D.of(1, 2);
567         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
568                 .rotate(center, Math.PI);
569 
570         // act/assert
571         runWithCoordinates((x, y) -> {
572             final Vector2D vec = Vector2D.of(x, y);
573 
574             final Vector2D centered = vec.subtract(center);
575             final Vector2D expectedVec = Vector2D.of(-centered.getX(), -centered.getY()).add(center);
576 
577             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
578         });
579     }
580 
581     @Test
582     void testApply_shearAlongX() {
583         // arrange
584         final double shearFactor = -2;
585         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
586                 .shear(shearFactor, 0);
587 
588         // act/assert
589         runWithCoordinates((x, y) -> {
590             final Vector2D vec = Vector2D.of(x, y);
591 
592             final Vector2D expectedVec = Vector2D.of(x + (shearFactor * y), y);
593 
594             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
595         });
596     }
597 
598     @Test
599     void testApply_shearAlongY() {
600         // arrange
601         final double shearFactor = 2;
602         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
603                 .shear(0, shearFactor);
604 
605         // act/assert
606         runWithCoordinates((x, y) -> {
607             final Vector2D vec = Vector2D.of(x, y);
608 
609             final Vector2D expectedVec = Vector2D.of(x, y + (shearFactor * x));
610 
611             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
612         });
613     }
614 
615     @Test
616     void testApply_shearAlongXAndY() {
617         // arrange
618         final double shearX = 2;
619         final double shearY = -3;
620         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
621                 .shear(shearX, shearY);
622 
623         // act/assert
624         runWithCoordinates((x, y) -> {
625             final Vector2D vec = Vector2D.of(x, y);
626 
627             final Vector2D expectedVec = Vector2D.of(x + (shearX * y), y + (shearY * x));
628 
629             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
630         });
631     }
632 
633     @Test
634     void testApply_translateShear() {
635         // arrange
636         final Vector2D translation = Vector2D.of(7, 8);
637         final double shearX = -4;
638         final double shearY = 5;
639         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
640                 .translate(translation)
641                 .shear(shearX, shearY);
642 
643         // act/assert
644         runWithCoordinates((x, y) -> {
645             final Vector2D vec = Vector2D.of(x, y);
646 
647             final double tx = x + translation.getX();
648             final double ty = y + translation.getY();
649 
650             final Vector2D expectedVec = Vector2D.of(tx + (shearX * ty), ty + (shearY * tx));
651 
652             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
653         });
654     }
655 
656 
657     @Test
658     void testApply_translateScaleRotate() {
659         // arrange
660         final Vector2D translation = Vector2D.of(-2.0, -3.0);
661         final Vector2D scale = Vector2D.of(5.0, 6.0);
662 
663         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
664                 .translate(translation)
665                 .scale(scale)
666                 .rotate(Angle.PI_OVER_TWO);
667 
668         // act/assert
669         EuclideanTestUtils.assertCoordinatesEqual(Vector2D.of(12, -5), transform.apply(Vector2D.of(1, 1)), EPS);
670 
671         runWithCoordinates((x, y) -> {
672             final Vector2D vec = Vector2D.of(x, y);
673 
674             final Vector2D temp = Vector2D.of(
675                         (x + translation.getX()) * scale.getX(),
676                         (y + translation.getY()) * scale.getY()
677                     );
678             final Vector2D expectedVec = Vector2D.of(-temp.getY(), temp.getX());
679 
680             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
681         });
682     }
683 
684     @Test
685     void testApply_scaleTranslateRotate() {
686         // arrange
687         final Vector2D scale = Vector2D.of(5.0, 6.0);
688         final Vector2D translation = Vector2D.of(-2.0, -3.0);
689 
690         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
691                 .scale(scale)
692                 .translate(translation)
693                 .rotate(-Angle.PI_OVER_TWO);
694 
695         // act/assert
696         runWithCoordinates((x, y) -> {
697             final Vector2D vec = Vector2D.of(x, y);
698 
699             final Vector2D temp = Vector2D.of(
700                         (x * scale.getX()) + translation.getX(),
701                         (y * scale.getY()) + translation.getY()
702                     );
703             final Vector2D expectedVec = Vector2D.of(temp.getY(), -temp.getX());
704 
705             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
706         });
707     }
708 
709     @Test
710     void testApplyXY() {
711         // arrange
712         final Vector2D scale = Vector2D.of(5.0, 6.0);
713         final Vector2D translation = Vector2D.of(-2.0, -3.0);
714         final Vector2D shear = Vector2D.of(7, 8);
715 
716         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
717                 .scale(scale)
718                 .translate(translation)
719                 .rotate(-Angle.PI_OVER_TWO)
720                 .shear(shear.getX(), shear.getY());
721 
722         // act/assert
723         runWithCoordinates((x, y) -> {
724             final Vector2D scaledAndTranslated = Vector2D.of(
725                         (x * scale.getX()) + translation.getX(),
726                         (y * scale.getY()) + translation.getY()
727                     );
728             final Vector2D rotated = Vector2D.of(scaledAndTranslated.getY(), -scaledAndTranslated.getX());
729             final Vector2D expected = Vector2D.of(
730                         rotated.getX() + (rotated.getY() * shear.getX()),
731                         rotated.getY() + (rotated.getX() * shear.getY())
732                     );
733 
734             Assertions.assertEquals(expected.getX(), transform.applyX(x, y), EPS);
735             Assertions.assertEquals(expected.getY(), transform.applyY(x, y), EPS);
736         });
737     }
738 
739     @Test
740     void testApplyVector_identity() {
741         // arrange
742         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity();
743 
744         // act/assert
745         runWithCoordinates((x, y) -> {
746             final Vector2D v = Vector2D.of(x, y);
747 
748             EuclideanTestUtils.assertCoordinatesEqual(v, transform.applyVector(v), EPS);
749         });
750     }
751 
752     @Test
753     void testApplyVector_translate() {
754         // arrange
755         final Vector2D translation = Vector2D.of(1.1, -Math.PI);
756 
757         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
758                 .translate(translation);
759 
760         // act/assert
761         runWithCoordinates((x, y) -> {
762             final Vector2D vec = Vector2D.of(x, y);
763 
764             EuclideanTestUtils.assertCoordinatesEqual(vec, transform.applyVector(vec), EPS);
765         });
766     }
767 
768     @Test
769     void testApplyVector_scale() {
770         // arrange
771         final Vector2D factors = Vector2D.of(2.0, -3.0);
772 
773         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
774                 .scale(factors);
775 
776         // act/assert
777         runWithCoordinates((x, y) -> {
778             final Vector2D vec = Vector2D.of(x, y);
779 
780             final Vector2D expectedVec = Vector2D.of(factors.getX() * x, factors.getY() * y);
781 
782             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.applyVector(vec), EPS);
783         });
784     }
785 
786     @Test
787     void testApplyVector_representsDisplacement() {
788         // arrange
789         final Vector2D p1 = Vector2D.of(2, 3);
790 
791         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
792                 .scale(1.5)
793                 .translate(4, 6)
794                 .rotate(Angle.PI_OVER_TWO);
795 
796         // act/assert
797         runWithCoordinates((x, y) -> {
798             final Vector2D p2 = Vector2D.of(x, y);
799             final Vector2D input = p1.subtract(p2);
800 
801             final Vector2D expected = transform.apply(p1).subtract(transform.apply(p2));
802 
803             EuclideanTestUtils.assertCoordinatesEqual(expected, transform.applyVector(input), EPS);
804         });
805     }
806 
807     @Test
808     void testApplyVectorXY() {
809         // arrange
810         final Vector2D p1 = Vector2D.of(2, 3);
811 
812         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
813                 .scale(1.5)
814                 .translate(4, 6)
815                 .rotate(0.3 * Math.PI);
816 
817         // act/assert
818         runWithCoordinates((x, y) -> {
819             final Vector2D p2 = p1.add(Vector2D.of(x, y));
820 
821             final Vector2D expected = transform.apply(p1).vectorTo(transform.apply(p2));
822 
823             Assertions.assertEquals(expected.getX(), transform.applyVectorX(x, y), EPS);
824             Assertions.assertEquals(expected.getY(), transform.applyVectorY(x, y), EPS);
825         });
826     }
827 
828     @Test
829     void testApplyDirection_identity() {
830         // arrange
831         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity();
832 
833         // act/assert
834         EuclideanTestUtils.permuteSkipZero(-5, 5, 0.5, (x, y) -> {
835             final Vector2D v = Vector2D.of(x, y);
836 
837             EuclideanTestUtils.assertCoordinatesEqual(v.normalize(), transform.applyDirection(v), EPS);
838         });
839     }
840 
841     @Test
842     void testApplyDirection_translate() {
843         // arrange
844         final Vector2D translation = Vector2D.of(1.1, -Math.PI);
845 
846         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
847                 .translate(translation);
848 
849         // act/assert
850         EuclideanTestUtils.permuteSkipZero(-5, 5, 0.5, (x, y) -> {
851             final Vector2D vec = Vector2D.of(x, y);
852 
853             EuclideanTestUtils.assertCoordinatesEqual(vec.normalize(), transform.applyDirection(vec), EPS);
854         });
855     }
856 
857     @Test
858     void testApplyDirection_scale() {
859         // arrange
860         final Vector2D factors = Vector2D.of(2.0, -3.0);
861 
862         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
863                 .scale(factors);
864 
865         // act/assert
866         EuclideanTestUtils.permuteSkipZero(-5, 5, 0.5, (x, y) -> {
867             final Vector2D vec = Vector2D.of(x, y);
868 
869             final Vector2D expectedVec = Vector2D.of(factors.getX() * x, factors.getY() * y).normalize();
870 
871             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.applyDirection(vec), EPS);
872         });
873     }
874 
875     @Test
876     void testApplyDirection_representsNormalizedDisplacement() {
877         // arrange
878         final Vector2D p1 = Vector2D.of(2.1, 3.2);
879 
880         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
881                 .scale(1.5)
882                 .translate(4, 6)
883                 .rotate(Angle.PI_OVER_TWO);
884 
885         // act/assert
886         EuclideanTestUtils.permute(-5, 5, 0.5, (x, y) -> {
887             final Vector2D p2 = Vector2D.of(x, y);
888             final Vector2D input = p1.subtract(p2);
889 
890             final Vector2D expected = transform.apply(p1).subtract(transform.apply(p2)).normalize();
891 
892             EuclideanTestUtils.assertCoordinatesEqual(expected, transform.applyDirection(input), EPS);
893         });
894     }
895 
896     @Test
897     void testApplyDirection_illegalNorm() {
898         // act/assert
899         Assertions.assertThrows(IllegalArgumentException.class, () -> AffineTransformMatrix2D.createScale(1, 0).applyDirection(Vector2D.Unit.PLUS_Y));
900         Assertions.assertThrows(IllegalArgumentException.class, () -> AffineTransformMatrix2D.createScale(2).applyDirection(Vector2D.ZERO));
901     }
902 
903     @Test
904     void testDeterminant() {
905         // act/assert
906         Assertions.assertEquals(1.0, AffineTransformMatrix2D.identity().determinant(), EPS);
907         Assertions.assertEquals(6.0, AffineTransformMatrix2D.of(
908                 2, 0, 4,
909                 0, 3, 5
910             ).determinant(), EPS);
911         Assertions.assertEquals(-6.0, AffineTransformMatrix2D.of(
912                 2, 0, 4,
913                 0, -3, 5
914             ).determinant(), EPS);
915         Assertions.assertEquals(-5.0, AffineTransformMatrix2D.of(
916                 1, 3, 0,
917                 2, 1, 0
918             ).determinant(), EPS);
919         Assertions.assertEquals(-0.0, AffineTransformMatrix2D.of(
920                 0, 0, 1,
921                 0, 0, 2
922             ).determinant(), EPS);
923     }
924 
925     @Test
926     void testPreservesOrientation() {
927         // act/assert
928         Assertions.assertTrue(AffineTransformMatrix2D.identity().preservesOrientation());
929         Assertions.assertTrue(AffineTransformMatrix2D.of(
930                 2, 0, 4,
931                 0, 3, 5
932             ).preservesOrientation());
933 
934         Assertions.assertFalse(AffineTransformMatrix2D.of(
935                 2, 0, 4,
936                 0, -3, 5
937             ).preservesOrientation());
938         Assertions.assertFalse(AffineTransformMatrix2D.of(
939                 1, 3, 0,
940                 2, 1, 0
941             ).preservesOrientation());
942         Assertions.assertFalse(AffineTransformMatrix2D.of(
943                 0, 0, 1,
944                 0, 0, 2
945             ).preservesOrientation());
946     }
947 
948     @Test
949     void testMultiply() {
950         // arrange
951         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
952                     1, 2, 3,
953                     5, 6, 7
954                 );
955         final AffineTransformMatrix2D b = AffineTransformMatrix2D.of(
956                     13, 14, 15,
957                     17, 18, 19
958                 );
959 
960         // act
961         final AffineTransformMatrix2D result = a.multiply(b);
962 
963         // assert
964         final double[] arr = result.toArray();
965         Assertions.assertArrayEquals(new double[] {
966             47, 50, 56,
967             167, 178, 196
968         }, arr, EPS);
969     }
970 
971     @Test
972     void testMultiply_combinesTransformOperations() {
973         // arrange
974         final Vector2D translation1 = Vector2D.of(1, 2);
975         final double scale = 2.0;
976         final Vector2D translation2 = Vector2D.of(4, 5);
977 
978         final AffineTransformMatrix2D a = AffineTransformMatrix2D.createTranslation(translation1);
979         final AffineTransformMatrix2D b = AffineTransformMatrix2D.createScale(scale);
980         final AffineTransformMatrix2D c = AffineTransformMatrix2D.identity();
981         final AffineTransformMatrix2D d = AffineTransformMatrix2D.createTranslation(translation2);
982 
983         // act
984         final AffineTransformMatrix2D transform = d.multiply(c).multiply(b).multiply(a);
985 
986         // assert
987         runWithCoordinates((x, y) -> {
988             final Vector2D vec = Vector2D.of(x, y);
989 
990             final Vector2D expectedVec = vec
991                     .add(translation1)
992                     .multiply(scale)
993                     .add(translation2);
994 
995             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
996         });
997     }
998 
999     @Test
1000     void testPremultiply() {
1001         // arrange
1002         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
1003                     1, 2, 3,
1004                     5, 6, 7
1005                 );
1006         final AffineTransformMatrix2D b = AffineTransformMatrix2D.of(
1007                     13, 14, 15,
1008                     17, 18, 19
1009                 );
1010 
1011         // act
1012         final AffineTransformMatrix2D result = b.premultiply(a);
1013 
1014         // assert
1015         final double[] arr = result.toArray();
1016         Assertions.assertArrayEquals(new double[] {
1017             47, 50, 56,
1018             167, 178, 196
1019         }, arr, EPS);
1020     }
1021 
1022     @Test
1023     void testPremultiply_combinesTransformOperations() {
1024         // arrange
1025         final Vector2D translation1 = Vector2D.of(1, 2);
1026         final double scale = 2.0;
1027         final Vector2D translation2 = Vector2D.of(4, 5);
1028 
1029         final AffineTransformMatrix2D a = AffineTransformMatrix2D.createTranslation(translation1);
1030         final AffineTransformMatrix2D b = AffineTransformMatrix2D.createScale(scale);
1031         final AffineTransformMatrix2D c = AffineTransformMatrix2D.identity();
1032         final AffineTransformMatrix2D d = AffineTransformMatrix2D.createTranslation(translation2);
1033 
1034         // act
1035         final AffineTransformMatrix2D transform = a.premultiply(b).premultiply(c).premultiply(d);
1036 
1037         // assert
1038         runWithCoordinates((x, y) -> {
1039             final Vector2D vec = Vector2D.of(x, y);
1040 
1041             final Vector2D expectedVec = vec
1042                     .add(translation1)
1043                     .multiply(scale)
1044                     .add(translation2);
1045 
1046             EuclideanTestUtils.assertCoordinatesEqual(expectedVec, transform.apply(vec), EPS);
1047         });
1048     }
1049 
1050     @Test
1051     void testInverse_identity() {
1052         // act
1053         final AffineTransformMatrix2D inverse = AffineTransformMatrix2D.identity().inverse();
1054 
1055         // assert
1056         final double[] expected = {
1057             1, 0, 0,
1058             0, 1, 0
1059         };
1060         Assertions.assertArrayEquals(expected, inverse.toArray(), 0.0);
1061     }
1062 
1063     @Test
1064     void testInverse_multiplyByInverse_producesIdentity() {
1065         // arrange
1066         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
1067                     1, 3, 7,
1068                     2, 4, 9
1069                 );
1070 
1071         final AffineTransformMatrix2D inv = a.inverse();
1072 
1073         // act
1074         final AffineTransformMatrix2D result = inv.multiply(a);
1075 
1076         // assert
1077         final double[] expected = {
1078             1, 0, 0,
1079             0, 1, 0
1080         };
1081         Assertions.assertArrayEquals(expected, result.toArray(), EPS);
1082     }
1083 
1084     @Test
1085     void testInverse_translate() {
1086         // arrange
1087         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createTranslation(1, -2);
1088 
1089         // act
1090         final AffineTransformMatrix2D inverse = transform.inverse();
1091 
1092         // assert
1093         final double[] expected = {
1094             1, 0, -1,
1095             0, 1, 2
1096         };
1097         Assertions.assertArrayEquals(expected, inverse.toArray(), 0.0);
1098     }
1099 
1100     @Test
1101     void testInverse_scale() {
1102         // arrange
1103         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createScale(10, -2);
1104 
1105         // act
1106         final AffineTransformMatrix2D inverse = transform.inverse();
1107 
1108         // assert
1109         final double[] expected = {
1110             0.1, 0, 0,
1111             0, -0.5, 0
1112         };
1113         Assertions.assertArrayEquals(expected, inverse.toArray(), 0.0);
1114     }
1115 
1116     @Test
1117     void testInverse_rotate() {
1118         // arrange
1119         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createRotation(Angle.PI_OVER_TWO);
1120 
1121         // act
1122         final AffineTransformMatrix2D inverse = transform.inverse();
1123 
1124         // assert
1125         final double[] expected = {
1126             0, 1, 0,
1127             -1, 0, 0
1128         };
1129         Assertions.assertArrayEquals(expected, inverse.toArray(), EPS);
1130     }
1131 
1132     @Test
1133     void testInverse_rotate_aroundCenter() {
1134         // arrange
1135         final Vector2D center = Vector2D.of(1, 2);
1136         final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createRotation(center, Angle.PI_OVER_TWO);
1137 
1138         // act
1139         final AffineTransformMatrix2D inverse = transform.inverse();
1140 
1141         // assert
1142         final double[] expected = {
1143             0, 1, -1,
1144             -1, 0, 3
1145         };
1146         Assertions.assertArrayEquals(expected, inverse.toArray(), EPS);
1147     }
1148 
1149     @Test
1150     void testInverse_undoesOriginalTransform() {
1151         // arrange
1152         final Vector2D v1 = Vector2D.ZERO;
1153         final Vector2D v2 = Vector2D.Unit.PLUS_X;
1154         final Vector2D v3 = Vector2D.of(1, 1);
1155         final Vector2D v4 = Vector2D.of(-2, 3);
1156 
1157         final Vector2D center = Vector2D.of(-0.5, 2);
1158 
1159         // act/assert
1160         runWithCoordinates((x, y) -> {
1161             final AffineTransformMatrix2D transform = AffineTransformMatrix2D
1162                         .createTranslation(x, y)
1163                         .scale(2, 3)
1164                         .translate(x / 3, y / 3)
1165                         .rotate(x / 4)
1166                         .rotate(center, y / 2);
1167 
1168             final AffineTransformMatrix2D inverse = transform.inverse();
1169 
1170             EuclideanTestUtils.assertCoordinatesEqual(v1, inverse.apply(transform.apply(v1)), EPS);
1171             EuclideanTestUtils.assertCoordinatesEqual(v2, inverse.apply(transform.apply(v2)), EPS);
1172             EuclideanTestUtils.assertCoordinatesEqual(v3, inverse.apply(transform.apply(v3)), EPS);
1173             EuclideanTestUtils.assertCoordinatesEqual(v4, inverse.apply(transform.apply(v4)), EPS);
1174         });
1175     }
1176 
1177     @Test
1178     void testInverse_nonInvertible() {
1179         // act/assert
1180         GeometryTestUtils.assertThrowsWithMessage(() -> AffineTransformMatrix2D.of(
1181                 0, 0, 0,
1182                 0, 0, 0).inverse(), IllegalStateException.class, "Matrix is not invertible; matrix determinant is 0.0");
1183 
1184         GeometryTestUtils.assertThrowsWithMessage(() -> AffineTransformMatrix2D.of(
1185                 1, 0, 0,
1186                 0, Double.NaN, 0).inverse(), IllegalStateException.class, "Matrix is not invertible; matrix determinant is NaN");
1187 
1188         GeometryTestUtils.assertThrowsWithMessage(() -> AffineTransformMatrix2D.of(
1189                 1, 0, 0,
1190                 0, Double.NEGATIVE_INFINITY, 0).inverse(), IllegalStateException.class, "Matrix is not invertible; matrix determinant is -Infinity");
1191 
1192         GeometryTestUtils.assertThrowsWithMessage(() -> AffineTransformMatrix2D.of(
1193                 Double.POSITIVE_INFINITY, 0, 0,
1194                 0, 1, 0).inverse(), IllegalStateException.class, "Matrix is not invertible; matrix determinant is Infinity");
1195 
1196         GeometryTestUtils.assertThrowsWithMessage(() -> AffineTransformMatrix2D.of(
1197                 1, 0, Double.NaN,
1198                 0, 1, 0).inverse(), IllegalStateException.class, "Matrix is not invertible; invalid matrix element: NaN");
1199 
1200         GeometryTestUtils.assertThrowsWithMessage(() -> AffineTransformMatrix2D.of(
1201                 1, 0, Double.POSITIVE_INFINITY,
1202                 0, 1, 0).inverse(), IllegalStateException.class, "Matrix is not invertible; invalid matrix element: Infinity");
1203 
1204         GeometryTestUtils.assertThrowsWithMessage(() -> AffineTransformMatrix2D.of(
1205                 1, 0, Double.NEGATIVE_INFINITY,
1206                 0, 1, 0).inverse(), IllegalStateException.class, "Matrix is not invertible; invalid matrix element: -Infinity");
1207     }
1208 
1209     @Test
1210     void testLinear() {
1211         // arrange
1212         final AffineTransformMatrix2D mat = AffineTransformMatrix2D.of(
1213                 2, 3, 4,
1214                 5, 6, 7);
1215 
1216         // act
1217         final AffineTransformMatrix2D result = mat.linear();
1218 
1219         // assert
1220         final double[] expected = {
1221             2, 3, 0,
1222             5, 6, 0
1223         };
1224         Assertions.assertArrayEquals(expected, result.toArray(), 0.0);
1225     }
1226 
1227     @Test
1228     void testLinearTranspose() {
1229         // arrange
1230         final AffineTransformMatrix2D mat = AffineTransformMatrix2D.of(
1231                 2, 3, 4,
1232                 5, 6, 7);
1233 
1234         // act
1235         final AffineTransformMatrix2D result = mat.linearTranspose();
1236 
1237         // assert
1238         final double[] expected = {
1239             2, 5, 0,
1240             3, 6, 0
1241         };
1242         Assertions.assertArrayEquals(expected, result.toArray(), 0.0);
1243     }
1244 
1245     @Test
1246     void testNormalTransform() {
1247         // act/assert
1248         checkNormalTransform(AffineTransformMatrix2D.identity());
1249 
1250         checkNormalTransform(AffineTransformMatrix2D.createTranslation(2, 3));
1251         checkNormalTransform(AffineTransformMatrix2D.createTranslation(-3, -4));
1252 
1253         checkNormalTransform(AffineTransformMatrix2D.createScale(2, 5));
1254         checkNormalTransform(AffineTransformMatrix2D.createScale(-3, 4));
1255         checkNormalTransform(AffineTransformMatrix2D.createScale(-2, -5));
1256 
1257         checkNormalTransform(AffineTransformMatrix2D.createRotation(Angle.PI_OVER_TWO));
1258         checkNormalTransform(AffineTransformMatrix2D.createRotation(THREE_PI_OVER_TWO));
1259 
1260         checkNormalTransform(AffineTransformMatrix2D.createRotation(Vector2D.of(3, 4), THREE_PI_OVER_TWO)
1261                 .translate(8, 2)
1262                 .scale(-3, -2));
1263         checkNormalTransform(AffineTransformMatrix2D.createScale(2, -1)
1264                 .translate(-3, -4)
1265                 .rotate(Vector2D.of(-0.5, 0.5), 0.75 * Math.PI));
1266     }
1267 
1268     private void checkNormalTransform(final AffineTransformMatrix2D transform) {
1269         final AffineTransformMatrix2D normalTransform = transform.normalTransform();
1270 
1271         final Vector2D p1 = Vector2D.of(-0.25, 0.75);
1272         final Vector2D t1 = transform.apply(p1);
1273 
1274         EuclideanTestUtils.permute(-10, 10, 1, (x, y) -> {
1275             final Vector2D p2 = Vector2D.of(x, y);
1276             final Vector2D n = Lines.fromPoints(p1, p2, TEST_PRECISION).getOffsetDirection();
1277 
1278             final Vector2D t2 = transform.apply(p2);
1279 
1280             final Line tLine = transform.preservesOrientation() ?
1281                     Lines.fromPoints(t1, t2, TEST_PRECISION) :
1282                     Lines.fromPoints(t2, t1, TEST_PRECISION);
1283             final Vector2D expected = tLine.getOffsetDirection();
1284 
1285             final Vector2D actual = normalTransform.apply(n).normalize();
1286 
1287             EuclideanTestUtils.assertCoordinatesEqual(expected, actual, EPS);
1288         });
1289     }
1290 
1291     @Test
1292     void testNormalTransform_nonInvertible() {
1293         // act/assert
1294         Assertions.assertThrows(IllegalStateException.class, () -> AffineTransformMatrix2D.createScale(0).normalTransform());
1295     }
1296 
1297     @Test
1298     void testHashCode() {
1299         // arrange
1300         final double[] values = {
1301             1, 2, 3,
1302             5, 6, 7
1303         };
1304 
1305         // act/assert
1306         final int orig = AffineTransformMatrix2D.of(values).hashCode();
1307         final int same = AffineTransformMatrix2D.of(values).hashCode();
1308 
1309         Assertions.assertEquals(orig, same);
1310 
1311         double[] temp;
1312         for (int i = 0; i < values.length; ++i) {
1313             temp = values.clone();
1314             temp[i] = 0;
1315 
1316             final int modified = AffineTransformMatrix2D.of(temp).hashCode();
1317 
1318             Assertions.assertNotEquals(orig, modified);
1319         }
1320     }
1321 
1322     @Test
1323     void testEquals() {
1324         // arrange
1325         final double[] values = {
1326             1, 2, 3,
1327             5, 6, 7
1328         };
1329 
1330         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(values);
1331 
1332         // act/assert
1333         GeometryTestUtils.assertSimpleEqualsCases(a);
1334 
1335         double[] temp;
1336         for (int i = 0; i < values.length; ++i) {
1337             temp = values.clone();
1338             temp[i] = 0;
1339 
1340             final AffineTransformMatrix2D modified = AffineTransformMatrix2D.of(temp);
1341 
1342             Assertions.assertNotEquals(a, modified);
1343         }
1344     }
1345 
1346     @Test
1347     void testEqualsAndHashCode_signedZeroConsistency() {
1348         // arrange
1349         final double[] arrWithPosZero = {
1350             1.0, 0.0, 0.0,
1351             0.0, 1.0, 0.0
1352         };
1353         final double[] arrWithNegZero = {
1354             1.0, 0.0, 0.0,
1355             0.0, 1.0, -0.0
1356         };
1357         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(arrWithPosZero);
1358         final AffineTransformMatrix2D b = AffineTransformMatrix2D.of(arrWithNegZero);
1359         final AffineTransformMatrix2D c = AffineTransformMatrix2D.of(arrWithPosZero);
1360         final AffineTransformMatrix2D d = AffineTransformMatrix2D.of(arrWithNegZero);
1361 
1362         // act/assert
1363         Assertions.assertFalse(a.equals(b));
1364         Assertions.assertNotEquals(a.hashCode(), b.hashCode());
1365 
1366         Assertions.assertTrue(a.equals(c));
1367         Assertions.assertEquals(a.hashCode(), c.hashCode());
1368 
1369         Assertions.assertTrue(b.equals(d));
1370         Assertions.assertEquals(b.hashCode(), d.hashCode());
1371     }
1372 
1373     @Test
1374     void testToString() {
1375         // arrange
1376         final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
1377                     1, 2, 3,
1378                     5, 6, 7
1379                 );
1380 
1381         // act
1382         final String result = a.toString();
1383 
1384         // assert
1385         Assertions.assertEquals(
1386                 "[ 1.0, 2.0, 3.0; " +
1387                 "5.0, 6.0, 7.0 ]", result);
1388     }
1389 
1390     @FunctionalInterface
1391     private interface Coordinate2DTest {
1392 
1393         void run(double x, double y);
1394     }
1395 
1396     private static void runWithCoordinates(final Coordinate2DTest test) {
1397         runWithCoordinates(test, -1e-2, 1e-2, 5e-3);
1398         runWithCoordinates(test, -1e2, 1e2, 5);
1399     }
1400 
1401     private static void runWithCoordinates(final Coordinate2DTest test, final double min, final double max, final double step) {
1402         for (double x = min; x <= max; x += step) {
1403             for (double y = min; y <= max; y += step) {
1404                 test.run(x, y);
1405             }
1406         }
1407     }
1408 }