You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by er...@apache.org on 2020/02/23 17:02:45 UTC
[commons-geometry] 01/04: GEOMETRY-89: adding linear,
linearTranspose, and normalTransform methods to AffineTransformMatrixXD
This is an automated email from the ASF dual-hosted git repository.
erans pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-geometry.git
commit d39700591b7e1e2cdad20e6c5a5094a43cf6c54c
Author: Matt Juntunen <ma...@hotmail.com>
AuthorDate: Sat Feb 22 11:57:53 2020 -0500
GEOMETRY-89: adding linear, linearTranspose, and normalTransform methods to AffineTransformMatrixXD
---
.../euclidean/AbstractAffineTransformMatrix.java | 38 +++++++-
.../euclidean/oned/AffineTransformMatrix1D.java | 32 +++++-
.../euclidean/threed/AffineTransformMatrix3D.java | 40 +++++++-
.../euclidean/twod/AffineTransformMatrix2D.java | 36 ++++++-
.../oned/AffineTransformMatrix1DTest.java | 66 +++++++++++++
.../threed/AffineTransformMatrix3DTest.java | 107 +++++++++++++++++++++
.../twod/AffineTransformMatrix2DTest.java | 95 ++++++++++++++++++
7 files changed, 407 insertions(+), 7 deletions(-)
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractAffineTransformMatrix.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractAffineTransformMatrix.java
index 9674517..58e1d5a 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractAffineTransformMatrix.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/AbstractAffineTransformMatrix.java
@@ -19,8 +19,11 @@ package org.apache.commons.geometry.euclidean;
/** Base class for affine transform matrices in Euclidean space.
*
* @param <V> Vector/point implementation type defining the space.
+ * @param <M> Matrix transform implementation type.
*/
-public abstract class AbstractAffineTransformMatrix<V extends EuclideanVector<V>>
+public abstract class AbstractAffineTransformMatrix<
+ V extends EuclideanVector<V>,
+ M extends AbstractAffineTransformMatrix<V, M>>
implements EuclideanTransform<V> {
/** Apply this transform to the given vector, ignoring translations and normalizing the
@@ -40,6 +43,39 @@ public abstract class AbstractAffineTransformMatrix<V extends EuclideanVector<V>
public abstract double determinant();
/** {@inheritDoc}
+ * @throws IllegalStateException if the matrix cannot be inverted
+ */
+ @Override
+ public abstract M inverse();
+
+ /** Return a matrix containing only the linear portion of this transform.
+ * The returned instance contains the same matrix elements as this instance
+ * but with the translation component set to zero.
+ * @return a matrix containing only the linear portion of this transform
+ */
+ public abstract M linear();
+
+ /** Return a matrix containing the transpose of the linear portion of this transform.
+ * The returned instance is linear, meaning it has a translation component of zero.
+ * @return a matrix containing the transpose of the linear portion of this transform
+ */
+ public abstract M linearTranspose();
+
+ /** Return a transform suitable for transforming normals. The returned matrix is
+ * the inverse transpose of the linear portion of this instance, i.e.
+ * <code>N = (L<sup>-1</sup>)<sup>T</sup></code>, where <code>L</code> is the linear portion
+ * of this instance and <code>N</code> is the returned matrix. Note that normals
+ * transformed with the returned matrix may be scaled during transformation and require
+ * normalization.
+ * @return a transform suitable for transforming normals
+ * @throws IllegalStateException if the matrix cannot be inverted
+ * @see <a href="https://en.wikipedia.org/wiki/Normal_(geometry)#Transforming_normals">Transforming normals</a>
+ */
+ public M normalTransform() {
+ return inverse().linearTranspose();
+ }
+
+ /** {@inheritDoc}
*
* <p>This method returns true if the determinant of the matrix is positive.</p>
*/
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/AffineTransformMatrix1D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/AffineTransformMatrix1D.java
index 66cfd36..0b773df 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/AffineTransformMatrix1D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/AffineTransformMatrix1D.java
@@ -31,7 +31,7 @@ import org.apache.commons.geometry.euclidean.internal.Vectors;
* use arrays containing 2 elements, instead of 4.
* </p>
*/
-public final class AffineTransformMatrix1D extends AbstractAffineTransformMatrix<Vector1D> {
+public final class AffineTransformMatrix1D extends AbstractAffineTransformMatrix<Vector1D, AffineTransformMatrix1D> {
/** The number of internal matrix elements. */
private static final int NUM_ELEMENTS = 2;
@@ -112,6 +112,34 @@ public final class AffineTransformMatrix1D extends AbstractAffineTransformMatrix
return m00;
}
+ /** {@inheritDoc}
+ *
+ * <p><strong>Example</strong>
+ * <pre>
+ * [ a, b ] [ a, 0 ]
+ * [ 0, 1 ] → [ 0, 1 ]
+ * </pre>
+ */
+ @Override
+ public AffineTransformMatrix1D linear() {
+ return new AffineTransformMatrix1D(m00, 0.0);
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p>In the one dimensional case, this is exactly the same as {@link #linear()}.</p>
+ *
+ * <p><strong>Example</strong>
+ * <pre>
+ * [ a, b ] [ a, 0 ]
+ * [ 0, 1 ] → [ 0, 1 ]
+ * </pre>
+ */
+ @Override
+ public AffineTransformMatrix1D linearTranspose() {
+ return linear();
+ }
+
/** Get a new transform containing the result of applying a translation logically after
* the transformation represented by the current instance. This is achieved by
* creating a new translation transform and pre-multiplying it with the current
@@ -200,7 +228,7 @@ public final class AffineTransformMatrix1D extends AbstractAffineTransformMatrix
/** {@inheritDoc}
*
- * @throws IllegalStateException if the transform matrix cannot be inverted
+ * @throws IllegalStateException if the matrix cannot be inverted
*/
@Override
public AffineTransformMatrix1D inverse() {
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AffineTransformMatrix3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AffineTransformMatrix3D.java
index a0fcf21..ab46582 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AffineTransformMatrix3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/AffineTransformMatrix3D.java
@@ -34,7 +34,7 @@ import org.apache.commons.numbers.core.Precision;
* use arrays containing 12 elements, instead of 16.
* </p>
*/
-public final class AffineTransformMatrix3D extends AbstractAffineTransformMatrix<Vector3D> {
+public final class AffineTransformMatrix3D extends AbstractAffineTransformMatrix<Vector3D, AffineTransformMatrix3D> {
/** The number of internal matrix elements. */
private static final int NUM_ELEMENTS = 12;
@@ -206,6 +206,42 @@ public final class AffineTransformMatrix3D extends AbstractAffineTransformMatrix
);
}
+ /** {@inheritDoc}
+ *
+ * <p><strong>Example</strong>
+ * <pre>
+ * [ a, b, c, d ] [ a, b, c, 0 ]
+ * [ e, f, g, h ] [ e, f, g, 0 ]
+ * [ i, j, k, l ] → [ i, j, k, 0 ]
+ * [ 0, 0, 0, 1 ] [ 0, 0, 0, 1 ]
+ * </pre>
+ */
+ @Override
+ public AffineTransformMatrix3D linear() {
+ return new AffineTransformMatrix3D(
+ m00, m01, m02, 0.0,
+ m10, m11, m12, 0.0,
+ m20, m21, m22, 0.0);
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p><strong>Example</strong>
+ * <pre>
+ * [ a, b, c, d ] [ a, e, i, 0 ]
+ * [ e, f, g, h ] [ b, f, j, 0 ]
+ * [ i, j, k, l ] → [ c, g, k, 0 ]
+ * [ 0, 0, 0, 1 ] [ 0, 0, 0, 1 ]
+ * </pre>
+ */
+ @Override
+ public AffineTransformMatrix3D linearTranspose() {
+ return new AffineTransformMatrix3D(
+ m00, m10, m20, 0.0,
+ m01, m11, m21, 0.0,
+ m02, m12, m22, 0.0);
+ }
+
/** Apply a translation to the current instance, returning the result as a new transform.
* @param translation vector containing the translation values for each axis
* @return a new transform containing the result of applying a translation to
@@ -318,7 +354,7 @@ public final class AffineTransformMatrix3D extends AbstractAffineTransformMatrix
/** {@inheritDoc}
*
- * @throws IllegalStateException if the transform matrix cannot be inverted
+ * @throws IllegalStateException if the matrix cannot be inverted
*/
@Override
public AffineTransformMatrix3D inverse() {
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2D.java
index a311fa2..b45ac3e 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2D.java
@@ -33,7 +33,7 @@ import org.apache.commons.numbers.core.Precision;
* use arrays containing 6 elements, instead of 9.
* </p>
*/
-public final class AffineTransformMatrix2D extends AbstractAffineTransformMatrix<Vector2D> {
+public final class AffineTransformMatrix2D extends AbstractAffineTransformMatrix<Vector2D, AffineTransformMatrix2D> {
/** The number of internal matrix elements. */
private static final int NUM_ELEMENTS = 6;
@@ -170,6 +170,38 @@ public final class AffineTransformMatrix2D extends AbstractAffineTransformMatrix
);
}
+ /** {@inheritDoc}
+ *
+ * <p><strong>Example</strong>
+ * <pre>
+ * [ a, b, c ] [ a, b, 0 ]
+ * [ d, e, f ] → [ d, e, 0 ]
+ * [ 0, 0, 1 ] [ 0, 0, 1 ]
+ * </pre>
+ */
+ @Override
+ public AffineTransformMatrix2D linear() {
+ return new AffineTransformMatrix2D(
+ m00, m01, 0.0,
+ m10, m11, 0.0);
+ }
+
+ /** {@inheritDoc}
+ *
+ * <p><strong>Example</strong>
+ * <pre>
+ * [ a, b, c ] [ a, d, 0 ]
+ * [ d, e, f ] → [ b, e, 0 ]
+ * [ 0, 0, 1 ] [ 0, 0, 1 ]
+ * </pre>
+ */
+ @Override
+ public AffineTransformMatrix2D linearTranspose() {
+ return new AffineTransformMatrix2D(
+ m00, m10, 0.0,
+ m01, m11, 0.0);
+ }
+
/** Apply a translation to the current instance, returning the result as a new transform.
* @param translation vector containing the translation values for each axis
* @return a new transform containing the result of applying a translation to
@@ -277,7 +309,7 @@ public final class AffineTransformMatrix2D extends AbstractAffineTransformMatrix
/** {@inheritDoc}
*
- * @throws IllegalStateException if the transform matrix cannot be inverted
+ * @throws IllegalStateException if the matrix cannot be inverted
*/
@Override
public AffineTransformMatrix2D inverse() {
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/AffineTransformMatrix1DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/AffineTransformMatrix1DTest.java
index 28b6e98..ebd92c2 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/AffineTransformMatrix1DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/AffineTransformMatrix1DTest.java
@@ -533,6 +533,72 @@ public class AffineTransformMatrix1DTest {
}
@Test
+ public void testLinear() {
+ // arrange
+ AffineTransformMatrix1D mat = AffineTransformMatrix1D.of(2, 3);
+
+ // act
+ AffineTransformMatrix1D result = mat.linear();
+
+ // assert
+ Assert.assertArrayEquals(new double[] {2, 0}, result.toArray(), 0.0);
+ }
+
+ @Test
+ public void testLinearTranspose() {
+ // arrange
+ AffineTransformMatrix1D mat = AffineTransformMatrix1D.of(2, 3);
+
+ // act
+ AffineTransformMatrix1D result = mat.linearTranspose();
+
+ // assert
+ Assert.assertArrayEquals(new double[] {2, 0}, result.toArray(), 0.0);
+ }
+
+ @Test
+ public void testNormalTransform() {
+ // act/assert
+ checkNormalTransform(AffineTransformMatrix1D.identity());
+
+ checkNormalTransform(AffineTransformMatrix1D.createTranslation(4));
+ checkNormalTransform(AffineTransformMatrix1D.createTranslation(-4));
+
+ checkNormalTransform(AffineTransformMatrix1D.createScale(2));
+ checkNormalTransform(AffineTransformMatrix1D.createScale(-2));
+
+ checkNormalTransform(AffineTransformMatrix1D.createScale(2).translate(3));
+ checkNormalTransform(AffineTransformMatrix1D.createScale(2).translate(-3));
+ checkNormalTransform(AffineTransformMatrix1D.createTranslation(2).scale(-3));
+ checkNormalTransform(AffineTransformMatrix1D.createTranslation(-4).scale(-1));
+ }
+
+ private void checkNormalTransform(AffineTransformMatrix1D transform) {
+ AffineTransformMatrix1D normalTransform = transform.normalTransform();
+
+ Vector1D expectedPlus = transform.apply(Vector1D.Unit.PLUS)
+ .subtract(transform.apply(Vector1D.ZERO))
+ .normalize();
+
+ Vector1D expectedMinus = transform.apply(Vector1D.Unit.MINUS)
+ .subtract(transform.apply(Vector1D.ZERO))
+ .normalize();
+
+ EuclideanTestUtils.assertCoordinatesEqual(expectedPlus,
+ normalTransform.apply(Vector1D.Unit.PLUS).normalize(), EPS);
+ EuclideanTestUtils.assertCoordinatesEqual(expectedMinus,
+ normalTransform.apply(Vector1D.Unit.MINUS).normalize(), EPS);
+ }
+
+ @Test
+ public void testNormalTransform_nonInvertible() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ AffineTransformMatrix1D.createScale(0).normalTransform();
+ }, IllegalStateException.class);
+ }
+
+ @Test
public void testInverse_identity() {
// act
AffineTransformMatrix1D inverse = AffineTransformMatrix1D.identity().inverse();
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/AffineTransformMatrix3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/AffineTransformMatrix3DTest.java
index 05969bb..31da300 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/AffineTransformMatrix3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/AffineTransformMatrix3DTest.java
@@ -19,6 +19,8 @@ package org.apache.commons.geometry.euclidean.threed;
import java.util.function.UnaryOperator;
import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
import org.apache.commons.geometry.euclidean.EuclideanTestUtils.PermuteCallback3D;
import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
@@ -31,6 +33,9 @@ public class AffineTransformMatrix3DTest {
private static final double EPS = 1e-12;
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(EPS);
+
@Test
public void testOf() {
// arrange
@@ -1020,6 +1025,108 @@ public class AffineTransformMatrix3DTest {
}
@Test
+ public void testLinear() {
+ // arrange
+ AffineTransformMatrix3D mat = AffineTransformMatrix3D.of(
+ 2, 3, 4, 5,
+ 6, 7, 8, 9,
+ 10, 11, 12, 13);
+
+ // act
+ AffineTransformMatrix3D result = mat.linear();
+
+ // assert
+ double[] expected = {
+ 2, 3, 4, 0,
+ 6, 7, 8, 0,
+ 10, 11, 12, 0
+ };
+ Assert.assertArrayEquals(expected, result.toArray(), 0.0);
+ }
+
+ @Test
+ public void testLinearTranspose() {
+ // arrange
+ AffineTransformMatrix3D mat = AffineTransformMatrix3D.of(
+ 2, 3, 4, 5,
+ 6, 7, 8, 9,
+ 10, 11, 12, 13);
+
+ // act
+ AffineTransformMatrix3D result = mat.linearTranspose();
+
+ // assert
+ double[] expected = {
+ 2, 6, 10, 0,
+ 3, 7, 11, 0,
+ 4, 8, 12, 0
+ };
+ Assert.assertArrayEquals(expected, result.toArray(), 0.0);
+ }
+
+ @Test
+ public void testNormalTransform() {
+ // act/assert
+ checkNormalTransform(AffineTransformMatrix3D.identity());
+
+ checkNormalTransform(AffineTransformMatrix3D.createTranslation(2, 3, 4));
+ checkNormalTransform(AffineTransformMatrix3D.createTranslation(-3, -4, -5));
+
+ checkNormalTransform(AffineTransformMatrix3D.createScale(2, 5, 0.5));
+ checkNormalTransform(AffineTransformMatrix3D.createScale(-3, 4, 2));
+ checkNormalTransform(AffineTransformMatrix3D.createScale(-0.1, -0.5, 0.8));
+ checkNormalTransform(AffineTransformMatrix3D.createScale(-2, -5, -8));
+
+ QuaternionRotation rotA = QuaternionRotation.fromAxisAngle(Vector3D.of(2, 3, 4), 0.75 * Math.PI);
+ QuaternionRotation rotB = QuaternionRotation.fromAxisAngle(Vector3D.of(-1, 1, -1), 1.75 * Math.PI);
+
+ checkNormalTransform(AffineTransformMatrix3D.createRotation(Vector3D.of(1, 1, 1), rotA));
+ checkNormalTransform(AffineTransformMatrix3D.createRotation(Vector3D.of(-1, -1, -1), rotB));
+
+ checkNormalTransform(AffineTransformMatrix3D.createTranslation(2, 3, 4)
+ .scale(7, 5, 4)
+ .rotate(rotA));
+ checkNormalTransform(AffineTransformMatrix3D.createRotation(Vector3D.ZERO, rotB)
+ .translate(7, 5, 4)
+ .rotate(rotA)
+ .scale(2, 3, 0.5));
+ }
+
+ private void checkNormalTransform(AffineTransformMatrix3D transform) {
+ AffineTransformMatrix3D normalTransform = transform.normalTransform();
+
+ Vector3D p1 = Vector3D.of(-0.25, 0.75, 0.5);
+ Vector3D p2 = Vector3D.of(0.5, -0.75, 0.25);
+
+ Vector3D t1 = transform.apply(p1);
+ Vector3D t2 = transform.apply(p2);
+
+ EuclideanTestUtils.permute(-10, 10, 1, (x, y, z) -> {
+ Vector3D p3 = Vector3D.of(x, y, z);
+ Vector3D n = Plane.fromPoints(p1, p2, p3, TEST_PRECISION).getNormal();
+
+ Vector3D t3 = transform.apply(p3);
+
+ Plane tPlane = transform.preservesOrientation() ?
+ Plane.fromPoints(t1, t2, t3, TEST_PRECISION) :
+ Plane.fromPoints(t1, t3, t2, TEST_PRECISION);
+ Vector3D expected = tPlane.getNormal();
+
+ Vector3D actual = normalTransform.apply(n).normalize();
+
+ EuclideanTestUtils.assertCoordinatesEqual(expected, actual, EPS);
+ });
+ }
+
+ @Test
+ public void testNormalTransform_nonInvertible() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ AffineTransformMatrix3D.createScale(0).normalTransform();
+ }, IllegalStateException.class);
+ }
+
+ @Test
public void testHashCode() {
// arrange
double[] values = new double[] {
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2DTest.java
index 1ebf2be..0ac3e7a 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/AffineTransformMatrix2DTest.java
@@ -19,6 +19,8 @@ package org.apache.commons.geometry.euclidean.twod;
import java.util.function.UnaryOperator;
import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
import org.apache.commons.numbers.angle.PlaneAngleRadians;
import org.junit.Assert;
@@ -28,6 +30,9 @@ public class AffineTransformMatrix2DTest {
private static final double EPS = 1e-12;
+ private static final DoublePrecisionContext TEST_PRECISION =
+ new EpsilonDoublePrecisionContext(EPS);
+
@Test
public void testOf() {
// arrange
@@ -981,6 +986,96 @@ public class AffineTransformMatrix2DTest {
}
@Test
+ public void testLinear() {
+ // arrange
+ AffineTransformMatrix2D mat = AffineTransformMatrix2D.of(
+ 2, 3, 4,
+ 5, 6, 7);
+
+ // act
+ AffineTransformMatrix2D result = mat.linear();
+
+ // assert
+ double[] expected = {
+ 2, 3, 0,
+ 5, 6, 0
+ };
+ Assert.assertArrayEquals(expected, result.toArray(), 0.0);
+ }
+
+ @Test
+ public void testLinearTranspose() {
+ // arrange
+ AffineTransformMatrix2D mat = AffineTransformMatrix2D.of(
+ 2, 3, 4,
+ 5, 6, 7);
+
+ // act
+ AffineTransformMatrix2D result = mat.linearTranspose();
+
+ // assert
+ double[] expected = {
+ 2, 5, 0,
+ 3, 6, 0
+ };
+ Assert.assertArrayEquals(expected, result.toArray(), 0.0);
+ }
+
+ @Test
+ public void testNormalTransform() {
+ // act/assert
+ checkNormalTransform(AffineTransformMatrix2D.identity());
+
+ checkNormalTransform(AffineTransformMatrix2D.createTranslation(2, 3));
+ checkNormalTransform(AffineTransformMatrix2D.createTranslation(-3, -4));
+
+ checkNormalTransform(AffineTransformMatrix2D.createScale(2, 5));
+ checkNormalTransform(AffineTransformMatrix2D.createScale(-3, 4));
+ checkNormalTransform(AffineTransformMatrix2D.createScale(-2, -5));
+
+ checkNormalTransform(AffineTransformMatrix2D.createRotation(PlaneAngleRadians.PI_OVER_TWO));
+ checkNormalTransform(AffineTransformMatrix2D.createRotation(PlaneAngleRadians.THREE_PI_OVER_TWO));
+
+ checkNormalTransform(AffineTransformMatrix2D.createRotation(Vector2D.of(3, 4), PlaneAngleRadians.THREE_PI_OVER_TWO)
+ .translate(8, 2)
+ .scale(-3, -2));
+ checkNormalTransform(AffineTransformMatrix2D.createScale(2, -1)
+ .translate(-3, -4)
+ .rotate(Vector2D.of(-0.5, 0.5), 0.75 * Math.PI));
+ }
+
+ private void checkNormalTransform(AffineTransformMatrix2D transform) {
+ AffineTransformMatrix2D normalTransform = transform.normalTransform();
+
+ Vector2D p1 = Vector2D.of(-0.25, 0.75);
+ Vector2D t1 = transform.apply(p1);
+
+ EuclideanTestUtils.permute(-10, 10, 1, (x, y) -> {
+ Vector2D p2 = Vector2D.of(x, y);
+ Vector2D n = Line.fromPoints(p1, p2, TEST_PRECISION).getOffsetDirection();
+
+ Vector2D t2 = transform.apply(p2);
+
+ Line tLine = transform.preservesOrientation() ?
+ Line.fromPoints(t1, t2, TEST_PRECISION) :
+ Line.fromPoints(t2, t1, TEST_PRECISION);
+ Vector2D expected = tLine.getOffsetDirection();
+
+ Vector2D actual = normalTransform.apply(n).normalize();
+
+ EuclideanTestUtils.assertCoordinatesEqual(expected, actual, EPS);
+ });
+ }
+
+ @Test
+ public void testNormalTransform_nonInvertible() {
+ // act/assert
+ GeometryTestUtils.assertThrows(() -> {
+ AffineTransformMatrix2D.createScale(0).normalTransform();
+ }, IllegalStateException.class);
+ }
+
+ @Test
public void testHashCode() {
// arrange
double[] values = new double[] {