You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by ma...@apache.org on 2021/04/09 01:10:20 UTC

[commons-geometry] branch master updated: GEOMETRY-119: adding Vectors.normalizeOrNull() method; scaling vectors if needed during normalization

This is an automated email from the ASF dual-hosted git repository.

mattjuntunen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-geometry.git


The following commit(s) were added to refs/heads/master by this push:
     new ea68dfb  GEOMETRY-119: adding Vectors.normalizeOrNull() method; scaling vectors if needed during normalization
ea68dfb is described below

commit ea68dfb0093aace23d856970b32e2b7f0941e425
Author: Matt Juntunen <ma...@apache.org>
AuthorDate: Wed Apr 7 08:03:35 2021 -0400

    GEOMETRY-119: adding Vectors.normalizeOrNull() method; scaling vectors if needed during normalization
---
 .../org/apache/commons/geometry/core/Vector.java   |  12 +-
 .../geometry/euclidean/internal/Vectors.java       |  18 +--
 .../commons/geometry/euclidean/oned/Vector1D.java  |  77 ++++++++----
 .../geometry/euclidean/threed/Vector3D.java        | 139 ++++++++++++++++-----
 .../threed/rotation/QuaternionRotation.java        |   7 +-
 .../commons/geometry/euclidean/twod/Vector2D.java  | 125 ++++++++++++++----
 .../geometry/euclidean/internal/VectorsTest.java   |  15 ---
 .../geometry/euclidean/oned/Vector1DTest.java      |  52 +++++++-
 .../geometry/euclidean/threed/Vector3DTest.java    |  75 ++++++++++-
 .../geometry/euclidean/twod/Vector2DTest.java      |  68 +++++++++-
 .../examples/jmh/euclidean/VectorPerformance.java  |  43 +++++--
 .../io/euclidean/threed/obj/PolygonObjParser.java  |   5 +-
 .../geometry/io/euclidean/threed/stl/StlUtils.java |   5 +-
 13 files changed, 504 insertions(+), 137 deletions(-)

diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Vector.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Vector.java
index 65a009b..dbd3e49 100644
--- a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Vector.java
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Vector.java
@@ -93,11 +93,21 @@ public interface Vector<V extends Vector<V>> extends Spatial {
 
     /** Get a normalized vector aligned with the instance. The returned
      * vector has a magnitude of 1.
-     * @return a new normalized vector
+     * @return normalized vector
      * @throws IllegalArgumentException if the norm is zero, NaN, or infinite
+     * @see #normalizeOrNull()
      */
     V normalize();
 
+    /** Attempt to compute a normalized vector aligned with the instance, returning null if
+     * such a vector cannot be computed. This method is equivalent to {@link #normalize()}
+     * but returns null instead of throwing an exception on failure.
+     * @return normalized vector or null if such a vector cannot be computed, i.e. if the
+     *      norm is zero, NaN, or infinite
+     * @see #normalize()
+     */
+    V normalizeOrNull();
+
     /** Multiply the instance by a scalar.
      * @param a scalar
      * @return a new vector
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/internal/Vectors.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/internal/Vectors.java
index 4f33535..aac199c 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/internal/Vectors.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/internal/Vectors.java
@@ -46,7 +46,7 @@ public final class Vectors {
      */
     public static double checkedNorm(final double norm) {
         if (!isRealNonZero(norm)) {
-            throw new IllegalArgumentException("Illegal norm: " + norm);
+            throw illegalNorm(norm);
         }
 
         return norm;
@@ -63,18 +63,12 @@ public final class Vectors {
         return checkedNorm(vec.norm());
     }
 
-    /** Attempt to normalize the given vector, returning null if the vector cannot be normalized
-     * due to the norm being NaN, infinite, or null.
-     * @param <V> Vector implementation type
-     * @param vec the vector to attempt to normalize
-     * @return the normalized vector if successful, otherwise null
+    /** Return an exception indicating an illegal norm value.
+     * @param norm illegal norm value
+     * @return exception indicating an illegal norm value
      */
-    public static <V extends Vector<V>> V tryNormalize(final V vec) {
-        final double norm = vec.norm();
-        if (isRealNonZero(norm)) {
-            return vec.normalize();
-        }
-        return null;
+    public static IllegalArgumentException illegalNorm(final double norm) {
+        return new IllegalArgumentException("Illegal norm: " + norm);
     }
 
     /** Get the L<sub>2</sub> norm (commonly known as the Euclidean norm) for the vector
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 ed968fe..009762c 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
@@ -188,6 +188,12 @@ public class Vector1D extends EuclideanVector<Vector1D> {
 
     /** {@inheritDoc} */
     @Override
+    public Unit normalizeOrNull() {
+        return Unit.tryCreateNormalized(x, false);
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public Vector1D multiply(final double a) {
         return new Vector1D(a * x);
     }
@@ -413,31 +419,6 @@ public class Vector1D extends EuclideanVector<Vector1D> {
             super(x);
         }
 
-        /**
-         * Creates a normalized vector.
-         *
-         * @param x Vector coordinate.
-         * @return a vector whose norm is 1.
-         * @throws IllegalArgumentException if the norm of the given value is zero, NaN, or infinite
-         */
-        public static Unit from(final double x) {
-            Vectors.checkedNorm(Vectors.norm(x));
-            return x > 0 ? PLUS : MINUS;
-        }
-
-        /**
-         * Creates a normalized vector.
-         *
-         * @param v Vector.
-         * @return a vector whose norm is 1.
-         * @throws IllegalArgumentException if the norm of the given value is zero, NaN, or infinite
-         */
-        public static Unit from(final Vector1D v) {
-            return v instanceof Unit ?
-                (Unit) v :
-                from(v.getX());
-        }
-
         /** {@inheritDoc} */
         @Override
         public double norm() {
@@ -458,6 +439,12 @@ public class Vector1D extends EuclideanVector<Vector1D> {
 
         /** {@inheritDoc} */
         @Override
+        public Unit normalizeOrNull() {
+            return this;
+        }
+
+        /** {@inheritDoc} */
+        @Override
         public Vector1D withNorm(final double mag) {
             return multiply(mag);
         }
@@ -467,5 +454,45 @@ public class Vector1D extends EuclideanVector<Vector1D> {
         public Vector1D negate() {
             return this == PLUS ? MINUS : PLUS;
         }
+
+        /** Create a normalized vector.
+         * @param x Vector coordinate.
+         * @return a vector whose norm is 1.
+         * @throws IllegalArgumentException if the norm of the given value is zero, NaN, or infinite
+         */
+        public static Unit from(final double x) {
+            return tryCreateNormalized(x, true);
+        }
+
+        /** Create a normalized vector.
+         * @param v Vector.
+         * @return a vector whose norm is 1.
+         * @throws IllegalArgumentException if the norm of the given value is zero, NaN, or infinite
+         */
+        public static Unit from(final Vector1D v) {
+            return v instanceof Unit ?
+                (Unit) v :
+                from(v.getX());
+        }
+
+        /** Attempt to create a normalized vector from the given coordinate values. If {@code throwOnFailure}
+         * is true, an exception is thrown if a normalized vector cannot be created. Otherwise, null
+         * is returned.
+         * @param x x coordinate
+         * @param throwOnFailure if true, an exception will be thrown if a normalized vector cannot be created
+         * @return normalized vector or null if one cannot be created and {@code throwOnFailure}
+         *      is false
+         * @throws IllegalArgumentException if the computed norm is zero, NaN, or infinite
+         */
+        private static Unit tryCreateNormalized(final double x, final boolean throwOnFailure) {
+            final double norm = Vectors.norm(x);
+
+            if (Vectors.isRealNonZero(norm)) {
+                return x > 0 ? PLUS : MINUS;
+            } else if (throwOnFailure) {
+                throw Vectors.illegalNorm(norm);
+            }
+            return null;
+        }
     }
 }
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 0585789..0cb86c0 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
@@ -248,6 +248,12 @@ public class Vector3D extends MultiDimensionalEuclideanVector<Vector3D> {
 
     /** {@inheritDoc} */
     @Override
+    public Unit normalizeOrNull() {
+        return Unit.tryCreateNormalized(x, y, z, false);
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public Vector3D multiply(final double a) {
         return new Vector3D(a * x, a * y, a * z);
     }
@@ -760,6 +766,21 @@ public class Vector3D extends MultiDimensionalEuclideanVector<Vector3D> {
         /** Negation of unit vector (coordinates: 0, 0, -1). */
         public static final Unit MINUS_Z = new Unit(0d, 0d, -1d);
 
+        /** Maximum coordinate value for computing normalized vectors
+         * with raw, unscaled values.
+         */
+        private static final double UNSCALED_MAX = 0x1.0p+500;
+
+        /** Factor used to scale up coordinate values in order to produce
+         * normalized coordinates without overflow or underflow.
+         */
+        private static final double SCALE_UP_FACTOR = 0x1.0p+600;
+
+        /** Factor used to scale down coordinate values in order to produce
+         * normalized coordinates without overflow or underflow.
+         */
+        private static final double SCALE_DOWN_FACTOR = 0x1.0p-600;
+
         /** Simple constructor. Callers are responsible for ensuring that the given
          * values represent a normalized vector.
          * @param x x coordinate value
@@ -770,35 +791,6 @@ public class Vector3D extends MultiDimensionalEuclideanVector<Vector3D> {
             super(x, y, z);
         }
 
-        /**
-         * Creates a normalized vector.
-         *
-         * @param x Vector coordinate.
-         * @param y Vector coordinate.
-         * @param z Vector coordinate.
-         * @return a vector whose norm is 1.
-         * @throws IllegalArgumentException if the norm of the given value
-         *      is zero, NaN, or infinite
-         */
-        public static Unit from(final double x, final double y, final double z) {
-            final double invNorm = 1 / Vectors.checkedNorm(Vectors.norm(x, y, z));
-            return new Unit(x * invNorm, y * invNorm, z * invNorm);
-        }
-
-        /**
-         * Creates a normalized vector.
-         *
-         * @param v Vector.
-         * @return a vector whose norm is 1.
-         * @throws IllegalArgumentException if the norm of the given
-         *      value is zero, NaN, or infinite
-         */
-        public static Unit from(final Vector3D v) {
-            return v instanceof Unit ?
-                (Unit) v :
-                from(v.getX(), v.getY(), v.getZ());
-        }
-
         /** {@inheritDoc} */
         @Override
         public double norm() {
@@ -819,6 +811,12 @@ public class Vector3D extends MultiDimensionalEuclideanVector<Vector3D> {
 
         /** {@inheritDoc} */
         @Override
+        public Unit normalizeOrNull() {
+            return this;
+        }
+
+        /** {@inheritDoc} */
+        @Override
         public Vector3D withNorm(final double mag) {
             return multiply(mag);
         }
@@ -828,5 +826,88 @@ public class Vector3D extends MultiDimensionalEuclideanVector<Vector3D> {
         public Unit negate() {
             return new Unit(-getX(), -getY(), -getZ());
         }
+
+        /** Create a normalized vector.
+         * @param x Vector coordinate.
+         * @param y Vector coordinate.
+         * @param z Vector coordinate.
+         * @return a vector whose norm is 1.
+         * @throws IllegalArgumentException if the norm of the given value is zero, NaN,
+         *      or infinite
+         */
+        public static Unit from(final double x, final double y, final double z) {
+            return tryCreateNormalized(x, y, z, true);
+        }
+
+        /** Create a normalized vector.
+         * @param v Vector.
+         * @return a vector whose norm is 1.
+         * @throws IllegalArgumentException if the norm of the given value is zero, NaN,
+         *      or infinite
+         */
+        public static Unit from(final Vector3D v) {
+            return v instanceof Unit ?
+                (Unit) v :
+                from(v.getX(), v.getY(), v.getZ());
+        }
+
+        /** Attempt to create a normalized vector from the given coordinate values. If {@code throwOnFailure}
+         * is true, an exception is thrown if a normalized vector cannot be created. Otherwise, null
+         * is returned.
+         * @param x x coordinate
+         * @param y y coordinate
+         * @param z z coordinate
+         * @param throwOnFailure if true, an exception will be thrown if a normalized vector cannot be created
+         * @return normalized vector or null if one cannot be created and {@code throwOnFailure}
+         *      is false
+         * @throws IllegalArgumentException if the computed norm is zero, NaN, or infinite
+         */
+        private static Unit tryCreateNormalized(final double x, final double y, final double z,
+                final boolean throwOnFailure) {
+
+            // Compute the inverse norm directly. If the result is a non-zero real number,
+            // then we can go ahead and construct the unit vector immediately. If not,
+            // we'll do some extra work for edge cases.
+            final double norm = Math.sqrt((x * x) + (y * y) + (z * z));
+            final double normInv = 1.0 / norm;
+            if (Vectors.isRealNonZero(normInv)) {
+                return new Unit(
+                        x * normInv,
+                        y * normInv,
+                        z * normInv);
+            }
+
+            // Direct computation did not work. Try scaled versions of the coordinates
+            // to handle overflow and underflow.
+            final double scaledX;
+            final double scaledY;
+            final double scaledZ;
+
+            final double maxCoord = Math.max(Math.max(Math.abs(x), Math.abs(y)), Math.abs(z));
+            if (maxCoord > UNSCALED_MAX) {
+                scaledX = x * SCALE_DOWN_FACTOR;
+                scaledY = y * SCALE_DOWN_FACTOR;
+                scaledZ = z * SCALE_DOWN_FACTOR;
+            } else {
+                scaledX = x * SCALE_UP_FACTOR;
+                scaledY = y * SCALE_UP_FACTOR;
+                scaledZ = z * SCALE_UP_FACTOR;
+            }
+
+            final double scaledNormInv = 1.0 / Math.sqrt(
+                    (scaledX * scaledX) +
+                    (scaledY * scaledY) +
+                    (scaledZ * scaledZ));
+
+            if (Vectors.isRealNonZero(scaledNormInv)) {
+                return new Unit(
+                        scaledX * scaledNormInv,
+                        scaledY * scaledNormInv,
+                        scaledZ * scaledNormInv);
+            } else if (throwOnFailure) {
+                throw Vectors.illegalNorm(norm);
+            }
+            return null;
+        }
     }
 }
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/QuaternionRotation.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/QuaternionRotation.java
index 62e950a..76f1a2f 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/QuaternionRotation.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/rotation/QuaternionRotation.java
@@ -83,10 +83,11 @@ public final class QuaternionRotation implements Rotation3D {
      */
     @Override
     public Vector3D getAxis() {
-        final Vector3D axis = Vectors.tryNormalize(Vector3D.of(quat.getX(), quat.getY(), quat.getZ()));
+        final Vector3D axis = Vector3D.of(quat.getX(), quat.getY(), quat.getZ())
+                .normalizeOrNull();
         return axis != null ?
-            axis :
-            Vector3D.Unit.PLUS_X;
+                axis :
+                Vector3D.Unit.PLUS_X;
     }
 
     /**
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 84b1807..3a0cf9e 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
@@ -215,6 +215,12 @@ public class Vector2D extends MultiDimensionalEuclideanVector<Vector2D> {
 
     /** {@inheritDoc} */
     @Override
+    public Unit normalizeOrNull() {
+        return Unit.tryCreateNormalized(x, y, false);
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public Vector2D multiply(final double a) {
         return new Vector2D(a * x, a * y);
     }
@@ -686,6 +692,21 @@ public class Vector2D extends MultiDimensionalEuclideanVector<Vector2D> {
         /** Negation of unit vector (coordinates: 0, -1). */
         public static final Unit MINUS_Y = new Unit(0d, -1d);
 
+        /** Maximum coordinate value for computing normalized vectors
+         * with raw, unscaled values.
+         */
+        private static final double UNSCALED_MAX = 0x1.0p+500;
+
+        /** Factor used to scale up coordinate values in order to produce
+         * normalized coordinates without overflow or underflow.
+         */
+        private static final double SCALE_UP_FACTOR = 0x1.0p+600;
+
+        /** Factor used to scale down coordinate values in order to produce
+         * normalized coordinates without overflow or underflow.
+         */
+        private static final double SCALE_DOWN_FACTOR = 0x1.0p-600;
+
         /** Simple constructor. Callers are responsible for ensuring that the given
          * values represent a normalized vector.
          * @param x abscissa (first coordinate value)
@@ -695,32 +716,6 @@ public class Vector2D extends MultiDimensionalEuclideanVector<Vector2D> {
             super(x, y);
         }
 
-        /**
-         * Creates a normalized vector.
-         *
-         * @param x Vector coordinate.
-         * @param y Vector coordinate.
-         * @return a vector whose norm is 1.
-         * @throws IllegalArgumentException if the norm of the given value is zero, NaN, or infinite
-         */
-        public static Unit from(final double x, final double y) {
-            final double invNorm = 1 / Vectors.checkedNorm(Vectors.norm(x, y));
-            return new Unit(x * invNorm, y * invNorm);
-        }
-
-        /**
-         * Creates a normalized vector.
-         *
-         * @param v Vector.
-         * @return a vector whose norm is 1.
-         * @throws IllegalArgumentException if the norm of the given value is zero, NaN, or infinite
-         */
-        public static Unit from(final Vector2D v) {
-            return v instanceof Unit ?
-                (Unit) v :
-                from(v.getX(), v.getY());
-        }
-
         /** {@inheritDoc} */
         @Override
         public double norm() {
@@ -741,6 +736,12 @@ public class Vector2D extends MultiDimensionalEuclideanVector<Vector2D> {
 
         /** {@inheritDoc} */
         @Override
+        public Unit normalizeOrNull() {
+            return this;
+        }
+
+        /** {@inheritDoc} */
+        @Override
         public Vector2D.Unit orthogonal() {
             return new Unit(-getY(), getX());
         }
@@ -756,5 +757,77 @@ public class Vector2D extends MultiDimensionalEuclideanVector<Vector2D> {
         public Unit negate() {
             return new Unit(-getX(), -getY());
         }
+
+        /** Create a normalized vector.
+         * @param x Vector coordinate.
+         * @param y Vector coordinate.
+         * @return a vector whose norm is 1.
+         * @throws IllegalArgumentException if the norm of the given value is zero, NaN,
+         *      or infinite
+         */
+        public static Unit from(final double x, final double y) {
+            return tryCreateNormalized(x, y, true);
+        }
+
+        /** Create a normalized vector.
+         * @param v Vector.
+         * @return a vector whose norm is 1.
+         * @throws IllegalArgumentException if the norm of the given value is zero, NaN,
+         *      or infinite
+         */
+        public static Unit from(final Vector2D v) {
+            return v instanceof Unit ?
+                (Unit) v :
+                from(v.getX(), v.getY());
+        }
+
+        /** Attempt to create a normalized vector from the given coordinate values. If {@code throwOnFailure}
+         * is true, an exception is thrown if a normalized vector cannot be created. Otherwise, null
+         * is returned.
+         * @param x x coordinate
+         * @param y y coordinate
+         * @param throwOnFailure if true, an exception will be thrown if a normalized vector cannot be created
+         * @return normalized vector or null if one cannot be created and {@code throwOnFailure}
+         *      is false
+         * @throws IllegalArgumentException if the computed norm is zero, NaN, or infinite
+         */
+        private static Unit tryCreateNormalized(final double x, final double y, final boolean throwOnFailure) {
+
+            // Compute the inverse norm directly. If the result is a non-zero real number,
+            // then we can go ahead and construct the unit vector immediately. If not,
+            // we'll do some extra work for edge cases.
+            final double norm = Vectors.norm(x, y);
+            final double normInv = 1.0 / norm;
+            if (Vectors.isRealNonZero(normInv)) {
+                return new Unit(
+                        x * normInv,
+                        y * normInv);
+            }
+
+            // Direct computation did not work. Try scaled versions of the coordinates
+            // to handle overflow and underflow.
+            final double scaledX;
+            final double scaledY;
+
+            final double maxCoord = Math.max(Math.abs(x), Math.abs(y));
+            if (maxCoord > UNSCALED_MAX) {
+                scaledX = x * SCALE_DOWN_FACTOR;
+                scaledY = y * SCALE_DOWN_FACTOR;
+            } else {
+                scaledX = x * SCALE_UP_FACTOR;
+                scaledY = y * SCALE_UP_FACTOR;
+            }
+
+            final double scaledNormInv = 1.0 / Vectors.norm(scaledX, scaledY);
+
+            if (Vectors.isRealNonZero(scaledNormInv)) {
+                return new Unit(
+                        scaledX * scaledNormInv,
+                        scaledY * scaledNormInv);
+            } else if (throwOnFailure) {
+                throw Vectors.illegalNorm(norm);
+            }
+            return null;
+        }
     }
 }
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/internal/VectorsTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/internal/VectorsTest.java
index 0109a7d..1191d2d 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/internal/VectorsTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/internal/VectorsTest.java
@@ -17,7 +17,6 @@
 package org.apache.commons.geometry.euclidean.internal;
 
 import org.apache.commons.geometry.core.GeometryTestUtils;
-import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
 import org.apache.commons.geometry.euclidean.oned.Vector1D;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
 import org.junit.jupiter.api.Assertions;
@@ -85,20 +84,6 @@ public class VectorsTest {
     }
 
     @Test
-    public void testTryNormalize() {
-        // act/assert
-        EuclideanTestUtils.assertCoordinatesEqual(Vector3D.Unit.PLUS_X,
-                Vectors.tryNormalize(Vector3D.of(2, 0, 0)), EPS);
-
-        Assertions.assertNull(Vectors.tryNormalize(Vector3D.of(0, 0, 0)));
-        Assertions.assertNull(Vectors.tryNormalize(Vector3D.of(-0, 0, 0)));
-
-        Assertions.assertNull(Vectors.tryNormalize(Vector3D.of(Double.NaN, 1, 1)));
-        Assertions.assertNull(Vectors.tryNormalize(Vector3D.of(1, Double.POSITIVE_INFINITY, 1)));
-        Assertions.assertNull(Vectors.tryNormalize(Vector3D.of(1, 1, Double.NEGATIVE_INFINITY)));
-    }
-
-    @Test
     public void testNorm_oneD() {
         // act/assert
         Assertions.assertEquals(0.0, Vectors.norm(0.0), EPS);
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 b44edfe..bd6f56d 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
@@ -269,15 +269,28 @@ public class Vector1DTest {
         checkVector(Vector1D.of(-1).normalize(), -1);
         checkVector(Vector1D.of(5).normalize(), 1);
         checkVector(Vector1D.of(-5).normalize(), -1);
+
+        checkVector(Vector1D.of(Double.MIN_VALUE).normalize(), 1);
+        checkVector(Vector1D.of(-Double.MIN_VALUE).normalize(), -1);
+
+        checkVector(Vector1D.of(Double.MAX_VALUE).normalize(), 1);
+        checkVector(Vector1D.of(-Double.MAX_VALUE).normalize(), -1);
     }
 
     @Test
     public void testNormalize_illegalNorm() {
+        // arrange
+        final Pattern illegalNorm = Pattern.compile("^Illegal norm: (0\\.0|-?Infinity|NaN)");
+
         // act/assert
-        Assertions.assertThrows(IllegalArgumentException.class, () -> Vector1D.of(0.0).normalize());
-        Assertions.assertThrows(IllegalArgumentException.class, () -> Vector1D.of(Double.NaN).normalize());
-        Assertions.assertThrows(IllegalArgumentException.class, () -> Vector1D.of(Double.POSITIVE_INFINITY).normalize());
-        Assertions.assertThrows(IllegalArgumentException.class, () -> Vector1D.of(Double.NEGATIVE_INFINITY).normalize());
+        GeometryTestUtils.assertThrowsWithMessage(Vector1D.ZERO::normalize,
+                IllegalArgumentException.class, illegalNorm);
+        GeometryTestUtils.assertThrowsWithMessage(Vector1D.NaN::normalize,
+                IllegalArgumentException.class, illegalNorm);
+        GeometryTestUtils.assertThrowsWithMessage(Vector1D.POSITIVE_INFINITY::normalize,
+                IllegalArgumentException.class, illegalNorm);
+        GeometryTestUtils.assertThrowsWithMessage(Vector1D.NEGATIVE_INFINITY::normalize,
+                IllegalArgumentException.class, illegalNorm);
     }
 
     @Test
@@ -291,6 +304,37 @@ public class Vector1DTest {
     }
 
     @Test
+    public void testNormalizeOrNull() {
+        // act/assert
+        checkVector(Vector1D.of(100).normalizeOrNull(), 1);
+        checkVector(Vector1D.of(-100).normalizeOrNull(), -1);
+
+        checkVector(Vector1D.of(2).normalizeOrNull(), 1);
+        checkVector(Vector1D.of(-2).normalizeOrNull(), -1);
+
+        checkVector(Vector1D.of(Double.MIN_VALUE).normalizeOrNull(), 1);
+        checkVector(Vector1D.of(-Double.MIN_VALUE).normalizeOrNull(), -1);
+
+        checkVector(Vector1D.of(Double.MAX_VALUE).normalizeOrNull(), 1);
+        checkVector(Vector1D.of(-Double.MAX_VALUE).normalizeOrNull(), -1);
+
+        Assertions.assertNull(Vector1D.ZERO.normalizeOrNull());
+        Assertions.assertNull(Vector1D.NaN.normalizeOrNull());
+        Assertions.assertNull(Vector1D.POSITIVE_INFINITY.normalizeOrNull());
+        Assertions.assertNull(Vector1D.NEGATIVE_INFINITY.normalizeOrNull());
+    }
+
+    @Test
+    public void testNormalizeOrNull_isIdempotent() {
+        // arrange
+        final Vector1D v = Vector1D.of(2).normalizeOrNull();
+
+        // act/assert
+        Assertions.assertSame(v, v.normalizeOrNull());
+        checkVector(v.normalizeOrNull(), 1.0);
+    }
+
+    @Test
     public void testNegate() {
         // act/assert
         checkVector(Vector1D.of(0.1).negate(), -0.1);
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 daec297..e02460a 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
@@ -34,7 +34,6 @@ import org.apache.commons.rng.simple.RandomSource;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
-
 public class Vector3DTest {
 
     private static final double EPS = 1e-15;
@@ -396,16 +395,40 @@ public class Vector3DTest {
         checkVector(Vector3D.of(2, 2, 2).normalize(), invSqrt3, invSqrt3, invSqrt3);
         checkVector(Vector3D.of(-2, -2, -2).normalize(), -invSqrt3, -invSqrt3, -invSqrt3);
 
+        checkVector(Vector3D.of(Double.MIN_VALUE, 0, 0).normalize(), 1, 0, 0);
+        checkVector(Vector3D.of(0, Double.MIN_VALUE, 0).normalize(), 0, 1, 0);
+        checkVector(Vector3D.of(0, 0, Double.MIN_VALUE).normalize(), 0, 0, 1);
+
+        checkVector(Vector3D.of(-Double.MIN_VALUE, Double.MIN_VALUE, Double.MIN_VALUE).normalize(),
+                -invSqrt3, invSqrt3, invSqrt3);
+
+        checkVector(Vector3D.of(Double.MIN_NORMAL, 0, 0).normalize(), 1, 0, 0);
+        checkVector(Vector3D.of(0, Double.MIN_NORMAL, 0).normalize(), 0, 1, 0);
+        checkVector(Vector3D.of(0, 0, Double.MIN_NORMAL).normalize(), 0, 0, 1);
+
+        checkVector(Vector3D.of(Double.MIN_NORMAL, Double.MIN_NORMAL, -Double.MIN_NORMAL).normalize(),
+                invSqrt3, invSqrt3, -invSqrt3);
+
+        checkVector(Vector3D.of(Double.MAX_VALUE, -Double.MAX_VALUE, Double.MAX_VALUE).normalize(),
+                invSqrt3, -invSqrt3, invSqrt3);
+
         Assertions.assertEquals(1.0, Vector3D.of(5, -4, 2).normalize().norm(), EPS);
     }
 
     @Test
     public void testNormalize_illegalNorm() {
+        // arrange
+        final Pattern illegalNorm = Pattern.compile("^Illegal norm: (0\\.0|-?Infinity|NaN)");
+
         // act/assert
-        Assertions.assertThrows(IllegalArgumentException.class, Vector3D.ZERO::normalize);
-        Assertions.assertThrows(IllegalArgumentException.class, Vector3D.NaN::normalize);
-        Assertions.assertThrows(IllegalArgumentException.class, Vector3D.POSITIVE_INFINITY::normalize);
-        Assertions.assertThrows(IllegalArgumentException.class, Vector3D.NEGATIVE_INFINITY::normalize);
+        GeometryTestUtils.assertThrowsWithMessage(Vector3D.ZERO::normalize,
+                IllegalArgumentException.class, illegalNorm);
+        GeometryTestUtils.assertThrowsWithMessage(Vector3D.NaN::normalize,
+                IllegalArgumentException.class, illegalNorm);
+        GeometryTestUtils.assertThrowsWithMessage(Vector3D.POSITIVE_INFINITY::normalize,
+                IllegalArgumentException.class, illegalNorm);
+        GeometryTestUtils.assertThrowsWithMessage(Vector3D.NEGATIVE_INFINITY::normalize,
+                IllegalArgumentException.class, illegalNorm);
     }
 
     @Test
@@ -420,6 +443,48 @@ public class Vector3DTest {
     }
 
     @Test
+    public void testNormalizeOrNull() {
+        // arrange
+        final double invSqrt3 = 1 / Math.sqrt(3);
+
+        // act/assert
+        checkVector(Vector3D.of(100, 0, 0).normalizeOrNull(), 1, 0, 0);
+        checkVector(Vector3D.of(-100, 0, 0).normalizeOrNull(), -1, 0, 0);
+
+        checkVector(Vector3D.of(2, 2, 2).normalizeOrNull(), invSqrt3, invSqrt3, invSqrt3);
+        checkVector(Vector3D.of(-2, -2, -2).normalizeOrNull(), -invSqrt3, -invSqrt3, -invSqrt3);
+
+        checkVector(Vector3D.of(Double.MIN_VALUE, 0, 0).normalizeOrNull(), 1, 0, 0);
+        checkVector(Vector3D.of(0, Double.MIN_VALUE, 0).normalizeOrNull(), 0, 1, 0);
+        checkVector(Vector3D.of(0, 0, Double.MIN_VALUE).normalizeOrNull(), 0, 0, 1);
+
+        checkVector(Vector3D.of(-Double.MIN_VALUE, Double.MIN_VALUE, Double.MIN_VALUE).normalizeOrNull(),
+                -invSqrt3, invSqrt3, invSqrt3);
+
+        checkVector(Vector3D.of(Double.MIN_NORMAL, Double.MIN_NORMAL, -Double.MIN_NORMAL).normalizeOrNull(),
+                invSqrt3, invSqrt3, -invSqrt3);
+
+        checkVector(Vector3D.of(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE).normalizeOrNull(),
+                -invSqrt3, -invSqrt3, -invSqrt3);
+
+        Assertions.assertNull(Vector3D.ZERO.normalizeOrNull());
+        Assertions.assertNull(Vector3D.NaN.normalizeOrNull());
+        Assertions.assertNull(Vector3D.POSITIVE_INFINITY.normalizeOrNull());
+        Assertions.assertNull(Vector3D.NEGATIVE_INFINITY.normalizeOrNull());
+    }
+
+    @Test
+    public void testNormalizeOrNull_isIdempotent() {
+        // arrange
+        final double invSqrt3 = 1 / Math.sqrt(3);
+        final Vector3D v = Vector3D.of(2, 2, 2).normalizeOrNull();
+
+        // act/assert
+        Assertions.assertSame(v, v.normalizeOrNull());
+        checkVector(v.normalizeOrNull(), invSqrt3, invSqrt3, invSqrt3);
+    }
+
+    @Test
     public void testOrthogonal() {
         // arrange
         final Vector3D v1 = Vector3D.of(0.1, 2.5, 1.3);
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 07eba28..581b11b 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
@@ -317,21 +317,43 @@ public class Vector2DTest {
 
     @Test
     public void testNormalize() {
+        // arrange
+        final double invSqrt2 = 1.0 / Math.sqrt(2);
+
         // act/assert
         checkVector(Vector2D.of(100, 0).normalize(), 1, 0);
         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(Double.MIN_VALUE, 0).normalize(), 1, 0);
+        checkVector(Vector2D.of(0, Double.MIN_VALUE).normalize(), 0, 1);
+
+        checkVector(Vector2D.of(-Double.MIN_VALUE, Double.MIN_VALUE).normalize(), -invSqrt2, invSqrt2);
+
+        checkVector(Vector2D.of(Double.MIN_NORMAL, 0).normalize(), 1, 0, 0);
+        checkVector(Vector2D.of(0, Double.MIN_NORMAL).normalize(), 0, 1, 0);
+
+        checkVector(Vector2D.of(Double.MIN_NORMAL, -Double.MIN_NORMAL).normalize(), invSqrt2, -invSqrt2);
+
+        checkVector(Vector2D.of(-Double.MAX_VALUE, -Double.MAX_VALUE).normalize(), -invSqrt2, -invSqrt2);
     }
 
     @Test
     public void testNormalize_illegalNorm() {
+        // arrange
+        final Pattern illegalNorm = Pattern.compile("^Illegal norm: (0\\.0|-?Infinity|NaN)");
+
         // act/assert
-        Assertions.assertThrows(IllegalArgumentException.class, Vector2D.ZERO::normalize);
-        Assertions.assertThrows(IllegalArgumentException.class, Vector2D.NaN::normalize);
-        Assertions.assertThrows(IllegalArgumentException.class, Vector2D.POSITIVE_INFINITY::normalize);
-        Assertions.assertThrows(IllegalArgumentException.class, Vector2D.NEGATIVE_INFINITY::normalize);
+        GeometryTestUtils.assertThrowsWithMessage(Vector2D.ZERO::normalize,
+                IllegalArgumentException.class, illegalNorm);
+        GeometryTestUtils.assertThrowsWithMessage(Vector2D.NaN::normalize,
+                IllegalArgumentException.class, illegalNorm);
+        GeometryTestUtils.assertThrowsWithMessage(Vector2D.POSITIVE_INFINITY::normalize,
+                IllegalArgumentException.class, illegalNorm);
+        GeometryTestUtils.assertThrowsWithMessage(Vector2D.NEGATIVE_INFINITY::normalize,
+                IllegalArgumentException.class, illegalNorm);
     }
 
     @Test
@@ -346,6 +368,44 @@ public class Vector2DTest {
     }
 
     @Test
+    public void testNormalizeOrNull() {
+        // arrange
+        final double invSqrt2 = 1 / Math.sqrt(2);
+
+        // act/assert
+        checkVector(Vector2D.of(100, 0).normalizeOrNull(), 1, 0);
+        checkVector(Vector2D.of(-100, 0).normalizeOrNull(), -1, 0);
+
+        checkVector(Vector2D.of(2, 2).normalizeOrNull(), invSqrt2, invSqrt2);
+        checkVector(Vector2D.of(-2, -2).normalizeOrNull(), -invSqrt2, -invSqrt2);
+
+        checkVector(Vector2D.of(Double.MIN_VALUE, 0).normalizeOrNull(), 1, 0);
+        checkVector(Vector2D.of(0, Double.MIN_VALUE).normalizeOrNull(), 0, 1);
+
+        checkVector(Vector2D.of(-Double.MIN_VALUE, -Double.MIN_VALUE).normalizeOrNull(), -invSqrt2, -invSqrt2);
+
+        checkVector(Vector2D.of(Double.MIN_NORMAL, -Double.MIN_NORMAL).normalizeOrNull(), invSqrt2, -invSqrt2);
+
+        checkVector(Vector2D.of(Double.MAX_VALUE, -Double.MAX_VALUE).normalizeOrNull(), invSqrt2, -invSqrt2);
+
+        Assertions.assertNull(Vector2D.ZERO.normalizeOrNull());
+        Assertions.assertNull(Vector2D.NaN.normalizeOrNull());
+        Assertions.assertNull(Vector2D.POSITIVE_INFINITY.normalizeOrNull());
+        Assertions.assertNull(Vector2D.NEGATIVE_INFINITY.normalizeOrNull());
+    }
+
+    @Test
+    public void testNormalizeOrNull_isIdempotent() {
+        // arrange
+        final double invSqrt2 = 1 / Math.sqrt(2);
+        final Vector2D v = Vector2D.of(2, 2).normalizeOrNull();
+
+        // act/assert
+        Assertions.assertSame(v, v.normalizeOrNull());
+        checkVector(v.normalizeOrNull(), invSqrt2, invSqrt2);
+    }
+
+    @Test
     public void testNegate() {
         // act/assert
         checkVector(Vector2D.of(1, 2).negate(), -1, -2);
diff --git a/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/VectorPerformance.java b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/VectorPerformance.java
index 85df71e..65f0a28 100644
--- a/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/VectorPerformance.java
+++ b/commons-geometry-examples/examples-jmh/src/main/java/org/apache/commons/geometry/examples/jmh/euclidean/VectorPerformance.java
@@ -305,14 +305,14 @@ public class VectorPerformance {
         }
     }
 
-    /** Run a benchmark test on a function that produces a vector.
+    /** Run a benchmark test on a function that accepts a vector.
      * @param <V> Vector implementation type
      * @param input vector input
      * @param bh jmh blackhole for consuming output
      * @param fn function to call
      */
-    private static <V extends Vector<V>> void testUnary(final VectorInputBase<V> input, final Blackhole bh,
-            final UnaryOperator<V> fn) {
+    private static <V extends Vector<V>> void testFunction(final VectorInputBase<V> input, final Blackhole bh,
+            final Function<V, ?> fn) {
         for (final V vec : input.getVectors()) {
             bh.consume(fn.apply(vec));
         }
@@ -324,7 +324,7 @@ public class VectorPerformance {
      */
     @Benchmark
     public void baseline(final VectorInput1D input, final Blackhole bh) {
-        testUnary(input, bh, UnaryOperator.identity());
+        testFunction(input, bh, UnaryOperator.identity());
     }
 
     /** Benchmark testing the performance of the {@link Vector1D#norm()} method.
@@ -360,7 +360,16 @@ public class VectorPerformance {
      */
     @Benchmark
     public void normalize1D(final NormalizableVectorInput1D input, final Blackhole bh) {
-        testUnary(input, bh, Vector1D::normalize);
+        testFunction(input, bh, Vector1D::normalize);
+    }
+
+    /** Benchmark testing the performance of the {@link Vector1D#normalizeOrNull()} method.
+     * @param input benchmark state input
+     * @param bh jmh blackhole for consuming output
+     */
+    @Benchmark
+    public void normalizeOrNull1D(final VectorInput1D input, final Blackhole bh) {
+        testFunction(input, bh, v -> v.normalizeOrNull());
     }
 
     /** Benchmark testing the performance of the {@link Vector2D#normalize()}
@@ -370,7 +379,17 @@ public class VectorPerformance {
      */
     @Benchmark
     public void normalize2D(final NormalizableVectorInput2D input, final Blackhole bh) {
-        testUnary(input, bh, Vector2D::normalize);
+        testFunction(input, bh, Vector2D::normalize);
+    }
+
+    /** Benchmark testing the performance of the {@link Vector2D#normalizeOrNull()}
+     * method.
+     * @param input benchmark state input
+     * @param bh jmh blackhole for consuming output
+     */
+    @Benchmark
+    public void normalizeOrNull2D(final VectorInput2D input, final Blackhole bh) {
+        testFunction(input, bh, v -> v.normalizeOrNull());
     }
 
     /** Benchmark testing the performance of the {@link Vector3D#normalize()}
@@ -380,6 +399,16 @@ public class VectorPerformance {
      */
     @Benchmark
     public void normalize3D(final NormalizableVectorInput3D input, final Blackhole bh) {
-        testUnary(input, bh, Vector3D::normalize);
+        testFunction(input, bh, Vector3D::normalize);
+    }
+
+    /** Benchmark testing the performance of the {@link Vector3D#normalizeOrNull()}
+     * method.
+     * @param input benchmark state input
+     * @param bh jmh blackhole for consuming output
+     */
+    @Benchmark
+    public void normalizeOrNull3D(final VectorInput3D input, final Blackhole bh) {
+        testFunction(input, bh, v -> v.normalizeOrNull());
     }
 }
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/PolygonObjParser.java b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/PolygonObjParser.java
index c2d7031..5b7b452 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/PolygonObjParser.java
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/obj/PolygonObjParser.java
@@ -28,7 +28,6 @@ import java.util.function.IntFunction;
 import java.util.function.ToIntFunction;
 import java.util.stream.Collectors;
 
-import org.apache.commons.geometry.euclidean.internal.Vectors;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
 import org.apache.commons.geometry.io.core.internal.SimpleTextParser;
 
@@ -323,7 +322,7 @@ public class PolygonObjParser extends AbstractObjParser {
                 }
             }
 
-            return Vectors.tryNormalize(sum);
+            return sum.normalizeOrNull();
         }
 
         /** Compute a normal for the face using its first three vertices. The vertices will wind in a
@@ -340,7 +339,7 @@ public class PolygonObjParser extends AbstractObjParser {
             final Vector3D p1 = modelVertexFn.apply(vertexAttributes.get(1).getVertexIndex());
             final Vector3D p2 = modelVertexFn.apply(vertexAttributes.get(2).getVertexIndex());
 
-            return Vectors.tryNormalize(p0.vectorTo(p1).cross(p0.vectorTo(p2)));
+            return p0.vectorTo(p1).cross(p0.vectorTo(p2)).normalizeOrNull();
         }
 
         /** Get the vertex attributes for the face listed in the order that produces a counter-clockwise
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlUtils.java b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlUtils.java
index e47505f..2c81b5b 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlUtils.java
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlUtils.java
@@ -18,7 +18,6 @@ package org.apache.commons.geometry.io.euclidean.threed.stl;
 
 import java.nio.ByteBuffer;
 
-import org.apache.commons.geometry.euclidean.internal.Vectors;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
 
 /** Utility methods for the STL format.
@@ -51,7 +50,7 @@ final class StlUtils {
             final Vector3D normal) {
         if (normal != null) {
             // try to normalize it
-            final Vector3D normalized = Vectors.tryNormalize(normal);
+            final Vector3D normalized = normal.normalizeOrNull();
             if (normalized != null) {
                 return normalized;
             }
@@ -93,7 +92,7 @@ final class StlUtils {
      * @return the normal for the given triangle vertices or null if one could not be computed
      */
     private static Vector3D computeTriangleNormal(final Vector3D p1, final Vector3D p2, final Vector3D p3) {
-        final Vector3D normal = Vectors.tryNormalize(p1.vectorTo(p2).cross(p1.vectorTo(p3)));
+        final Vector3D normal = p1.vectorTo(p2).cross(p1.vectorTo(p3)).normalizeOrNull();
         return normal != null ?
                 normal :
                 null;