You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by lu...@apache.org on 2015/04/12 17:16:09 UTC

[math] Added a way to build polyhedrons sets from vertices and facets.

Repository: commons-math
Updated Branches:
  refs/heads/master 4aa1d98ad -> fa6fcf208


Added a way to build polyhedrons sets from vertices and facets.


Project: http://git-wip-us.apache.org/repos/asf/commons-math/repo
Commit: http://git-wip-us.apache.org/repos/asf/commons-math/commit/fa6fcf20
Tree: http://git-wip-us.apache.org/repos/asf/commons-math/tree/fa6fcf20
Diff: http://git-wip-us.apache.org/repos/asf/commons-math/diff/fa6fcf20

Branch: refs/heads/master
Commit: fa6fcf2080a7d2a2202713035d887d80e8a9400d
Parents: 4aa1d98
Author: Luc Maisonobe <lu...@apache.org>
Authored: Sun Apr 12 17:15:55 2015 +0200
Committer: Luc Maisonobe <lu...@apache.org>
Committed: Sun Apr 12 17:15:55 2015 +0200

----------------------------------------------------------------------
 pom.xml                                         |   8 +-
 src/changes/changes.xml                         |   4 +
 .../math4/exception/util/LocalizedFormats.java  |   4 +
 .../math4/geometry/euclidean/threed/Plane.java  |   8 +-
 .../euclidean/threed/PolyhedronsSet.java        | 201 ++++++++++++-
 .../util/LocalizedFormats_fr.properties         |   4 +
 .../exception/util/LocalizedFormatsTest.java    |   2 +-
 .../geometry/euclidean/threed/PLYParser.java    | 290 +++++++++++++++++++
 .../euclidean/threed/PolyhedronsSetTest.java    |  91 +++++-
 .../threed/pentomino-N-bad-orientation.ply      |  40 +++
 .../euclidean/threed/pentomino-N-hole.ply       |  39 +++
 .../threed/pentomino-N-out-of-plane.ply         |  40 +++
 .../euclidean/threed/pentomino-N-too-close.ply  |  86 ++++++
 .../geometry/euclidean/threed/pentomino-N.ply   |  39 +++
 14 files changed, 835 insertions(+), 21 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/commons-math/blob/fa6fcf20/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index f9712b6..ad8578d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -602,9 +602,15 @@
             <exclude>src/test/resources/org/apache/commons/math4/stat/data/NumAcc4.txt</exclude>
             <exclude>src/test/resources/org/apache/commons/math4/stat/data/Michelso.txt</exclude>
             <exclude>src/test/resources/org/apache/commons/math4/stat/data/Mavro.txt</exclude>
+            <exclude>src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/issue-1211.bsp</exclude>
+            <exclude>src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N-bad-orientation.ply</exclude>
+            <exclude>src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N-hole.ply</exclude>
+            <exclude>src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N-out-of-plane.ply</exclude>
+            <exclude>src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N-too-close.ply</exclude>
+            <exclude>src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N-.ply</exclude>
 
             <!-- direction numbers for Sobol generation from Frances Y. Kuo and Stephen Joe,
-                 available under a BSD-style license (see NOTICE.txt and LICENSE.txt) -->
+                 available under a BSD-style license (see LICENSE.txt) -->
             <exclude>src/main/resources/assets/org/apache/commons/math4/random/new-joe-kuo-6.1000</exclude>
 
             <!-- text file explaining reference to a public domain image -->

http://git-wip-us.apache.org/repos/asf/commons-math/blob/fa6fcf20/src/changes/changes.xml
----------------------------------------------------------------------
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 39fe6fe..0bd49c4 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -54,6 +54,10 @@ If the output is not quite correct, check for invisible trailing spaces!
     </release>
 
     <release version="4.0" date="XXXX-XX-XX" description="">
+      <action dev="luc" type="add">
+        Added a way to build polyhedrons sets from a list of vertices and
+        facets specified using vertices indices.
+      </action>    
       <action dev="luc" type="fix" issue="MATH-1191">
         Fixed ignored method parameters in QRDecomposition protected methods.
       </action>    

http://git-wip-us.apache.org/repos/asf/commons-math/blob/fa6fcf20/src/main/java/org/apache/commons/math4/exception/util/LocalizedFormats.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/math4/exception/util/LocalizedFormats.java b/src/main/java/org/apache/commons/math4/exception/util/LocalizedFormats.java
index 547bfe3..699a0cc 100644
--- a/src/main/java/org/apache/commons/math4/exception/util/LocalizedFormats.java
+++ b/src/main/java/org/apache/commons/math4/exception/util/LocalizedFormats.java
@@ -70,6 +70,7 @@ public enum LocalizedFormats implements Localizable {
     CANNOT_TRANSFORM_TO_DOUBLE("Conversion Exception in Transformation: {0}"),
     CARDAN_ANGLES_SINGULARITY("Cardan angles singularity"),
     CLASS_DOESNT_IMPLEMENT_COMPARABLE("class ({0}) does not implement Comparable"),
+    CLOSE_VERTICES("too close vertices near point ({0}, {1}, {2})"),
     CLOSEST_ORTHOGONAL_MATRIX_HAS_NEGATIVE_DETERMINANT("the closest orthogonal matrix has a negative determinant {0}"),
     COLUMN_INDEX_OUT_OF_RANGE("column index {0} out of allowed range [{1}, {2}]"),
     COLUMN_INDEX("column index ({0})"), /* keep */
@@ -91,6 +92,7 @@ public enum LocalizedFormats implements Localizable {
     DISCRETE_CUMULATIVE_PROBABILITY_RETURNED_NAN("Discrete cumulative probability function returned NaN for argument {0}"),
     DISTRIBUTION_NOT_LOADED("distribution not loaded"),
     DUPLICATED_ABSCISSA_DIVISION_BY_ZERO("duplicated abscissa {0} causes division by zero"),
+    EDGE_CONNECTED_TO_ONE_FACET("edge joining points ({0}, {1}, {2}) and ({3}, {4}, {5}) is connected to one facet only"),
     ELITISM_RATE("elitism rate ({0})"),
     EMPTY_CLUSTER_IN_K_MEANS("empty cluster in k-means"),
     EMPTY_INTERPOLATION_SAMPLE("sample for interpolation is empty"),
@@ -103,6 +105,7 @@ public enum LocalizedFormats implements Localizable {
     EULER_ANGLES_SINGULARITY("Euler angles singularity"),
     EVALUATION("evaluation"), /* keep */
     EXPANSION_FACTOR_SMALLER_THAN_ONE("expansion factor smaller than one ({0})"),
+    FACET_ORIENTATION_MISMATCH("facets orientation mismatch around edge joining points ({0}, {1}, {2}) and ({3}, {4}, {5})"),
     FACTORIAL_NEGATIVE_PARAMETER("must have n >= 0 for n!, got n = {0}"),
     FAILED_BRACKETING("number of iterations={4}, maximum iterations={5}, initial={6}, lower bound={7}, upper bound={8}, final a value={0}, final b value={1}, f(a)={2}, f(b)={3}"),
     FAILED_FRACTION_CONVERSION("Unable to convert {0} to fraction after {1} iterations"),
@@ -284,6 +287,7 @@ public enum LocalizedFormats implements Localizable {
     OUT_OF_BOUND_SIGNIFICANCE_LEVEL("out of bounds significance level {0}, must be between {1} and {2}"),
     SIGNIFICANCE_LEVEL("significance level ({0})"), /* keep */
     OUT_OF_ORDER_ABSCISSA_ARRAY("the abscissae array must be sorted in a strictly increasing order, but the {0}-th element is {1} whereas {2}-th is {3}"),
+    OUT_OF_PLANE("point ({0}, {1}, {2}) is out of plane"),
     OUT_OF_RANGE_ROOT_OF_UNITY_INDEX("out of range root of unity index {0} (must be in [{1};{2}])"),
     OUT_OF_RANGE("out of range"), /* keep */
     OUT_OF_RANGE_SIMPLE("{0} out of [{1}, {2}] range"), /* keep */

http://git-wip-us.apache.org/repos/asf/commons-math/blob/fa6fcf20/src/main/java/org/apache/commons/math4/geometry/euclidean/threed/Plane.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/math4/geometry/euclidean/threed/Plane.java b/src/main/java/org/apache/commons/math4/geometry/euclidean/threed/Plane.java
index b8f2fa6..039f5f2 100644
--- a/src/main/java/org/apache/commons/math4/geometry/euclidean/threed/Plane.java
+++ b/src/main/java/org/apache/commons/math4/geometry/euclidean/threed/Plane.java
@@ -311,8 +311,8 @@ public class Plane implements Hyperplane<Euclidean3D>, Embedding<Euclidean3D, Eu
      */
     public boolean isSimilarTo(final Plane plane) {
         final double angle = Vector3D.angle(w, plane.w);
-        return ((angle < 1.0e-10) && (FastMath.abs(originOffset - plane.originOffset) < 1.0e-10)) ||
-               ((angle > (FastMath.PI - 1.0e-10)) && (FastMath.abs(originOffset + plane.originOffset) < 1.0e-10));
+        return ((angle < 1.0e-10) && (FastMath.abs(originOffset - plane.originOffset) < tolerance)) ||
+               ((angle > (FastMath.PI - 1.0e-10)) && (FastMath.abs(originOffset + plane.originOffset) < tolerance));
     }
 
     /** Rotate the plane around the specified point.
@@ -375,7 +375,7 @@ public class Plane implements Hyperplane<Euclidean3D>, Embedding<Euclidean3D, Eu
      */
     public Line intersection(final Plane other) {
         final Vector3D direction = Vector3D.crossProduct(w, other.w);
-        if (direction.getNorm() < 1.0e-10) {
+        if (direction.getNorm() < tolerance) {
             return null;
         }
         final Vector3D point = intersection(this, other, new Plane(direction, tolerance));
@@ -446,7 +446,7 @@ public class Plane implements Hyperplane<Euclidean3D>, Embedding<Euclidean3D, Eu
      * @return true if p belongs to the plane
      */
     public boolean contains(final Vector3D p) {
-        return FastMath.abs(getOffset(p)) < 1.0e-10;
+        return FastMath.abs(getOffset(p)) < tolerance;
     }
 
     /** Get the offset (oriented distance) of a parallel plane.

http://git-wip-us.apache.org/repos/asf/commons-math/blob/fa6fcf20/src/main/java/org/apache/commons/math4/geometry/euclidean/threed/PolyhedronsSet.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/math4/geometry/euclidean/threed/PolyhedronsSet.java b/src/main/java/org/apache/commons/math4/geometry/euclidean/threed/PolyhedronsSet.java
index 2f3bfa9..744aa9f 100644
--- a/src/main/java/org/apache/commons/math4/geometry/euclidean/threed/PolyhedronsSet.java
+++ b/src/main/java/org/apache/commons/math4/geometry/euclidean/threed/PolyhedronsSet.java
@@ -17,11 +17,18 @@
 package org.apache.commons.math4.geometry.euclidean.threed;
 
 import java.awt.geom.AffineTransform;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.List;
 
+import org.apache.commons.math4.exception.MathIllegalArgumentException;
+import org.apache.commons.math4.exception.NumberIsTooSmallException;
+import org.apache.commons.math4.exception.util.LocalizedFormats;
 import org.apache.commons.math4.geometry.Point;
 import org.apache.commons.math4.geometry.euclidean.oned.Euclidean1D;
 import org.apache.commons.math4.geometry.euclidean.twod.Euclidean2D;
+import org.apache.commons.math4.geometry.euclidean.twod.PolygonsSet;
 import org.apache.commons.math4.geometry.euclidean.twod.SubLine;
 import org.apache.commons.math4.geometry.euclidean.twod.Vector2D;
 import org.apache.commons.math4.geometry.partitioning.AbstractRegion;
@@ -73,7 +80,7 @@ public class PolyhedronsSet extends AbstractRegion<Euclidean3D, Euclidean2D> {
         super(tree, tolerance);
     }
 
-    /** Build a polyhedrons set from a Boundary REPresentation (B-rep).
+    /** Build a polyhedrons set from a Boundary REPresentation (B-rep) specified by sub-hyperplanes.
      * <p>The boundary is provided as a collection of {@link
      * SubHyperplane sub-hyperplanes}. Each sub-hyperplane has the
      * interior part of the region on its minus side and the exterior on
@@ -99,6 +106,29 @@ public class PolyhedronsSet extends AbstractRegion<Euclidean3D, Euclidean2D> {
         super(boundary, tolerance);
     }
 
+    /** Build a polyhedrons set from a Boundary REPresentation (B-rep) specified by connected vertices.
+     * <p>
+     * The boundary is provided as a list of vertices and a list of facets.
+     * Each facet is specified as an integer array containing the arrays vertices
+     * indices in the vertices list. Each facet normal is oriented by right hand
+     * rule to the facet vertices list.
+     * </p>
+     * <p>
+     * Some basic sanity checks are performed but not everything is thoroughly
+     * assessed, so it remains under caller responsibility to ensure the vertices
+     * and facets are consistent and properly define a polyhedrons set.
+     * </p>
+     * @param vertices list of polyhedrons set vertices
+     * @param facets list of facets, as vertices indices in the vertices list
+     * @param tolerance tolerance below which points are considered identical
+     * @exception MathIllegalArgumentException if some basic sanity checks fail
+     * @since 3.5
+     */
+    public PolyhedronsSet(final List<Vector3D> vertices, final List<int[]> facets,
+                          final double tolerance) {
+        super(buildBoundary(vertices, facets, tolerance), tolerance);
+    }
+
     /** Build a parallellepipedic box.
      * @param xMin low bound along the x direction
      * @param xMax high bound along the x direction
@@ -147,6 +177,175 @@ public class PolyhedronsSet extends AbstractRegion<Euclidean3D, Euclidean2D> {
         return boundary.getTree(false);
     }
 
+    /** Build boundary from vertices and facets.
+     * @param vertices list of polyhedrons set vertices
+     * @param facets list of facets, as vertices indices in the vertices list
+     * @param tolerance tolerance below which points are considered identical
+     * @return boundary as a list of sub-hyperplanes
+     * @exception MathIllegalArgumentException if some basic sanity checks fail
+     * @since 3.5
+     */
+    private static List<SubHyperplane<Euclidean3D>> buildBoundary(final List<Vector3D> vertices,
+                                                                  final List<int[]> facets,
+                                                                  final double tolerance) {
+
+        // check vertices distances
+        for (int i = 0; i < vertices.size() - 1; ++i) {
+            final Vector3D vi = vertices.get(i);
+            for (int j = i + 1; j < vertices.size(); ++j) {
+                if (Vector3D.distance(vi, vertices.get(j)) <= tolerance) {
+                    throw new MathIllegalArgumentException(LocalizedFormats.CLOSE_VERTICES,
+                                                           vi.getX(), vi.getY(), vi.getZ());
+                }
+            }
+        }
+
+        // find how vertices are referenced by facets
+        final int[][] references = findReferences(vertices, facets);
+
+        // find how vertices are linked together by edges along the facets they belong to
+        final int[][] successors = successors(vertices, facets, references);
+
+        // check edges orientations
+        for (int vA = 0; vA < vertices.size(); ++vA) {
+            for (final int vB : successors[vA]) {
+
+                if (vB >= 0) {
+                    // when facets are properly oriented, if vB is the successor of vA on facet f1,
+                    // then there must be an adjacent facet f2 where vA is the successor of vB
+                    boolean found = false;
+                    for (final int v : successors[vB]) {
+                        found = found || (v == vA);
+                    }
+                    if (!found) {
+                        final Vector3D start = vertices.get(vA);
+                        final Vector3D end   = vertices.get(vB);
+                        throw new MathIllegalArgumentException(LocalizedFormats.EDGE_CONNECTED_TO_ONE_FACET,
+                                                               start.getX(), start.getY(), start.getZ(),
+                                                               end.getX(),   end.getY(),   end.getZ());
+                    }
+                }
+            }
+        }
+
+        final List<SubHyperplane<Euclidean3D>> boundary = new ArrayList<SubHyperplane<Euclidean3D>>();
+
+        for (final int[] facet : facets) {
+
+            // define facet plane from the first 3 points
+            Plane plane = new Plane(vertices.get(facet[0]), vertices.get(facet[1]), vertices.get(facet[2]),
+                                    tolerance);
+
+            // check all points are in the plane
+            final Vector2D[] two2Points = new Vector2D[facet.length];
+            for (int i = 0 ; i < facet.length; ++i) {
+                final Vector3D v = vertices.get(facet[i]);
+                if (!plane.contains(v)) {
+                    throw new MathIllegalArgumentException(LocalizedFormats.OUT_OF_PLANE,
+                                                           v.getX(), v.getY(), v.getZ());
+                }
+                two2Points[i] = plane.toSubSpace(v);
+            }
+
+            // create the polygonal facet
+            boundary.add(new SubPlane(plane, new PolygonsSet(tolerance, two2Points)));
+
+        }
+
+        return boundary;
+
+    }
+
+    /** Find the facets that reference each edges.
+     * @param vertices list of polyhedrons set vertices
+     * @param facets list of facets, as vertices indices in the vertices list
+     * @return references array such that r[v][k] = f for some k if facet f contains vertex v
+     * @exception MathIllegalArgumentException if some facets have fewer than 3 vertices
+     * @since 3.5
+     */
+    private static int[][] findReferences(final List<Vector3D> vertices, final List<int[]> facets) {
+
+        // find the maximum number of facets a vertex belongs to
+        final int[] nbFacets = new int[vertices.size()];
+        int maxFacets  = 0;
+        for (final int[] facet : facets) {
+            if (facet.length < 3) {
+                throw new NumberIsTooSmallException(LocalizedFormats.WRONG_NUMBER_OF_POINTS,
+                                                    3, facet.length, true);
+            }
+            for (final int index : facet) {
+                maxFacets = FastMath.max(maxFacets, ++nbFacets[index]);
+            }
+        }
+
+        // set up the references array
+        final int[][] references = new int[vertices.size()][maxFacets];
+        for (int[] r : references) {
+            Arrays.fill(r, -1);
+        }
+        for (int f = 0; f < facets.size(); ++f) {
+            for (final int v : facets.get(f)) {
+                // vertex v is referenced by facet f
+                int k = 0;
+                while (k < maxFacets && references[v][k] >= 0) {
+                    ++k;
+                }
+                references[v][k] = f;
+            }
+        }
+
+        return references;
+
+    }
+
+    /** Find the successors of all vertices among all facets they belong to.
+     * @param vertices list of polyhedrons set vertices
+     * @param facets list of facets, as vertices indices in the vertices list
+     * @param references facets references array
+     * @return indices of vertices that follow vertex v in some facet (the array
+     * may contain extra entries at the end, set to negative indices)
+     * @exception MathIllegalArgumentException if the same vertex appears more than
+     * once in the successors list (which means one facet orientation is wrong)
+     * @since 3.5
+     */
+    private static int[][] successors(final List<Vector3D> vertices, final List<int[]> facets,
+                                      final int[][] references) {
+
+        // create an array large enough
+        final int[][] successors = new int[vertices.size()][references[0].length];
+        for (final int[] s : successors) {
+            Arrays.fill(s, -1);
+        }
+
+        for (int v = 0; v < vertices.size(); ++v) {
+            for (int k = 0; k < successors[v].length && references[v][k] >= 0; ++k) {
+
+                // look for vertex v
+                final int[] facet = facets.get(references[v][k]);
+                int i = 0;
+                while (i < facet.length && facet[i] != v) {
+                    ++i;
+                }
+
+                // we have found vertex v, we deduce its successor on current facet
+                successors[v][k] = facet[(i + 1) % facet.length];
+                for (int l = 0; l < k; ++l) {
+                    if (successors[v][l] == successors[v][k]) {
+                        final Vector3D start = vertices.get(v);
+                        final Vector3D end   = vertices.get(successors[v][k]);
+                        throw new MathIllegalArgumentException(LocalizedFormats.FACET_ORIENTATION_MISMATCH,
+                                                               start.getX(), start.getY(), start.getZ(),
+                                                               end.getX(),   end.getY(),   end.getZ());
+                    }
+                }
+
+            }
+        }
+
+        return successors;
+
+    }
+
     /** {@inheritDoc} */
     @Override
     public PolyhedronsSet buildNew(final BSPTree<Euclidean3D> tree) {

http://git-wip-us.apache.org/repos/asf/commons-math/blob/fa6fcf20/src/main/resources/assets/org/apache/commons/math4/exception/util/LocalizedFormats_fr.properties
----------------------------------------------------------------------
diff --git a/src/main/resources/assets/org/apache/commons/math4/exception/util/LocalizedFormats_fr.properties b/src/main/resources/assets/org/apache/commons/math4/exception/util/LocalizedFormats_fr.properties
index 678546a..eba92a0 100644
--- a/src/main/resources/assets/org/apache/commons/math4/exception/util/LocalizedFormats_fr.properties
+++ b/src/main/resources/assets/org/apache/commons/math4/exception/util/LocalizedFormats_fr.properties
@@ -43,6 +43,7 @@ CANNOT_SUBSTITUTE_ELEMENT_FROM_EMPTY_ARRAY = impossible de substituer un \u00e9l
 CANNOT_TRANSFORM_TO_DOUBLE = Exception de conversion dans une transformation : {0}
 CARDAN_ANGLES_SINGULARITY = singularit\u00e9 d''angles de Cardan
 CLASS_DOESNT_IMPLEMENT_COMPARABLE = la classe ({0}) n''implante pas l''interface Comparable
+CLOSE_VERTICES = sommets trop proches \u00e0 proximit\u00e9 du point ({0}, {1}, {2})
 CLOSEST_ORTHOGONAL_MATRIX_HAS_NEGATIVE_DETERMINANT = la matrice orthogonale la plus proche a un d\u00e9terminant n\u00e9gatif {0}
 COLUMN_INDEX_OUT_OF_RANGE = l''index de colonne {0} est hors du domaine autoris\u00e9 [{1}, {2}]
 COLUMN_INDEX = index de colonne ({0})
@@ -64,6 +65,7 @@ DIMENSIONS_MISMATCH = dimensions incoh\u00e9rentes
 DISCRETE_CUMULATIVE_PROBABILITY_RETURNED_NAN = Discr\u00e8tes fonction de probabilit\u00e9 cumulative retourn\u00e9 NaN \u00e0 l''argument de {0}
 DISTRIBUTION_NOT_LOADED = aucune distribution n''a \u00e9t\u00e9 charg\u00e9e
 DUPLICATED_ABSCISSA_DIVISION_BY_ZERO = la duplication de l''abscisse {0} engendre une division par z\u00e9ro
+EDGE_CONNECTED_TO_ONE_FACET = l''ar\u00eate joignant les points ({0}, {1}, {2}) et ({3}, {4}, {5}) n''est connect\u00e9e qu''\u00e0 une seule facette
 ELITISM_RATE = proportion d''\u00e9litisme ({0})
 EMPTY_CLUSTER_IN_K_MEANS = groupe vide dans l''algorithme des k-moyennes
 EMPTY_INTERPOLATION_SAMPLE = \u00e9chantillon d''interpolation vide
@@ -76,6 +78,7 @@ EQUAL_VERTICES_IN_SIMPLEX = sommets {0} et {1} \u00e9gaux dans la configuration
 EULER_ANGLES_SINGULARITY = singularit\u00e9 d''angles d''Euler
 EVALUATION = \u00e9valuation
 EXPANSION_FACTOR_SMALLER_THAN_ONE = facteur d''extension inf\u00e9rieur \u00e0 un ({0})
+FACET_ORIENTATION_MISMATCH = orientations incoh\u00e9rentes des facettes de part et d''autre de l''ar\u00eate joignant les points ({0}, {1}, {2}) et ({3}, {4}, {5})
 FACTORIAL_NEGATIVE_PARAMETER = n doit \u00eatre positif pour le calcul de n!, or n = {0}
 FAILED_BRACKETING = nombre d''it\u00e9rations = {4}, it\u00e9rations maximum = {5}, valeur initiale = {6}, borne inf\u00e9rieure = {7}, borne sup\u00e9rieure = {8}, valeur a finale = {0}, valeur b finale = {1}, f(a) = {2}, f(b) = {3}
 FAILED_FRACTION_CONVERSION = Impossible de convertir {0} en fraction apr\u00e8s {1} it\u00e9rations
@@ -257,6 +260,7 @@ OUT_OF_BOUNDS_QUANTILE_VALUE = valeur de quantile {0} hors bornes, doit \u00eatr
 OUT_OF_BOUND_SIGNIFICANCE_LEVEL = niveau de signification {0} hors domaine, doit \u00eatre entre {1} et {2}
 SIGNIFICANCE_LEVEL = niveau de signification ({0})
 OUT_OF_ORDER_ABSCISSA_ARRAY = les abscisses doivent \u00eatre en ordre strictement croissant, mais l''\u00e9l\u00e9ment {0} vaut {1} alors que l''\u00e9l\u00e9ment {2} vaut {3}
+OUT_OF_PLANE = le point ({0}, {1}, {2}) est hors du plan
 OUT_OF_RANGE_ROOT_OF_UNITY_INDEX = l''indice de racine de l''unit\u00e9 {0} est hors du domaine autoris\u00e9 [{1};{2}]
 OUT_OF_RANGE_SIMPLE = {0} hors du domaine [{1}, {2}]
 OUT_OF_RANGE_LEFT = {0} hors du domaine ({1}, {2}]

http://git-wip-us.apache.org/repos/asf/commons-math/blob/fa6fcf20/src/test/java/org/apache/commons/math4/exception/util/LocalizedFormatsTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/commons/math4/exception/util/LocalizedFormatsTest.java b/src/test/java/org/apache/commons/math4/exception/util/LocalizedFormatsTest.java
index b497780..6912f5f 100644
--- a/src/test/java/org/apache/commons/math4/exception/util/LocalizedFormatsTest.java
+++ b/src/test/java/org/apache/commons/math4/exception/util/LocalizedFormatsTest.java
@@ -29,7 +29,7 @@ public class LocalizedFormatsTest {
 
     @Test
     public void testMessageNumber() {
-        Assert.assertEquals(322, LocalizedFormats.values().length);
+        Assert.assertEquals(326, LocalizedFormats.values().length);
     }
 
     @Test

http://git-wip-us.apache.org/repos/asf/commons-math/blob/fa6fcf20/src/test/java/org/apache/commons/math4/geometry/euclidean/threed/PLYParser.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/commons/math4/geometry/euclidean/threed/PLYParser.java b/src/test/java/org/apache/commons/math4/geometry/euclidean/threed/PLYParser.java
new file mode 100644
index 0000000..f65663c
--- /dev/null
+++ b/src/test/java/org/apache/commons/math4/geometry/euclidean/threed/PLYParser.java
@@ -0,0 +1,290 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.math4.geometry.euclidean.threed;
+
+import java.io.BufferedReader;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import org.apache.commons.math4.util.Precision;
+
+/** This class is a small and incomplete parser for PLY files.
+ * <p>
+ * This parser is only intended for test purposes, it does not
+ * parse the full header, it does not handle all properties,
+ * it has rudimentary error handling.
+ * </p>
+ * @since 3.5
+ */
+public class PLYParser {
+
+    /** Parsed vertices. */
+    private Vector3D[] vertices;
+
+    /** Parsed faces. */
+    private int[][] faces;
+
+    /** Reader for PLY data. */
+    private BufferedReader br;
+
+    /** Last parsed line. */
+    private String line;
+
+    /** Simple constructor.
+     * @param stream stream to parse (closing it remains caller responsibility)
+     * @exception IOException if stream cannot be read
+     * @exception ParseException if stream content cannot be parsed
+     */
+    public PLYParser(final InputStream stream)
+        throws IOException, ParseException {
+
+        try {
+            br = new BufferedReader(new InputStreamReader(stream, "UTF-8"));
+
+            // parse the header
+            List<Field> fields = parseNextLine();
+            if (fields.size() != 1 || fields.get(0).getToken() != Token.PLY) {
+                complain();
+            }
+
+            boolean parsing       = true;
+            int nbVertices        = -1;
+            int nbFaces           = -1;
+            int xIndex            = -1;
+            int yIndex            = -1;
+            int zIndex            = -1;
+            int vPropertiesNumber = -1;
+            boolean inVertexElt   = false;
+            boolean inFaceElt     = false;
+            while (parsing) {
+                fields = parseNextLine();
+                if (fields.size() < 1) {
+                    complain();
+                }
+                switch (fields.get(0).getToken()) {
+                    case FORMAT:
+                        if (fields.size() != 3 ||
+                        fields.get(1).getToken() != Token.ASCII ||
+                        fields.get(2).getToken() != Token.UNKNOWN ||
+                        !Precision.equals(Double.parseDouble(fields.get(2).getValue()), 1.0, 0.001)) {
+                            complain();
+                        }
+                        inVertexElt = false;
+                        inFaceElt   = false;
+                        break;
+                    case COMMENT:
+                        // we just ignore this line
+                        break;
+                    case ELEMENT:
+                        if (fields.size() != 3 ||
+                        (fields.get(1).getToken() != Token.VERTEX && fields.get(1).getToken() != Token.FACE) ||
+                        fields.get(2).getToken() != Token.UNKNOWN) {
+                            complain();
+                        }
+                        if (fields.get(1).getToken() == Token.VERTEX) {
+                            nbVertices  = Integer.parseInt(fields.get(2).getValue());
+                            inVertexElt = true;
+                            inFaceElt   = false;
+                        } else {
+                            nbFaces     = Integer.parseInt(fields.get(2).getValue());
+                            inVertexElt = false;
+                            inFaceElt   = true;
+                        }
+                        break;
+                    case PROPERTY:
+                        if (inVertexElt) {
+                            ++vPropertiesNumber;
+                            if (fields.size() != 3 ||
+                                (fields.get(1).getToken() != Token.CHAR   &&
+                                 fields.get(1).getToken() != Token.UCHAR  &&
+                                 fields.get(1).getToken() != Token.SHORT  &&
+                                 fields.get(1).getToken() != Token.USHORT &&
+                                 fields.get(1).getToken() != Token.INT    &&
+                                 fields.get(1).getToken() != Token.UINT   &&
+                                 fields.get(1).getToken() != Token.FLOAT  &&
+                                 fields.get(1).getToken() != Token.DOUBLE)) {
+                                complain();
+                            }
+                            if (fields.get(2).getToken() == Token.X) {
+                                xIndex = vPropertiesNumber;
+                            }else if (fields.get(2).getToken() == Token.Y) {
+                                yIndex = vPropertiesNumber;
+                            }else if (fields.get(2).getToken() == Token.Z) {
+                                zIndex = vPropertiesNumber;
+                            }
+                        } else if (inFaceElt) {
+                            if (fields.size() != 5 ||
+                                fields.get(1).getToken()  != Token.LIST   &&
+                                (fields.get(2).getToken() != Token.CHAR   &&
+                                 fields.get(2).getToken() != Token.UCHAR  &&
+                                 fields.get(2).getToken() != Token.SHORT  &&
+                                 fields.get(2).getToken() != Token.USHORT &&
+                                 fields.get(2).getToken() != Token.INT    &&
+                                 fields.get(2).getToken() != Token.UINT) ||
+                                (fields.get(3).getToken() != Token.CHAR   &&
+                                 fields.get(3).getToken() != Token.UCHAR  &&
+                                 fields.get(3).getToken() != Token.SHORT  &&
+                                 fields.get(3).getToken() != Token.USHORT &&
+                                 fields.get(3).getToken() != Token.INT    &&
+                                 fields.get(3).getToken() != Token.UINT) ||
+                                 fields.get(4).getToken() != Token.VERTEX_INDICES) {
+                                complain();
+                            }
+                        } else {
+                            complain();
+                        }
+                        break;
+                    case END_HEADER:
+                        inVertexElt = false;
+                        inFaceElt   = false;
+                        parsing     = false;
+                        break;
+                    default:
+                        throw new ParseException("unable to parse line: " + line, 0);
+                }
+            }
+            ++vPropertiesNumber;
+
+            // parse vertices
+            vertices = new Vector3D[nbVertices];
+            for (int i = 0; i < nbVertices; ++i) {
+                fields = parseNextLine();
+                if (fields.size() != vPropertiesNumber ||
+                    fields.get(xIndex).getToken() != Token.UNKNOWN ||
+                    fields.get(yIndex).getToken() != Token.UNKNOWN ||
+                    fields.get(zIndex).getToken() != Token.UNKNOWN) {
+                    complain();
+                }
+                vertices[i] = new Vector3D(Double.parseDouble(fields.get(xIndex).getValue()),
+                                           Double.parseDouble(fields.get(yIndex).getValue()),
+                                           Double.parseDouble(fields.get(zIndex).getValue()));
+            }
+
+            // parse faces
+            faces = new int[nbFaces][];
+            for (int i = 0; i < nbFaces; ++i) {
+                fields = parseNextLine();
+                if (fields.isEmpty() ||
+                    fields.size() != (Integer.parseInt(fields.get(0).getValue()) + 1)) {
+                    complain();
+                }
+                faces[i] = new int[fields.size() - 1];
+                for (int j = 0; j < faces[i].length; ++j) {
+                    faces[i][j] = Integer.parseInt(fields.get(j + 1).getValue());
+                }
+            }
+
+        } catch (NumberFormatException nfe) {
+            complain();
+        }
+    }
+
+    /** Complain about a bad line.
+     * @exception ParseException always thrown
+     */
+    private void complain() throws ParseException {
+        throw new ParseException("unable to parse line: " + line, 0);
+    }
+
+    /** Parse next line.
+     * @return parsed fields
+     * @exception IOException if stream cannot be read
+     * @exception ParseException if the line does not contain the expected number of fields
+     */
+    private List<Field> parseNextLine()
+        throws IOException, ParseException {
+        final List<Field> fields = new ArrayList<Field>();
+        line = br.readLine();
+        if (line == null) {
+            throw new EOFException();
+        }
+        final StringTokenizer tokenizer = new StringTokenizer(line);
+        while (tokenizer.hasMoreTokens()) {
+            fields.add(new Field(tokenizer.nextToken()));
+        }
+        return fields;
+    }
+
+    /** Get the parsed vertices.
+     * @return parsed vertices
+     */
+    public List<Vector3D> getVertices() {
+        return Arrays.asList(vertices);
+    }
+
+    /** Get the parsed faces.
+     * @return parsed faces
+     */
+    public List<int[]> getFaces() {
+        return Arrays.asList(faces);
+    }
+
+    /** Tokens from PLY files. */
+    private static enum Token {
+        PLY, FORMAT, ASCII, BINARY_BIG_ENDIAN, BINARY_LITTLE_ENDIAN,
+        COMMENT, ELEMENT, VERTEX, FACE, PROPERTY, LIST, OBJ_INFO,
+        CHAR, UCHAR, SHORT, USHORT, INT, UINT, FLOAT, DOUBLE,
+        X, Y, Z, VERTEX_INDICES, END_HEADER, UNKNOWN;
+    }
+
+    /** Parsed line fields. */
+    private static class Field {
+
+        /** Token. */
+        private final Token token;
+
+        /** Value. */
+        private final String value;
+
+        /** Simple constructor.
+         * @param value field value
+         */
+        public Field(final String value) {
+            Token parsedToken = null;
+            try {
+                parsedToken = Token.valueOf(value.toUpperCase());
+            } catch (IllegalArgumentException iae) {
+                parsedToken = Token.UNKNOWN;
+            }
+            this.token = parsedToken;
+            this.value = value;
+        }
+
+        /** Get the recognized token.
+         * @return recognized token
+         */
+        public Token getToken() {
+            return token;
+        }
+
+        /** Get the field value.
+         * @return field value
+         */
+        public String getValue() {
+            return value;
+        }
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/commons-math/blob/fa6fcf20/src/test/java/org/apache/commons/math4/geometry/euclidean/threed/PolyhedronsSetTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/commons/math4/geometry/euclidean/threed/PolyhedronsSetTest.java b/src/test/java/org/apache/commons/math4/geometry/euclidean/threed/PolyhedronsSetTest.java
index b96c045..5c6f93c 100644
--- a/src/test/java/org/apache/commons/math4/geometry/euclidean/threed/PolyhedronsSetTest.java
+++ b/src/test/java/org/apache/commons/math4/geometry/euclidean/threed/PolyhedronsSetTest.java
@@ -20,13 +20,17 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.Reader;
+import java.lang.reflect.Field;
 import java.text.ParseException;
 import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.Set;
+import java.util.Arrays;
+import java.util.List;
 
 import org.apache.commons.math4.exception.MathArithmeticException;
 import org.apache.commons.math4.exception.MathIllegalArgumentException;
+import org.apache.commons.math4.exception.util.ExceptionContext;
+import org.apache.commons.math4.exception.util.Localizable;
+import org.apache.commons.math4.exception.util.LocalizedFormats;
 import org.apache.commons.math4.geometry.Vector;
 import org.apache.commons.math4.geometry.euclidean.threed.Euclidean3D;
 import org.apache.commons.math4.geometry.euclidean.threed.Plane;
@@ -354,18 +358,7 @@ public class PolyhedronsSetTest {
             faces[9]=new int[]{1,4,0};  // front (-y)
             faces[10]=new int[]{3,6,2}; // back (+y)
             faces[11]=new int[]{6,3,7}; // back (+y)
-            //
-            Set<SubHyperplane<Euclidean3D>> pset=new HashSet<SubHyperplane<Euclidean3D>>();
-            for (int f=0; f<faces.length; f++) {
-                int[] vidx=faces[f];
-                Plane p=new Plane(verts[vidx[0]],verts[vidx[1]],verts[vidx[2]],tol);
-                Vector2D p0=p.toSubSpace(verts[vidx[0]]);
-                Vector2D p1=p.toSubSpace(verts[vidx[1]]);
-                Vector2D p2=p.toSubSpace(verts[vidx[2]]);
-                PolygonsSet lset=new PolygonsSet(tol,p0,p1,p2);
-                pset.add(new SubPlane(p,lset));
-            }
-            PolyhedronsSet polyset=new PolyhedronsSet(pset,tol);
+            PolyhedronsSet polyset = new PolyhedronsSet(Arrays.asList(verts), Arrays.asList(faces), tol);
             Assert.assertEquals(8.0, polyset.getSize(), 1.0e-10);
             Assert.assertEquals(24.0, polyset.getBoundarySize(), 1.0e-10);
             String dump = RegionDumper.dump(polyset);
@@ -376,6 +369,76 @@ public class PolyhedronsSetTest {
     }
 
     @Test
+    public void testConnectedFacets() throws IOException, ParseException {
+        InputStream stream = getClass().getResourceAsStream("pentomino-N.ply");
+        PLYParser   parser = new PLYParser(stream);
+        stream.close();
+        PolyhedronsSet polyhedron = new PolyhedronsSet(parser.getVertices(), parser.getFaces(), 1.0e-10);
+        Assert.assertEquals( 5.0, polyhedron.getSize(), 1.0e-10);
+        Assert.assertEquals(22.0, polyhedron.getBoundarySize(), 1.0e-10);
+    }
+
+    @Test
+    public void testTooClose() throws IOException, ParseException {
+        checkError("pentomino-N-too-close.ply", LocalizedFormats.CLOSE_VERTICES);
+    }
+
+    @Test
+    public void testHole() throws IOException, ParseException {
+        checkError("pentomino-N-hole.ply", LocalizedFormats.EDGE_CONNECTED_TO_ONE_FACET);
+    }
+
+    @Test
+    public void testNonPlanar() throws IOException, ParseException {
+        checkError("pentomino-N-out-of-plane.ply", LocalizedFormats.OUT_OF_PLANE);
+    }
+
+    @Test
+    public void testOrientation() throws IOException, ParseException {
+        checkError("pentomino-N-bad-orientation.ply", LocalizedFormats.FACET_ORIENTATION_MISMATCH);
+    }
+
+    @Test
+    public void testFacet2Vertices() throws IOException, ParseException {
+        checkError(Arrays.asList(Vector3D.ZERO, Vector3D.PLUS_I, Vector3D.PLUS_J, Vector3D.PLUS_K),
+                   Arrays.asList(new int[] { 0, 1, 2 }, new int[] {2, 3}),
+                   LocalizedFormats.WRONG_NUMBER_OF_POINTS);
+    }
+
+    private void checkError(final String resourceName, final LocalizedFormats expected) {
+        try {
+            InputStream stream = getClass().getResourceAsStream(resourceName);
+            PLYParser   parser = new PLYParser(stream);
+            stream.close();
+            checkError(parser.getVertices(), parser.getFaces(), expected);
+        } catch (IOException ioe) {
+            Assert.fail(ioe.getLocalizedMessage());
+        } catch (ParseException pe) {
+            Assert.fail(pe.getLocalizedMessage());
+        }
+    }
+
+    private void checkError(final List<Vector3D> vertices, final List<int[]> facets,
+                            final LocalizedFormats expected) {
+        try {
+            new PolyhedronsSet(vertices, facets, 1.0e-10);
+            Assert.fail("an exception should have been thrown");
+        } catch (MathIllegalArgumentException miae) {
+            try {
+                Field msgPatterns = ExceptionContext.class.getDeclaredField("msgPatterns");
+                msgPatterns.setAccessible(true);
+                @SuppressWarnings("unchecked")
+                List<Localizable> list = (List<Localizable>) msgPatterns.get(miae.getContext());
+                Assert.assertEquals(expected, list.get(0));
+            } catch (NoSuchFieldException nsfe) {
+                Assert.fail(nsfe.getLocalizedMessage());
+            } catch (IllegalAccessException iae) {
+                Assert.fail(iae.getLocalizedMessage());
+            }
+        }
+    }
+
+    @Test
     public void testIssue1211() throws IOException, ParseException {
 
         PolyhedronsSet polyset = RegionParser.parsePolyhedronsSet(loadTestData("issue-1211.bsp"));

http://git-wip-us.apache.org/repos/asf/commons-math/blob/fa6fcf20/src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N-bad-orientation.ply
----------------------------------------------------------------------
diff --git a/src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N-bad-orientation.ply b/src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N-bad-orientation.ply
new file mode 100644
index 0000000..4109576
--- /dev/null
+++ b/src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N-bad-orientation.ply
@@ -0,0 +1,40 @@
+ply
+format ascii 1.0
+comment this file represents the 'N' pentomino
+comment it has been created manually
+comment the shape has a reversed orientation for facet 3
+element vertex 16
+property double x
+property double y
+property double z
+element face 12
+property list uchar uint vertex_indices
+end_header
+0.0 0.0 0.0
+1.0 0.0 0.0
+1.0 1.0 0.0
+2.0 1.0 0.0
+2.0 4.0 0.0
+1.0 4.0 0.0
+1.0 2.0 0.0
+0.0 2.0 0.0
+0.0 0.0 1.0
+1.0 0.0 1.0
+1.0 1.0 1.0
+2.0 1.0 1.0
+2.0 4.0 1.0
+1.0 4.0 1.0
+1.0 2.0 1.0
+0.0 2.0 1.0
+5  8  9 10 14 15
+5 10 11 12 13 14
+5  7  6  2  1  0
+5  2  3  4  5  6
+4  0  1  9  8
+4  1  2 10  9
+4  2  3 11 10
+4  3  4 12 11
+4  4  5 13 12
+4  5  6 14 13
+4  6  7 15 14
+4  7  0  8 15
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/commons-math/blob/fa6fcf20/src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N-hole.ply
----------------------------------------------------------------------
diff --git a/src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N-hole.ply b/src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N-hole.ply
new file mode 100644
index 0000000..e40a025
--- /dev/null
+++ b/src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N-hole.ply
@@ -0,0 +1,39 @@
+ply
+format ascii 1.0
+comment this file represents the 'N' pentomino
+comment it has been created manually
+comment the shape has a missing face between vertices 0, 1, 9, 8
+element vertex 16
+property double x
+property double y
+property double z
+element face 11
+property list uchar uint vertex_indices
+end_header
+0.0 0.0 0.0
+1.0 0.0 0.0
+1.0 1.0 0.0
+2.0 1.0 0.0
+2.0 4.0 0.0
+1.0 4.0 0.0
+1.0 2.0 0.0
+0.0 2.0 0.0
+0.0 0.0 1.0
+1.0 0.0 1.0
+1.0 1.0 1.0
+2.0 1.0 1.0
+2.0 4.0 1.0
+1.0 4.0 1.0
+1.0 2.0 1.0
+0.0 2.0 1.0
+5  8  9 10 14 15
+5 10 11 12 13 14
+5  7  6  2  1  0
+5  6  5  4  3  2
+4  1  2 10  9
+4  2  3 11 10
+4  3  4 12 11
+4  4  5 13 12
+4  5  6 14 13
+4  6  7 15 14
+4  7  0  8 15
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/commons-math/blob/fa6fcf20/src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N-out-of-plane.ply
----------------------------------------------------------------------
diff --git a/src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N-out-of-plane.ply b/src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N-out-of-plane.ply
new file mode 100644
index 0000000..c345eda
--- /dev/null
+++ b/src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N-out-of-plane.ply
@@ -0,0 +1,40 @@
+ply
+format ascii 1.0
+comment this file represents the 'N' pentomino
+comment it has been created manually
+comment the shape is distorted with edge 7 moved, so associated facets are not planar
+element vertex 16
+property double x
+property double y
+property double z
+element face 12
+property list uchar uint vertex_indices
+end_header
+0.0 0.0 0.0
+1.0 0.0 0.0
+1.0 1.0 0.0
+2.0 1.0 0.0
+2.0 4.0 0.0
+1.0 4.0 0.0
+1.0 2.0 0.0
+0.0 2.0 0.5
+0.0 0.0 1.0
+1.0 0.0 1.0
+1.0 1.0 1.0
+2.0 1.0 1.0
+2.0 4.0 1.0
+1.0 4.0 1.0
+1.0 2.0 1.0
+0.0 2.0 1.0
+5  8  9 10 14 15
+5 10 11 12 13 14
+5  7  6  2  1  0
+5  6  5  4  3  2
+4  0  1  9  8
+4  1  2 10  9
+4  2  3 11 10
+4  3  4 12 11
+4  4  5 13 12
+4  5  6 14 13
+4  6  7 15 14
+4  7  0  8 15
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/commons-math/blob/fa6fcf20/src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N-too-close.ply
----------------------------------------------------------------------
diff --git a/src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N-too-close.ply b/src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N-too-close.ply
new file mode 100644
index 0000000..1701540
--- /dev/null
+++ b/src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N-too-close.ply
@@ -0,0 +1,86 @@
+ply
+format ascii 1.0
+comment this file should trigger an error as it contains several vertices at the same location
+comment the file was originally created using blender http://www.blender.org
+element vertex 52
+property float x
+property float y
+property float z
+property float nx
+property float ny
+property float nz
+element face 20
+property list uchar uint vertex_indices
+end_header
+0.000000 0.000000 0.000000 1.000000 0.000000 0.000000
+0.000000 1.000000 0.000000 1.000000 0.000000 0.000000
+0.000000 1.000000 1.000000 1.000000 0.000000 0.000000
+0.000000 0.000000 1.000000 1.000000 0.000000 0.000000
+0.000000 1.000000 0.000000 0.000000 1.000000 0.000000
+-2.000000 1.000000 0.000000 0.000000 1.000000 0.000000
+-2.000000 1.000000 1.000000 0.000000 1.000000 0.000000
+0.000000 1.000000 1.000000 0.000000 1.000000 0.000000
+-2.000000 1.000000 1.000000 0.000000 0.000000 0.000000
+1.000000 1.000000 1.000000 0.000000 0.000000 0.000000
+0.000000 1.000000 1.000000 0.000000 0.000000 0.000000
+-2.000000 2.000000 1.000000 0.000000 0.000000 -1.000000
+1.000000 2.000000 1.000000 0.000000 0.000000 -1.000000
+1.000000 1.000000 1.000000 0.000000 0.000000 -1.000000
+-2.000000 1.000000 1.000000 -0.000000 -0.000000 -1.000000
+-2.000000 2.000000 0.000000 0.000000 -1.000000 -0.000000
+1.000000 2.000000 0.000000 0.000000 -1.000000 -0.000000
+1.000000 2.000000 1.000000 0.000000 -1.000000 -0.000000
+-2.000000 2.000000 1.000000 0.000000 -1.000000 -0.000000
+0.000000 0.000000 0.000000 -0.000000 0.000000 1.000000
+1.000000 1.000000 0.000000 -0.000000 0.000000 1.000000
+0.000000 1.000000 0.000000 -0.000000 0.000000 1.000000
+2.000000 0.000000 0.000000 -0.000000 0.000000 1.000000
+2.000000 1.000000 0.000000 -0.000000 0.000000 1.000000
+2.000000 1.000000 0.000000 -1.000000 0.000000 -0.000000
+2.000000 0.000000 0.000000 -1.000000 0.000000 -0.000000
+2.000000 0.000000 1.000000 -1.000000 0.000000 -0.000000
+2.000000 1.000000 1.000000 -1.000000 0.000000 -0.000000
+2.000000 0.000000 0.000000 0.000000 1.000000 0.000000
+0.000000 0.000000 0.000000 0.000000 1.000000 0.000000
+0.000000 0.000000 1.000000 0.000000 1.000000 0.000000
+2.000000 0.000000 1.000000 0.000000 1.000000 0.000000
+-2.000000 1.000000 0.000000 1.000000 0.000000 0.000000
+-2.000000 2.000000 0.000000 1.000000 0.000000 0.000000
+-2.000000 2.000000 1.000000 1.000000 0.000000 0.000000
+-2.000000 1.000000 1.000000 1.000000 0.000000 0.000000
+1.000000 1.000000 0.000000 0.000000 -1.000000 -0.000000
+2.000000 1.000000 0.000000 0.000000 -1.000000 -0.000000
+2.000000 1.000000 1.000000 0.000000 -1.000000 -0.000000
+1.000000 1.000000 1.000000 0.000000 -1.000000 -0.000000
+1.000000 2.000000 0.000000 -1.000000 0.000000 -0.000000
+1.000000 1.000000 0.000000 -1.000000 0.000000 -0.000000
+1.000000 1.000000 1.000000 -1.000000 0.000000 -0.000000
+1.000000 2.000000 1.000000 -1.000000 0.000000 -0.000000
+-2.000000 1.000000 0.000000 -0.000000 0.000000 1.000000
+1.000000 2.000000 0.000000 -0.000000 0.000000 1.000000
+-2.000000 2.000000 0.000000 -0.000000 0.000000 1.000000
+0.000000 0.000000 1.000000 -0.000000 0.000000 -1.000000
+2.000000 1.000000 1.000000 -0.000000 0.000000 -1.000000
+2.000000 0.000000 1.000000 -0.000000 0.000000 -1.000000
+2.000000 1.000000 1.000000 0.000000 0.000000 0.000000
+0.000000 1.000000 1.000000 -0.000000 -0.000000 -1.000000
+4 0 1 2 3
+4 4 5 6 7
+3 8 9 10
+3 11 12 13
+3 14 11 13
+4 15 16 17 18
+3 19 20 21
+3 22 23 20
+3 19 22 20
+4 24 25 26 27
+4 28 29 30 31
+4 32 33 34 35
+4 36 37 38 39
+4 40 41 42 43
+3 44 45 46
+3 21 20 45
+3 44 21 45
+3 47 48 49
+3 10 9 50
+3 47 51 48

http://git-wip-us.apache.org/repos/asf/commons-math/blob/fa6fcf20/src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N.ply
----------------------------------------------------------------------
diff --git a/src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N.ply b/src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N.ply
new file mode 100644
index 0000000..4efbf20
--- /dev/null
+++ b/src/test/resources/org/apache/commons/math4/geometry/euclidean/threed/pentomino-N.ply
@@ -0,0 +1,39 @@
+ply
+format ascii 1.0
+comment this file represents the 'N' pentomino
+comment it has been created manually
+element vertex 16
+property double x
+property double y
+property double z
+element face 12
+property list uchar uint vertex_indices
+end_header
+0.0 0.0 0.0
+1.0 0.0 0.0
+1.0 1.0 0.0
+2.0 1.0 0.0
+2.0 4.0 0.0
+1.0 4.0 0.0
+1.0 2.0 0.0
+0.0 2.0 0.0
+0.0 0.0 1.0
+1.0 0.0 1.0
+1.0 1.0 1.0
+2.0 1.0 1.0
+2.0 4.0 1.0
+1.0 4.0 1.0
+1.0 2.0 1.0
+0.0 2.0 1.0
+5  8  9 10 14 15
+5 10 11 12 13 14
+5  7  6  2  1  0
+5  6  5  4  3  2
+4  0  1  9  8
+4  1  2 10  9
+4  2  3 11 10
+4  3  4 12 11
+4  4  5 13 12
+4  5  6 14 13
+4  6  7 15 14
+4  7  0  8 15
\ No newline at end of file