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 2018/09/17 13:41:55 UTC

[commons-geometry] 03/07: GEOMETRY-10: adding private UnitVector private classes in Vector2D and Vector1D for normalization optimizations; adding Point?D#directionTo() method

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 dd0d3baa4f58cdb5d75759696a5f17be2c1d0708
Author: Matt Juntunen <ma...@hotmail.com>
AuthorDate: Sun Sep 16 23:24:42 2018 -0400

    GEOMETRY-10: adding private UnitVector private classes in Vector2D and Vector1D for normalization optimizations; adding Point?D#directionTo() method
---
 .../commons/geometry/euclidean/EuclideanPoint.java | 11 ++++
 .../commons/geometry/euclidean/oned/Point1D.java   |  6 ++
 .../commons/geometry/euclidean/oned/Vector1D.java  | 51 +++++++++++++--
 .../commons/geometry/euclidean/threed/Point3D.java |  2 +
 .../geometry/euclidean/threed/Vector3D.java        |  8 +--
 .../commons/geometry/euclidean/twod/Point2D.java   |  9 +++
 .../commons/geometry/euclidean/twod/Vector2D.java  | 63 ++++++++++++++----
 .../geometry/euclidean/oned/Point1DTest.java       | 35 ++++++++++
 .../geometry/euclidean/oned/Vector1DTest.java      | 68 ++++++++++++++++++--
 .../geometry/euclidean/threed/Point3DTest.java     | 37 +++++++++++
 .../geometry/euclidean/threed/Vector3DTest.java    | 15 ++++-
 .../geometry/euclidean/twod/Point2DTest.java       | 37 +++++++++++
 .../geometry/euclidean/twod/Vector2DTest.java      | 74 +++++++++++++++++++---
 13 files changed, 376 insertions(+), 40 deletions(-)

diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/EuclideanPoint.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/EuclideanPoint.java
index bde9f67..ed662b2 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/EuclideanPoint.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/EuclideanPoint.java
@@ -41,6 +41,17 @@ public interface EuclideanPoint<P extends EuclideanPoint<P, V>, V extends Euclid
      */
     V vectorTo(P p);
 
+    /** Returns the unit vector representing the direction of displacement from this
+     * point to the given point. This is exactly equivalent to {@code p.subtract(thisPoint).normalize()}
+     * but without the intermediate vector instance.
+     * @param p the point the returned vector will be directed toward
+     * @return unit vector representing the direction of displacement <em>from</em> this point
+     *      <em>to</em> the given point
+     * @throws IllegalNormException if the norm of the vector pointing from this point to {@code p}
+     *      is zero, NaN, or infinite
+     */
+    V directionTo(P p);
+
     /** Linearly interpolates between this point and the given point using the equation
      * {@code P = (1 - t)*A + t*B}, where {@code A} is the current point and {@code B}
      * is the given point. This means that if {@code t = 0}, a point equal to the current
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Point1D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Point1D.java
index 4f0fec1..7e977b7 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Point1D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Point1D.java
@@ -83,6 +83,12 @@ public final class Point1D extends Cartesian1D implements EuclideanPoint<Point1D
 
     /** {@inheritDoc} */
     @Override
+    public Vector1D directionTo(Point1D p) {
+        return Vector1D.normalize(p.getX() - getX());
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public Point1D lerp(Point1D p, double t) {
         return vectorCombination(1.0 - t, this, t, p);
     }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Vector1D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Vector1D.java
index 79a4d35..b678559 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Vector1D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Vector1D.java
@@ -17,6 +17,7 @@
 package org.apache.commons.geometry.euclidean.oned;
 
 import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.exception.IllegalNormException;
 import org.apache.commons.geometry.core.internal.SimpleTupleFormat;
 import org.apache.commons.geometry.euclidean.EuclideanVector;
 import org.apache.commons.geometry.euclidean.internal.Vectors;
@@ -25,16 +26,16 @@ import org.apache.commons.numbers.arrays.LinearCombination;
 /** This class represents a vector in one-dimensional Euclidean space.
  * Instances of this class are guaranteed to be immutable.
  */
-public final class Vector1D extends Cartesian1D implements EuclideanVector<Point1D, Vector1D> {
+public class Vector1D extends Cartesian1D implements EuclideanVector<Point1D, Vector1D> {
 
     /** Zero vector (coordinates: 0). */
     public static final Vector1D ZERO = new Vector1D(0.0);
 
     /** Unit vector (coordinates: 1). */
-    public static final Vector1D ONE  = new Vector1D(1.0);
+    public static final Vector1D ONE  = new UnitVector(1.0);
 
     /** Negation of unit vector (coordinates: -1). */
-    public static final Vector1D MINUS_ONE = new Vector1D(-1.0);
+    public static final Vector1D MINUS_ONE = new UnitVector(-1.0);
 
     // CHECKSTYLE: stop ConstantName
     /** A vector with all coordinates set to NaN. */
@@ -154,9 +155,7 @@ public final class Vector1D extends Cartesian1D implements EuclideanVector<Point
     /** {@inheritDoc} */
     @Override
     public Vector1D normalize() {
-        Vectors.ensureFiniteNonZeroNorm(getNorm());
-
-        return (getX() > 0.0) ? ONE : MINUS_ONE;
+        return normalize(getX());
     }
 
     /** {@inheritDoc} */
@@ -290,6 +289,17 @@ public final class Vector1D extends Cartesian1D implements EuclideanVector<Point
         return new Vector1D(x);
     }
 
+    /** Returns a normalized vector derived from the given value.
+     * @param x abscissa (first coordinate value)
+     * @return normalized vector instance
+     * @throws IllegalNormException if the norm of the given value is zero, NaN, or infinite
+     */
+    public static Vector1D normalize(final double x) {
+        Vectors.ensureFiniteNonZeroNorm(Vectors.norm(x));
+
+        return (x > 0.0) ? ONE : MINUS_ONE;
+    }
+
     /** Parses the given string and returns a new vector instance. The expected string
      * format is the same as that returned by {@link #toString()}.
      * @param str the string to parse
@@ -380,4 +390,33 @@ public final class Vector1D extends Cartesian1D implements EuclideanVector<Point
         return new Vector1D(
                 LinearCombination.value(a1, c1.getX(), a2, c2.getX(), a3, c3.getX(), a4, c4.getX()));
     }
+
+    /** Private class used to represent unit vectors. This allows optimizations to be performed for certain
+     * operations.
+     */
+    private static final class UnitVector extends Vector1D {
+
+        /** Serializable version identifier */
+        private static final long serialVersionUID = 20180903L;
+
+        /** Simple constructor. Callers are responsible for ensuring that the given
+         * values represent a normalized vector.
+         * @param x abscissa (first coordinate value)
+         */
+        private UnitVector(final double x) {
+            super(x);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Vector1D normalize() {
+            return this;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Vector1D withMagnitude(final double mag) {
+            return scalarMultiply(mag);
+        }
+    }
 }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Point3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Point3D.java
index 9e44bf5..24626ec 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Point3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Point3D.java
@@ -87,6 +87,8 @@ public final class Point3D extends Cartesian3D implements EuclideanPoint<Point3D
         return p.subtract(this);
     }
 
+    /** {@inheritDoc} */
+    @Override
     public Vector3D directionTo(Point3D p) {
         return Vector3D.normalize(
                 p.getX() - getX(),
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java
index cb66b1c..0014d4d 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java
@@ -425,7 +425,7 @@ public class Vector3D extends Cartesian3D implements EuclideanVector<Point3D, Ve
         // We need to check the norm value here to ensure that it's legal. However, we don't
         // want to incur the cost or floating point error of getting the actual norm and then
         // multiplying it again to get the square norm. So, we'll just check the squared norm
-        // directly. This will produce the same result as checking the actual norm since
+        // directly. This will produce the same error result as checking the actual norm since
         // Math.sqrt(0.0) == 0.0, Math.sqrt(Double.NaN) == Double.NaN and
         // Math.sqrt(Double.POSITIVE_INFINITY) == Double.POSITIVE_INFINITY.
         final double baseMagSq = Vectors.ensureFiniteNonZeroNorm(base.getNormSq());
@@ -478,10 +478,10 @@ public class Vector3D extends Cartesian3D implements EuclideanVector<Point3D, Ve
 
     /** Returns a normalized vector derived from the given values.
      * @param x abscissa (first coordinate value)
-     * @param y abscissa (second coordinate value)
+     * @param y ordinate (second coordinate value)
      * @param z height (third coordinate value)
      * @return normalized vector instance
-     * @throws IllegalNormException if the norm of the given values is zero
+     * @throws IllegalNormException if the norm of the given values is zero, NaN, or infinite
      */
     public static Vector3D normalize(final double x, final double y, final double z) {
         final double norm = Vectors.ensureFiniteNonZeroNorm(Vectors.norm(x, y, z));
@@ -598,7 +598,7 @@ public class Vector3D extends Cartesian3D implements EuclideanVector<Point3D, Ve
         /** Simple constructor. Callers are responsible for ensuring that the given
          * values represent a normalized vector.
          * @param x abscissa (first coordinate value)
-         * @param y abscissa (second coordinate value)
+         * @param y ordinate (second coordinate value)
          * @param z height (third coordinate value)
          */
         private UnitVector(final double x, final double y, final double z) {
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Point2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Point2D.java
index ece33f0..ddd707e 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Point2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Point2D.java
@@ -80,6 +80,15 @@ public final class Point2D extends Cartesian2D implements EuclideanPoint<Point2D
 
     /** {@inheritDoc} */
     @Override
+    public Vector2D directionTo(Point2D p) {
+        return Vector2D.normalize(
+                    p.getX() - getX(),
+                    p.getY() - getY()
+                );
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public Point2D lerp(Point2D p, double t) {
         return vectorCombination(1.0 - t, this, t, p);
     }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java
index 36338d0..7ae7edf 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java
@@ -25,22 +25,22 @@ import org.apache.commons.numbers.arrays.LinearCombination;
 /** This class represents a vector in two-dimensional Euclidean space.
  * Instances of this class are guaranteed to be immutable.
  */
-public final class Vector2D extends Cartesian2D implements EuclideanVector<Point2D, Vector2D> {
+public class Vector2D extends Cartesian2D implements EuclideanVector<Point2D, Vector2D> {
 
     /** Zero vector (coordinates: 0, 0). */
     public static final Vector2D ZERO   = new Vector2D(0, 0);
 
     /** Unit vector pointing in the direction of the positive x-axis. */
-    public static final Vector2D PLUS_X = new Vector2D(1, 0);
+    public static final Vector2D PLUS_X = new UnitVector(1, 0);
 
     /** Unit vector pointing in the direction of the negative x-axis. */
-    public static final Vector2D MINUS_X = new Vector2D(-1, 0);
+    public static final Vector2D MINUS_X = new UnitVector(-1, 0);
 
     /** Unit vector pointing in the direction of the positive y-axis. */
-    public static final Vector2D PLUS_Y = new Vector2D(0, 1);
+    public static final Vector2D PLUS_Y = new UnitVector(0, 1);
 
     /** Unit vector pointing in the direction of the negative y-axis. */
-    public static final Vector2D MINUS_Y = new Vector2D(0, -1);
+    public static final Vector2D MINUS_Y = new UnitVector(0, -1);
 
     // CHECKSTYLE: stop ConstantName
     /** A vector with all coordinates set to NaN. */
@@ -66,14 +66,6 @@ public final class Vector2D extends Cartesian2D implements EuclideanVector<Point
         super(x, y);
     }
 
-    /** Get the vector coordinates as a dimension 2 array.
-     * @return vector coordinates
-     */
-    @Override
-    public double[] toArray() {
-        return new double[] { getX(), getY() };
-    }
-
     /** {@inheritDoc} */
     @Override
     public Point2D asPoint() {
@@ -172,7 +164,7 @@ public final class Vector2D extends Cartesian2D implements EuclideanVector<Point
     /** {@inheritDoc} */
     @Override
     public Vector2D normalize() {
-        return scalarMultiply(1.0 / getFiniteNonZeroNorm());
+        return normalize(getX(), getY());
     }
 
     /** {@inheritDoc} */
@@ -395,6 +387,19 @@ public final class Vector2D extends Cartesian2D implements EuclideanVector<Point
         return PolarCoordinates.toCartesian(radius, azimuth, Vector2D::new);
     }
 
+    /** Returns a normalized vector derived from the given values.
+     * @param x abscissa (first coordinate value)
+     * @param y ordinate (second coordinate value)
+     * @return normalized vector instance
+     * @throws IllegalNormException if the norm of the given values is zero, NaN, or infinite
+     */
+    public static Vector2D normalize(final double x, final double y) {
+        final double norm = Vectors.ensureFiniteNonZeroNorm(Vectors.norm(x, y));
+        final double invNorm = 1.0 / norm;
+
+        return new UnitVector(x * invNorm, y * invNorm);
+    }
+
     /** Parses the given string and returns a new vector instance. The expected string
      * format is the same as that returned by {@link #toString()}.
      * @param str the string to parse
@@ -488,4 +493,34 @@ public final class Vector2D extends Cartesian2D implements EuclideanVector<Point
                 LinearCombination.value(a1, c1.getX(), a2, c2.getX(), a3, c3.getX(), a4, c4.getX()),
                 LinearCombination.value(a1, c1.getY(), a2, c2.getY(), a3, c3.getY(), a4, c4.getY()));
     }
+
+    /** Private class used to represent unit vectors. This allows optimizations to be performed for certain
+     * operations.
+     */
+    private static final class UnitVector extends Vector2D {
+
+        /** Serializable version identifier */
+        private static final long serialVersionUID = 20180903L;
+
+        /** Simple constructor. Callers are responsible for ensuring that the given
+         * values represent a normalized vector.
+         * @param x abscissa (first coordinate value)
+         * @param y abscissa (second coordinate value)
+         */
+        private UnitVector(final double x, final double y) {
+            super(x, y);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Vector2D normalize() {
+            return this;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Vector2D withMagnitude(final double mag) {
+            return scalarMultiply(mag);
+        }
+    }
 }
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Point1DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Point1DTest.java
index 25b2fe6..1b40bf2 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Point1DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Point1DTest.java
@@ -19,6 +19,8 @@ package org.apache.commons.geometry.euclidean.oned;
 
 import java.util.regex.Pattern;
 
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.exception.IllegalNormException;
 import org.apache.commons.numbers.core.Precision;
 import org.junit.Assert;
 import org.junit.Test;
@@ -118,6 +120,39 @@ public class Point1DTest {
     }
 
     @Test
+    public void testDirectionTo() {
+        // act/assert
+        Point1D p1 = Point1D.of(1);
+        Point1D p2 = Point1D.of(5);
+        Point1D p3 = Point1D.of(-2);
+
+        // act/assert
+        checkVector(p1.directionTo(p2), 1);
+        checkVector(p2.directionTo(p1), -1);
+
+        checkVector(p1.directionTo(p3), -1);
+        checkVector(p3.directionTo(p1), 1);
+    }
+
+    @Test
+    public void testDirectionTo_illegalNorm() {
+        // arrange
+        Point1D p = Point1D.of(2);
+
+        // act/assert
+        GeometryTestUtils.assertThrows(() -> Point1D.ZERO.directionTo(Point1D.ZERO),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> p.directionTo(p),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> p.directionTo(Point1D.NaN),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> Point1D.NEGATIVE_INFINITY.directionTo(p),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> p.directionTo(Point1D.POSITIVE_INFINITY),
+                IllegalNormException.class);
+    }
+
+    @Test
     public void testLerp() {
         // arrange
         Point1D p1 = Point1D.of(1);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Vector1DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Vector1DTest.java
index ee817cc..3c7bd50 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Vector1DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Vector1DTest.java
@@ -41,6 +41,22 @@ public class Vector1DTest {
     }
 
     @Test
+    public void testConstants_normalize() {
+        // act/assert
+        GeometryTestUtils.assertThrows(() -> Vector1D.ZERO.normalize(),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> Vector1D.NaN.normalize(),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> Vector1D.POSITIVE_INFINITY.normalize(),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> Vector1D.NEGATIVE_INFINITY.normalize(),
+                IllegalNormException.class);
+
+        Assert.assertSame(Vector1D.ONE.normalize(), Vector1D.ONE);
+        Assert.assertSame(Vector1D.MINUS_ONE.normalize(), Vector1D.MINUS_ONE);
+    }
+
+    @Test
     public void testAsPoint() {
         // act/assert
         checkPoint(Vector1D.of(0.0).asPoint(), 0.0);
@@ -135,6 +151,21 @@ public class Vector1DTest {
     }
 
     @Test
+    public void testWithMagnitude_unitVectors() {
+        // arrange
+        Vector1D v = Vector1D.of(2.0).normalize();
+
+        // act/assert
+        checkVector(Vector1D.ONE.withMagnitude(2.5), 2.5);
+        checkVector(Vector1D.MINUS_ONE.withMagnitude(3.14), -3.14);
+
+        for (double mag = -10.0; mag <= 10.0; ++mag)
+        {
+            Assert.assertEquals(Math.abs(mag), v.withMagnitude(mag).getMagnitude(), TEST_TOLERANCE);
+        }
+    }
+
+    @Test
     public void testAdd() {
         // arrange
         Vector1D v1 = Vector1D.of(1);
@@ -208,17 +239,27 @@ public class Vector1DTest {
     @Test
     public void testNormalize_illegalNorm() {
         // act/assert
-        GeometryTestUtils.assertThrows(() -> Vector1D.ZERO.normalize(),
+        GeometryTestUtils.assertThrows(() -> Vector1D.of(0.0).normalize(),
                 IllegalNormException.class);
-        GeometryTestUtils.assertThrows(() -> Vector1D.NaN.normalize(),
+        GeometryTestUtils.assertThrows(() -> Vector1D.of(Double.NaN).normalize(),
                 IllegalNormException.class);
-        GeometryTestUtils.assertThrows(() -> Vector1D.POSITIVE_INFINITY.normalize(),
+        GeometryTestUtils.assertThrows(() -> Vector1D.of(Double.POSITIVE_INFINITY).normalize(),
                 IllegalNormException.class);
-        GeometryTestUtils.assertThrows(() -> Vector1D.NEGATIVE_INFINITY.normalize(),
+        GeometryTestUtils.assertThrows(() -> Vector1D.of(Double.NEGATIVE_INFINITY).normalize(),
                 IllegalNormException.class);
     }
 
     @Test
+    public void testNormalize_isIdempotent() {
+        // arrange
+        Vector1D v = Vector1D.of(2).normalize();
+
+        // act/assert
+        Assert.assertSame(v, v.normalize());
+        checkVector(v.normalize(), 1.0);
+    }
+
+    @Test
     public void testNegate() {
         // act/assert
         checkVector(Vector1D.of(0.1).negate(), -0.1);
@@ -541,6 +582,25 @@ public class Vector1DTest {
     }
 
     @Test
+    public void testNormalize_static() {
+        // act/assert
+        checkVector(Vector1D.normalize(2.0), 1);
+        checkVector(Vector1D.normalize(-4.0), -1);
+    }
+
+    @Test
+    public void testNormalize_static_illegalNorm() {
+        GeometryTestUtils.assertThrows(() -> Vector1D.normalize(0.0),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> Vector1D.normalize(Double.NaN),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> Vector1D.normalize(Double.NEGATIVE_INFINITY),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> Vector1D.normalize(Double.POSITIVE_INFINITY),
+                IllegalNormException.class);
+    }
+
+    @Test
     public void testLinearCombination() {
         // act/assert
         checkVector(Vector1D.linearCombination(2, Vector1D.of(3)), 6);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Point3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Point3DTest.java
index 3e312dd..e1cd149 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Point3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Point3DTest.java
@@ -19,6 +19,8 @@ package org.apache.commons.geometry.euclidean.threed;
 import java.util.regex.Pattern;
 
 import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.exception.IllegalNormException;
 import org.apache.commons.numbers.core.Precision;
 import org.junit.Assert;
 import org.junit.Test;
@@ -103,6 +105,41 @@ public class Point3DTest {
     }
 
     @Test
+    public void testDirectionTo() {
+        // act/assert
+        double invSqrt3 = 1.0 / Math.sqrt(3);
+
+        Point3D p1 = Point3D.of(1, 1, 1);
+        Point3D p2 = Point3D.of(1, 5, 1);
+        Point3D p3 = Point3D.of(-2, -2, -2);
+
+        // act/assert
+        checkVector(p1.directionTo(p2), 0, 1, 0);
+        checkVector(p2.directionTo(p1), 0, -1, 0);
+
+        checkVector(p1.directionTo(p3), -invSqrt3, -invSqrt3, -invSqrt3);
+        checkVector(p3.directionTo(p1), invSqrt3, invSqrt3, invSqrt3);
+    }
+
+    @Test
+    public void testDirectionTo_illegalNorm() {
+        // arrange
+        Point3D p = Point3D.of(1, 2, 3);
+
+        // act/assert
+        GeometryTestUtils.assertThrows(() -> Point3D.ZERO.directionTo(Point3D.ZERO),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> p.directionTo(p),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> p.directionTo(Point3D.NaN),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> Point3D.NEGATIVE_INFINITY.directionTo(p),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> p.directionTo(Point3D.POSITIVE_INFINITY),
+                IllegalNormException.class);
+    }
+
+    @Test
     public void testLerp() {
         // arrange
         Point3D p1 = Point3D.of(1, -5, 2);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Vector3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Vector3DTest.java
index 48c4524..8d6fe9e 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Vector3DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Vector3DTest.java
@@ -53,8 +53,17 @@ public class Vector3DTest {
     }
 
     @Test
-    public void testNonZeroConstants_areUnitVectorInstances() {
+    public void testConstants_normalize() {
         // act/assert
+        GeometryTestUtils.assertThrows(() -> Vector3D.ZERO.normalize(),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> Vector3D.NaN.normalize(),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> Vector3D.POSITIVE_INFINITY.normalize(),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> Vector3D.NEGATIVE_INFINITY.normalize(),
+                IllegalNormException.class);
+
         Assert.assertSame(Vector3D.PLUS_X.normalize(), Vector3D.PLUS_X);
         Assert.assertSame(Vector3D.MINUS_X.normalize(), Vector3D.MINUS_X);
 
@@ -392,7 +401,9 @@ public class Vector3DTest {
                 IllegalNormException.class);
         GeometryTestUtils.assertThrows(() -> Vector3D.PLUS_X.orthogonal(Vector3D.MINUS_X),
                 IllegalNormException.class);
-        GeometryTestUtils.assertThrows(() -> Vector3D.of(1.0, 1.0, 1.0).orthogonal(Vector3D.of(-2.0, -2.0, -2.0)),
+        GeometryTestUtils.assertThrows(() -> Vector3D.of(1.0, 1.0, 1.0).orthogonal(Vector3D.of(2.0, 2.0, 2.0)),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> Vector3D.of(-1.01, -1.01, -1.01).orthogonal(Vector3D.of(20.1, 20.1, 20.1)),
                 IllegalNormException.class);
     }
 
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Point2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Point2DTest.java
index d5903de..dcf89d9 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Point2DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Point2DTest.java
@@ -19,6 +19,8 @@ package org.apache.commons.geometry.euclidean.twod;
 import java.util.regex.Pattern;
 
 import org.apache.commons.geometry.core.Geometry;
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.exception.IllegalNormException;
 import org.apache.commons.numbers.core.Precision;
 import org.junit.Assert;
 import org.junit.Test;
@@ -94,6 +96,41 @@ public class Point2DTest {
     }
 
     @Test
+    public void testDirectionTo() {
+        // act/assert
+        double invSqrt2 = 1.0 / Math.sqrt(2);
+
+        Point2D p1 = Point2D.of(1, 1);
+        Point2D p2 = Point2D.of(1, 5);
+        Point2D p3 = Point2D.of(-2, -2);
+
+        // act/assert
+        checkVector(p1.directionTo(p2), 0, 1);
+        checkVector(p2.directionTo(p1), 0, -1);
+
+        checkVector(p1.directionTo(p3), -invSqrt2, -invSqrt2);
+        checkVector(p3.directionTo(p1), invSqrt2, invSqrt2);
+    }
+
+    @Test
+    public void testDirectionTo_illegalNorm() {
+        // arrange
+        Point2D p = Point2D.of(1, 2);
+
+        // act/assert
+        GeometryTestUtils.assertThrows(() -> Point2D.ZERO.directionTo(Point2D.ZERO),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> p.directionTo(p),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> p.directionTo(Point2D.NaN),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> Point2D.NEGATIVE_INFINITY.directionTo(p),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> p.directionTo(Point2D.POSITIVE_INFINITY),
+                IllegalNormException.class);
+    }
+
+    @Test
     public void testLerp() {
         // arrange
         Point2D p1 = Point2D.of(1, -5);
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Vector2DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Vector2DTest.java
index 791f1c6..0beb633 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Vector2DTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/twod/Vector2DTest.java
@@ -44,17 +44,22 @@ public class Vector2DTest {
     }
 
     @Test
-    public void testToArray() {
-        // arrange
-        Vector2D v = Vector2D.of(1, 2);
+    public void testConstants_normalize() {
+        // act/assert
+        GeometryTestUtils.assertThrows(() -> Vector2D.ZERO.normalize(),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> Vector2D.NaN.normalize(),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> Vector2D.POSITIVE_INFINITY.normalize(),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> Vector2D.NEGATIVE_INFINITY.normalize(),
+                IllegalNormException.class);
 
-        // act
-        double[] arr = v.toArray();
+        Assert.assertSame(Vector2D.PLUS_X.normalize(), Vector2D.PLUS_X);
+        Assert.assertSame(Vector2D.MINUS_X.normalize(), Vector2D.MINUS_X);
 
-        // assert
-        Assert.assertEquals(2, arr.length);
-        Assert.assertEquals(1, arr[0], EPS);
-        Assert.assertEquals(2, arr[1], EPS);
+        Assert.assertSame(Vector2D.PLUS_Y.normalize(), Vector2D.PLUS_Y);
+        Assert.assertSame(Vector2D.MINUS_Y.normalize(), Vector2D.MINUS_Y);
     }
 
     @Test
@@ -175,6 +180,22 @@ public class Vector2DTest {
     }
 
     @Test
+    public void testWithMagnitude_unitVectors() {
+        // arrange
+        double eps = 1e-14;
+        Vector2D v = Vector2D.of(2.0, -3.0).normalize();
+
+        // act/assert
+        checkVector(Vector2D.PLUS_X.withMagnitude(2.5), 2.5, 0.0);
+        checkVector(Vector2D.MINUS_Y.withMagnitude(3.14), 0.0, -3.14);
+
+        for (double mag = -10.0; mag <= 10.0; ++mag)
+        {
+            Assert.assertEquals(Math.abs(mag), v.withMagnitude(mag).getMagnitude(), eps);
+        }
+    }
+
+    @Test
     public void testAdd() {
         // arrange
         Vector2D v1 = Vector2D.of(-1, 2);
@@ -249,7 +270,7 @@ public class Vector2DTest {
         checkVector(Vector2D.of(-100, 0).normalize(), -1, 0);
         checkVector(Vector2D.of(0, 100).normalize(), 0, 1);
         checkVector(Vector2D.of(0, -100).normalize(), 0, -1);
-        checkVector(Vector2D.of(-1, 2).normalize(), -1.0/Math.sqrt(5), 2.0/Math.sqrt(5));
+        checkVector(Vector2D.of(-1, 2).normalize(), -1.0 / Math.sqrt(5), 2.0 / Math.sqrt(5));
     }
 
     @Test
@@ -266,6 +287,17 @@ public class Vector2DTest {
     }
 
     @Test
+    public void testNormalize_isIdempotent() {
+        // arrange
+        double invSqrt2 = 1.0 / Math.sqrt(2);
+        Vector2D v = Vector2D.of(2, 2).normalize();
+
+        // act/assert
+        Assert.assertSame(v, v.normalize());
+        checkVector(v.normalize(), invSqrt2, invSqrt2);
+    }
+
+    @Test
     public void testNegate() {
         // act/assert
         checkVector(Vector2D.of(1, 2).negate(), -1, -2);
@@ -703,6 +735,28 @@ public class Vector2DTest {
     }
 
     @Test
+    public void testNormalize_static() {
+        // arrange
+        double invSqrt2 = 1.0 / Math.sqrt(2.0);
+
+        // act/assert
+        checkVector(Vector2D.normalize(2.0, -2.0), invSqrt2, -invSqrt2);
+        checkVector(Vector2D.normalize(-4.0, 4.0), -invSqrt2, invSqrt2);
+    }
+
+    @Test
+    public void testNormalize_static_illegalNorm() {
+        GeometryTestUtils.assertThrows(() -> Vector2D.normalize(0.0, 0.0),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> Vector2D.normalize(Double.NaN, 1.0),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> Vector2D.normalize(1.0, Double.NEGATIVE_INFINITY),
+                IllegalNormException.class);
+        GeometryTestUtils.assertThrows(() -> Vector2D.normalize(1.0, Double.POSITIVE_INFINITY),
+                IllegalNormException.class);
+    }
+
+    @Test
     public void testLinearCombination1() {
         // arrange
         Vector2D p1 = Vector2D.of(1, 2);