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 fb028f007055c035c3ac66f557eac871b940e17f Author: Matt Juntunen <matt.juntu...@hotmail.com> AuthorDate: Tue Sep 4 23:49:44 2018 -0400 GEOMETRY-10: adding Vector3D private class for normalization optimizations; adding Vector3D#orthogonal(Vector3D) method --- .../commons/geometry/euclidean/threed/Point3D.java | 7 ++ .../geometry/euclidean/threed/Vector3D.java | 93 +++++++++++++++++---- .../geometry/euclidean/threed/Vector3DTest.java | 96 +++++++++++++++++++++- 3 files changed, 178 insertions(+), 18 deletions(-) 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 52c574b..cfe0af9 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,13 @@ public final class Point3D extends Cartesian3D implements EuclideanPoint<Point3D return p.subtract(this); } + public Vector3D directionTo(Point3D p) { + return Vector3D.normalize( + p.getX() - getX(), + p.getY() - getY(), + p.getZ() - getZ()); + } + /** {@inheritDoc} */ @Override public Point3D lerp(Point3D p, double t) { 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 2a1a5f7..61c73d6 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 @@ -16,6 +16,7 @@ */ package org.apache.commons.geometry.euclidean.threed; +import org.apache.commons.geometry.core.internal.DoubleFunction3N; import org.apache.commons.geometry.core.internal.SimpleTupleFormat; import org.apache.commons.geometry.core.util.Vectors; import org.apache.commons.geometry.euclidean.EuclideanVector; @@ -25,28 +26,28 @@ import org.apache.commons.numbers.arrays.LinearCombination; /** This class represents a vector in three-dimensional Euclidean space. * Instances of this class are guaranteed to be immutable. */ -public final class Vector3D extends Cartesian3D implements EuclideanVector<Point3D, Vector3D> { +public class Vector3D extends Cartesian3D implements EuclideanVector<Point3D, Vector3D> { /** Zero (null) vector (coordinates: 0, 0, 0). */ public static final Vector3D ZERO = new Vector3D(0, 0, 0); /** First canonical vector (coordinates: 1, 0, 0). */ - public static final Vector3D PLUS_X = new Vector3D(1, 0, 0); + public static final Vector3D PLUS_X = new UnitVector(1, 0, 0); /** Opposite of the first canonical vector (coordinates: -1, 0, 0). */ - public static final Vector3D MINUS_X = new Vector3D(-1, 0, 0); + public static final Vector3D MINUS_X = new UnitVector(-1, 0, 0); /** Second canonical vector (coordinates: 0, 1, 0). */ - public static final Vector3D PLUS_Y = new Vector3D(0, 1, 0); + public static final Vector3D PLUS_Y = new UnitVector(0, 1, 0); /** Opposite of the second canonical vector (coordinates: 0, -1, 0). */ - public static final Vector3D MINUS_Y = new Vector3D(0, -1, 0); + public static final Vector3D MINUS_Y = new UnitVector(0, -1, 0); /** Third canonical vector (coordinates: 0, 0, 1). */ - public static final Vector3D PLUS_Z = new Vector3D(0, 0, 1); + public static final Vector3D PLUS_Z = new UnitVector(0, 0, 1); /** Opposite of the third canonical vector (coordinates: 0, 0, -1). */ - public static final Vector3D MINUS_Z = new Vector3D(0, 0, -1); + public static final Vector3D MINUS_Z = new UnitVector(0, 0, -1); // CHECKSTYLE: stop ConstantName /** A vector with all coordinates set to NaN. */ @@ -61,8 +62,8 @@ public final class Vector3D extends Cartesian3D implements EuclideanVector<Point public static final Vector3D NEGATIVE_INFINITY = new Vector3D(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY); - /** Serializable UID */ - private static final long serialVersionUID = 20180710L; + /** Serializable version identifier */ + private static final long serialVersionUID = 20180903L; /** Simple constructor. * Build a vector from its coordinates @@ -189,7 +190,7 @@ public final class Vector3D extends Cartesian3D implements EuclideanVector<Point /** {@inheritDoc} */ @Override public Vector3D normalize() throws IllegalStateException { - return scalarMultiply(1.0 / getNonZeroNorm()); + return normalize(getX(), getY(), getZ()); } /** Get a vector orthogonal to the instance. @@ -225,6 +226,19 @@ public final class Vector3D extends Cartesian3D implements EuclideanVector<Point return new Vector3D(inverse * y, -inverse * x, 0); } + /** Returns a unit vector orthogonal to the current vector and pointing in the direction + * of {@code dir}. This method is equivalent to calling {@code dir.reject(vec).normalize()} + * except that no intermediate vector object is produced. + * @param dir the direction to use for generating the orthogonal vector + * @return unit vector orthogonal to the current vector and pointing in the direction of + * {@code dir} that does not lie along the current vector + * @throws IllegalStateException if the norm of the current vector is zero or the given + * vector is collinear with this vector. + */ + public Vector3D orthogonal(Vector3D dir) throws IllegalStateException { + return dir.getComponent(this, true, Vector3D::normalize); + } + /** {@inheritDoc} * <p>This method computes the angular separation between two * vectors using the dot product for well separated vectors and the @@ -323,13 +337,13 @@ public final class Vector3D extends Cartesian3D implements EuclideanVector<Point /** {@inheritDoc} */ @Override public Vector3D project(Vector3D base) throws IllegalStateException { - return getComponent(base, false); + return getComponent(base, false, Vector3D::new); } /** {@inheritDoc} */ @Override public Vector3D reject(Vector3D base) throws IllegalStateException { - return getComponent(base, true); + return getComponent(base, true, Vector3D::new); } /** @@ -402,11 +416,12 @@ public final class Vector3D extends Cartesian3D implements EuclideanVector<Point * @param reject If true, the rejection of this instance from {@code base} is * returned. If false, the projection of this instance onto {@code base} * is returned. + * @param factory factory function used to build the final vector * @return The projection or rejection of this instance relative to {@code base}, * depending on the value of {@code reject}. * @throws IllegalStateException if {@code base} has a zero norm */ - private Vector3D getComponent(Vector3D base, boolean reject) throws IllegalStateException { + private Vector3D getComponent(Vector3D base, boolean reject, DoubleFunction3N<Vector3D> factory) throws IllegalStateException { final double aDotB = dotProduct(base); final double baseMagSq = base.getNormSq(); @@ -421,10 +436,10 @@ public final class Vector3D extends Cartesian3D implements EuclideanVector<Point final double projZ = scale * base.getZ(); if (reject) { - return new Vector3D(getX() - projX, getY() - projY, getZ() - projZ); + return factory.apply(getX() - projX, getY() - projY, getZ() - projZ); } - return new Vector3D(projX, projY, projZ); + return factory.apply(projX, projY, projZ); } /** Computes the dot product between to vectors. This method simply @@ -515,6 +530,23 @@ public final class Vector3D extends Cartesian3D implements EuclideanVector<Point return SphericalCoordinates.toCartesian(radius, azimuth, polar, Vector3D::new); } + /** Returns a normalized vector derived from the given values. + * @param x abscissa (first coordinate value) + * @param y abscissa (second coordinate value) + * @param z height (third coordinate value) + * @return normalized vector instance + * @throws IllegalStateException if the norm of the given values is zero + */ + public static Vector3D normalize(final double x, final double y, final double z) throws IllegalStateException { + final double norm = Vectors.norm(x, y, z); + if (norm == 0.0) { + throw new ZeroNormException(); + } + final double invNorm = 1.0 / norm; + + return new UnitVector(x * invNorm, y * invNorm, z * 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 @@ -623,4 +655,35 @@ public final class Vector3D extends Cartesian3D implements EuclideanVector<Point LinearCombination.value(a1, c1.getY(), a2, c2.getY(), a3, c3.getY(), a4, c4.getY()), LinearCombination.value(a1, c1.getZ(), a2, c2.getZ(), a3, c3.getZ(), a4, c4.getZ())); } + + /** Private class used to represent unit vectors. This allows optimizations to be performed for certain + * operations. + */ + private static final class UnitVector extends Vector3D { + + /** 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) + * @param z height (third coordinate value) + */ + private UnitVector(final double x, final double y, final double z) { + super(x, y, z); + } + + /** {@inheritDoc} */ + @Override + public Vector3D normalize() { + return this; + } + + /** {@inheritDoc} */ + @Override + public Vector3D withMagnitude(final double mag) { + return scalarMultiply(mag); + } + } } 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 4880438..e0de1c5 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 @@ -51,6 +51,19 @@ public class Vector3DTest { } @Test + public void testNonZeroConstants_areUnitVectorInstances() { + // act/assert + Assert.assertSame(Vector3D.PLUS_X.normalize(), Vector3D.PLUS_X); + Assert.assertSame(Vector3D.MINUS_X.normalize(), Vector3D.MINUS_X); + + Assert.assertSame(Vector3D.PLUS_Y.normalize(), Vector3D.PLUS_Y); + Assert.assertSame(Vector3D.MINUS_Y.normalize(), Vector3D.MINUS_Y); + + Assert.assertSame(Vector3D.PLUS_Z.normalize(), Vector3D.PLUS_Z); + Assert.assertSame(Vector3D.MINUS_Z.normalize(), Vector3D.MINUS_Z); + } + + @Test public void testZero() { // act Vector3D zero = Vector3D.of(1, 2, 3).getZero(); @@ -131,6 +144,8 @@ public class Vector3DTest { double normZ = z / len; // act/assert + checkVector(Vector3D.of(x, y, z).withMagnitude(0.0), 0.0, 0.0, 0.0); + checkVector(Vector3D.of(x, y, z).withMagnitude(1.0), normX, normY, normZ); checkVector(Vector3D.of(x, y, -z).withMagnitude(1.0), normX, normY, -normZ); checkVector(Vector3D.of(x, -y, z).withMagnitude(1.0), normX, -normY, normZ); @@ -142,6 +157,14 @@ public class Vector3DTest { checkVector(Vector3D.of(x, y, z).withMagnitude(0.5), 0.5 * normX, 0.5 * normY, 0.5 * normZ); checkVector(Vector3D.of(x, y, z).withMagnitude(3), 3 * normX, 3 * normY, 3 * normZ); + + checkVector(Vector3D.of(x, y, z).withMagnitude(-0.5), -0.5 * normX, -0.5 * normY, -0.5 * normZ); + checkVector(Vector3D.of(x, y, z).withMagnitude(-3), -3 * normX, -3 * normY, -3 * normZ); + + for (double mag = -10.0; mag <= 10.0; ++mag) + { + Assert.assertEquals(Math.abs(mag), Vector3D.of(x, y, z).withMagnitude(mag).getMagnitude(), EPS); + } } @Test(expected = IllegalStateException.class) @@ -151,6 +174,22 @@ public class Vector3DTest { } @Test + public void testWithMagnitude_unitVectors() { + // arrange + Vector3D v = Vector3D.of(2.0, -3.0, 4.0).normalize(); + + // act/assert + checkVector(Vector3D.PLUS_X.withMagnitude(2.5), 2.5, 0.0, 0.0); + checkVector(Vector3D.MINUS_Y.withMagnitude(3.14), 0.0, -3.14, 0.0); + checkVector(Vector3D.PLUS_Z.withMagnitude(-1.1), 0.0, 0.0, -1.1); + + for (double mag = -10.0; mag <= 10.0; ++mag) + { + Assert.assertEquals(Math.abs(mag), v.withMagnitude(mag).getMagnitude(), EPS); + } + } + + @Test public void testAdd() { // arrange Vector3D v1 = Vector3D.of(1, 2, 3); @@ -247,7 +286,7 @@ public class Vector3DTest { checkVector(Vector3D.of(2, 2, 2).normalize(), invSqrt3, invSqrt3, invSqrt3); checkVector(Vector3D.of(-2, -2, -2).normalize(), -invSqrt3, -invSqrt3, -invSqrt3); - Assert.assertEquals(1.0, Vector3D.of(5, -4, 2).normalize().getNorm(), 1.0e-12); + Assert.assertEquals(1.0, Vector3D.of(5, -4, 2).normalize().getNorm(), EPS); } @Test(expected = IllegalStateException.class) @@ -257,6 +296,17 @@ public class Vector3DTest { } @Test + public void testNormalize_isIdempotent() { + // arrange + double invSqrt3 = 1 / Math.sqrt(3); + Vector3D v = Vector3D.of(2, 2, 2).normalize(); + + // act/assert + Assert.assertSame(v, v.normalize()); + checkVector(v.normalize(), invSqrt3, invSqrt3, invSqrt3); + } + + @Test public void testOrthogonal() { // arrange Vector3D v1 = Vector3D.of(0.1, 2.5, 1.3); @@ -278,6 +328,31 @@ public class Vector3DTest { } @Test + public void testOrthogonal_givenDirection() { + // arrange + double invSqrt2 = 1.0 / Math.sqrt(2.0); + + // act/assert + checkVector(Vector3D.PLUS_X.orthogonal(Vector3D.of(-1.0, 0.1, 0.0)), 0.0, 1.0, 0.0); + checkVector(Vector3D.PLUS_Y.orthogonal(Vector3D.of(2.0, 2.0, 2.0)), invSqrt2, 0.0, invSqrt2); + checkVector(Vector3D.PLUS_Z.orthogonal(Vector3D.of(3.0, 3.0, -3.0)), invSqrt2, invSqrt2, 0.0); + + checkVector(Vector3D.of(invSqrt2, invSqrt2, 0.0).orthogonal(Vector3D.of(1.0, 1.0, 0.2)), 0.0, 0.0, 1.0); + } + + @Test(expected = IllegalStateException.class) + public void testOrthogonal_givenDirection_zeroNorm() { + // act/assert + Vector3D.ZERO.orthogonal(Vector3D.PLUS_X); + } + + @Test(expected = IllegalStateException.class) + public void testOrthogonal_givenDirection_directionIsCollinear() { + // act/assert + Vector3D.PLUS_X.orthogonal(Vector3D.of(-2.0, 0.0, 0.0)); + } + + @Test public void testAngle() { // arrange double tolerance = 1e-10; @@ -956,7 +1031,7 @@ public class Vector3DTest { } @Test - public void testOf_arrayArg() { + public void testOfArray() { // act/assert checkVector(Vector3D.ofArray(new double[] { 1, 2, 3 }), 1, 2, 3); checkVector(Vector3D.ofArray(new double[] { -1, -2, -3 }), -1, -2, -3); @@ -967,7 +1042,7 @@ public class Vector3DTest { } @Test(expected = IllegalArgumentException.class) - public void testOf_arrayArg_invalidDimensions() { + public void testOfArray_invalidDimensions() { // act/assert Vector3D.ofArray(new double[] { 0.0, 0.0 }); } @@ -994,6 +1069,21 @@ public class Vector3DTest { } @Test + public void testNormalize_static() { + // arrange + double invSqrt3 = 1.0 / Math.sqrt(3.0); + + // act/assert + checkVector(Vector3D.normalize(2.0, -2.0, 2.0), invSqrt3, -invSqrt3, invSqrt3); + checkVector(Vector3D.normalize(-4.0, 4.0, -4.0), -invSqrt3, invSqrt3, -invSqrt3); + } + + @Test(expected = IllegalStateException.class) + public void testNormalize_static_zeroNorm() { + Vector3D.normalize(0.0, 0.0, 0.0); + } + + @Test public void testLinearCombination1() { // arrange Vector3D p1 = Vector3D.of(1, 2, 3);