1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
41 final double[] arr = {
42 1, 2, 3,
43 4, 5, 6
44 };
45
46
47 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.of(arr);
48
49
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
58 GeometryTestUtils.assertThrowsWithMessage(() -> AffineTransformMatrix2D.of(1, 2),
59 IllegalArgumentException.class, "Dimension mismatch: 2 != 6");
60 }
61
62 @Test
63 void testFromColumnVectors_twoVector() {
64
65 final Vector2D u = Vector2D.of(1, 2);
66 final Vector2D v = Vector2D.of(3, 4);
67
68
69 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.fromColumnVectors(u, v);
70
71
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
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
86 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.fromColumnVectors(u, v, t);
87
88
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
98 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity();
99
100
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
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
132 Assertions.assertThrows(IllegalArgumentException.class, () -> AffineTransformMatrix2D.from(v -> v.multiply(0)));
133 }
134
135 @Test
136 void testCreateTranslation_xy() {
137
138 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createTranslation(2, 3);
139
140
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
151 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createTranslation(Vector2D.of(5, 6));
152
153
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
164 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createScale(2, 3);
165
166
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
177 final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
178 2, 0, 10,
179 0, 3, 11
180 );
181
182
183 final AffineTransformMatrix2D result = a.translate(4, 5);
184
185
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
196 final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
197 2, 0, 10,
198 0, 3, 11
199 );
200
201
202 final AffineTransformMatrix2D result = a.translate(Vector2D.of(7, 8));
203
204
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
215 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createScale(Vector2D.of(4, 5));
216
217
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
228 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createScale(7);
229
230
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
241 final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
242 2, 0, 10,
243 0, 3, 11
244 );
245
246
247 final AffineTransformMatrix2D result = a.scale(4, 5);
248
249
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
260 final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
261 2, 0, 10,
262 0, 3, 11
263 );
264
265
266 final AffineTransformMatrix2D result = a.scale(Vector2D.of(7, 8));
267
268
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
279 final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
280 2, 0, 10,
281 0, 3, 11
282 );
283
284
285 final AffineTransformMatrix2D result = a.scale(10);
286
287
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
298 final double angle = Math.PI * 2.0 / 3.0;
299 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createRotation(angle);
300
301
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
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
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
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
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
352 final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
353 1, 2, 3,
354 4, 5, 6
355 );
356
357
358 final AffineTransformMatrix2D result = a.rotate(Angle.PI_OVER_TWO);
359
360
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
371 final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
372 1, 2, 3,
373 4, 5, 6
374 );
375
376
377 final AffineTransformMatrix2D result = a.rotate(Rotation2D.of(Angle.PI_OVER_TWO));
378
379
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
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
398 final AffineTransformMatrix2D result = a.rotate(center, Angle.PI_OVER_TWO);
399
400
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
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
419 final AffineTransformMatrix2D result = a.rotate(center, Rotation2D.of(Angle.PI_OVER_TWO));
420
421
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
432 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createShear(2, 3);
433
434
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
445 final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
446 1, 2, 3,
447 4, 5, 6
448 );
449
450
451 final AffineTransformMatrix2D result = a.shear(-2, 3);
452
453
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
464 final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
465 1, 2, 3,
466 4, 5, 6
467 );
468
469
470 final AffineTransformMatrix2D result = a.shear(0, 0);
471
472
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
483 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity();
484
485
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
496 final Vector2D translation = Vector2D.of(1.1, -Math.PI);
497
498 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
499 .translate(translation);
500
501
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
514 final Vector2D factors = Vector2D.of(2.0, -3.0);
515
516 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
517 .scale(factors);
518
519
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
532 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
533 .rotate(-Angle.PI_OVER_TWO);
534
535
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
548 final Vector2D center = Vector2D.of(1, 2);
549 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
550 .rotate(center, -Angle.PI_OVER_TWO);
551
552
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
566 final Vector2D center = Vector2D.of(1, 2);
567 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
568 .rotate(center, Math.PI);
569
570
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
584 final double shearFactor = -2;
585 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
586 .shear(shearFactor, 0);
587
588
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
601 final double shearFactor = 2;
602 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
603 .shear(0, shearFactor);
604
605
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
618 final double shearX = 2;
619 final double shearY = -3;
620 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
621 .shear(shearX, shearY);
622
623
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
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
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
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
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
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
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
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
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
742 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity();
743
744
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
755 final Vector2D translation = Vector2D.of(1.1, -Math.PI);
756
757 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
758 .translate(translation);
759
760
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
771 final Vector2D factors = Vector2D.of(2.0, -3.0);
772
773 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
774 .scale(factors);
775
776
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
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
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
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
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
831 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity();
832
833
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
844 final Vector2D translation = Vector2D.of(1.1, -Math.PI);
845
846 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
847 .translate(translation);
848
849
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
860 final Vector2D factors = Vector2D.of(2.0, -3.0);
861
862 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.identity()
863 .scale(factors);
864
865
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
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
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
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
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
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
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
961 final AffineTransformMatrix2D result = a.multiply(b);
962
963
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
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
984 final AffineTransformMatrix2D transform = d.multiply(c).multiply(b).multiply(a);
985
986
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
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
1012 final AffineTransformMatrix2D result = b.premultiply(a);
1013
1014
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
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
1035 final AffineTransformMatrix2D transform = a.premultiply(b).premultiply(c).premultiply(d);
1036
1037
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
1053 final AffineTransformMatrix2D inverse = AffineTransformMatrix2D.identity().inverse();
1054
1055
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
1066 final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
1067 1, 3, 7,
1068 2, 4, 9
1069 );
1070
1071 final AffineTransformMatrix2D inv = a.inverse();
1072
1073
1074 final AffineTransformMatrix2D result = inv.multiply(a);
1075
1076
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
1087 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createTranslation(1, -2);
1088
1089
1090 final AffineTransformMatrix2D inverse = transform.inverse();
1091
1092
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
1103 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createScale(10, -2);
1104
1105
1106 final AffineTransformMatrix2D inverse = transform.inverse();
1107
1108
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
1119 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createRotation(Angle.PI_OVER_TWO);
1120
1121
1122 final AffineTransformMatrix2D inverse = transform.inverse();
1123
1124
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
1135 final Vector2D center = Vector2D.of(1, 2);
1136 final AffineTransformMatrix2D transform = AffineTransformMatrix2D.createRotation(center, Angle.PI_OVER_TWO);
1137
1138
1139 final AffineTransformMatrix2D inverse = transform.inverse();
1140
1141
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
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
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
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
1212 final AffineTransformMatrix2D mat = AffineTransformMatrix2D.of(
1213 2, 3, 4,
1214 5, 6, 7);
1215
1216
1217 final AffineTransformMatrix2D result = mat.linear();
1218
1219
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
1230 final AffineTransformMatrix2D mat = AffineTransformMatrix2D.of(
1231 2, 3, 4,
1232 5, 6, 7);
1233
1234
1235 final AffineTransformMatrix2D result = mat.linearTranspose();
1236
1237
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
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
1294 Assertions.assertThrows(IllegalStateException.class, () -> AffineTransformMatrix2D.createScale(0).normalTransform());
1295 }
1296
1297 @Test
1298 void testHashCode() {
1299
1300 final double[] values = {
1301 1, 2, 3,
1302 5, 6, 7
1303 };
1304
1305
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
1325 final double[] values = {
1326 1, 2, 3,
1327 5, 6, 7
1328 };
1329
1330 final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(values);
1331
1332
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
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
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
1376 final AffineTransformMatrix2D a = AffineTransformMatrix2D.of(
1377 1, 2, 3,
1378 5, 6, 7
1379 );
1380
1381
1382 final String result = a.toString();
1383
1384
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 }