osdir.com

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[GitHub] asfgit closed pull request #1: GEOMETRY-1: Initial Code Commit


asfgit closed pull request #1: GEOMETRY-1:  Initial Code Commit
URL: https://github.com/apache/commons-geometry/pull/1
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/commons-geometry-bsp/pom.xml b/commons-geometry-core/pom.xml
similarity index 63%
rename from commons-geometry-bsp/pom.xml
rename to commons-geometry-core/pom.xml
index a02beae..0d10b34 100644
--- a/commons-geometry-bsp/pom.xml
+++ b/commons-geometry-core/pom.xml
@@ -27,20 +27,37 @@
   </parent>
 
   <groupId>org.apache.commons</groupId>
-  <artifactId>commons-geometry-bsp</artifactId>
+  <artifactId>commons-geometry-core</artifactId>
   <version>1.0-SNAPSHOT</version>
-  <name>Apache Commons Geometry Binary Space Partition</name>
+  <name>Apache Commons Geometry Core</name>
 
-  <description></description>
+  <description>Core interfaces and classes for Apache Commons Geometry.</description>
 
   <properties>
     <!-- OSGi -->
-    <commons.osgi.symbolicName>org.apache.commons.geometry.bsp</commons.osgi.symbolicName>
-    <commons.osgi.export>org.apache.commons.geometry.bsp</commons.osgi.export>
+    <commons.osgi.symbolicName>org.apache.commons.geometry.core</commons.osgi.symbolicName>
+    <commons.osgi.export>org.apache.commons.geometry.core</commons.osgi.export>
     <!-- Java 9+ -->
-    <commons.automatic.module.name>org.apache.commons.geometry.bsp</commons.automatic.module.name>
+    <commons.automatic.module.name>org.apache.commons.geometry.core</commons.automatic.module.name>
     <!-- Workaround to avoid duplicating config files. -->
     <geometry.parent.dir>${basedir}/..</geometry.parent.dir>
   </properties>
+  
+  <build>
+    <plugins>
+      <!-- Make the core test utilities accessible to other projects. -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>test-jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
 
 </project>
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Geometry.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Geometry.java
new file mode 100644
index 0000000..adef1ab
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Geometry.java
@@ -0,0 +1,32 @@
+/*
+ * 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.geometry.core;
+
+/** Class containing geometric constants.
+ */
+public class Geometry {
+
+    /** Alias for {@link Math#PI}, placed here for completeness. */
+    public static final double PI = Math.PI;
+
+    /** Constant representing {@code 2*pi}.
+     */
+    public static final double TWO_PI = 2.0 * Math.PI;
+
+    /** Private constructor */
+    private Geometry() {}
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Point.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Point.java
new file mode 100644
index 0000000..9a9b2f4
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Point.java
@@ -0,0 +1,45 @@
+/*
+ * 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.geometry.core;
+
+import java.io.Serializable;
+
+/** This interface represents a generic geometrical point.
+ * @param <S> Type of the space.
+ * @see Space
+ * @see Vector
+ */
+public interface Point<S extends Space> extends Serializable {
+
+    /** Get the space to which the point belongs.
+     * @return containing space
+     */
+    Space getSpace();
+
+    /**
+     * Returns true if any coordinate of this point is NaN; false otherwise
+     * @return  true if any coordinate of this point is NaN; false otherwise
+     */
+    boolean isNaN();
+
+    /** Compute the distance between the instance and another point.
+     * @param p second point
+     * @return the distance between the instance and p
+     */
+    double distance(Point<S> p);
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Space.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Space.java
new file mode 100644
index 0000000..a932550
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Space.java
@@ -0,0 +1,39 @@
+/*
+ * 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.geometry.core;
+
+import java.io.Serializable;
+
+/** This interface represents a generic space, with affine and vectorial counterparts.
+ * @see Vector
+ */
+public interface Space extends Serializable {
+
+    /** Get the dimension of the space.
+     * @return dimension of the space
+     */
+    int getDimension();
+
+    /** Get the n-1 dimension subspace of this space.
+     * @return n-1 dimension sub-space of this space
+     * @see #getDimension()
+     * @exception UnsupportedOperationException for dimension-1 spaces
+     * which do not have sub-spaces
+     */
+    Space getSubSpace() throws UnsupportedOperationException;
+
+}
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
new file mode 100644
index 0000000..e13799c
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/Vector.java
@@ -0,0 +1,160 @@
+/*
+ * 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.geometry.core;
+
+import java.text.NumberFormat;
+
+/** This interface represents a generic vector in a vectorial space or a point in an affine space.
+ * @param <S> Type of the space.
+ * @see Space
+ * @see Point
+ */
+public interface Vector<S extends Space> {
+
+    /** Get the space to which the point belongs.
+     * @return containing space
+     */
+    Space getSpace();
+
+    /** Get the null vector of the vectorial space or origin point of the affine space.
+     * @return null vector of the vectorial space or origin point of the affine space
+     */
+    Vector<S> getZero();
+
+    /** Get the L<sub>1</sub> norm for the vector.
+     * @return L<sub>1</sub> norm for the vector
+     */
+    double getNorm1();
+
+    /** Get the L<sub>2</sub> norm for the vector.
+     * @return Euclidean norm for the vector
+     */
+    double getNorm();
+
+    /** Get the square of the norm for the vector.
+     * @return square of the Euclidean norm for the vector
+     */
+    double getNormSq();
+
+    /** Get the L<sub>&infin;</sub> norm for the vector.
+     * @return L<sub>&infin;</sub> norm for the vector
+     */
+    double getNormInf();
+
+    /** Add a vector to the instance.
+     * @param v vector to add
+     * @return a new vector
+     */
+    Vector<S> add(Vector<S> v);
+
+    /** Add a scaled vector to the instance.
+     * @param factor scale factor to apply to v before adding it
+     * @param v vector to add
+     * @return a new vector
+     */
+    Vector<S> add(double factor, Vector<S> v);
+
+    /** Subtract a vector from the instance.
+     * @param v vector to subtract
+     * @return a new vector
+     */
+    Vector<S> subtract(Vector<S> v);
+
+    /** Subtract a scaled vector from the instance.
+     * @param factor scale factor to apply to v before subtracting it
+     * @param v vector to subtract
+     * @return a new vector
+     */
+    Vector<S> subtract(double factor, Vector<S> v);
+
+    /** Get the opposite of the instance.
+     * @return a new vector which is opposite to the instance
+     */
+    Vector<S> negate();
+
+    /** Get a normalized vector aligned with the instance.
+     * @return a new normalized vector
+     * @exception IllegalStateException if the norm is zero
+     */
+    Vector<S> normalize() throws IllegalStateException;
+
+    /** Multiply the instance by a scalar.
+     * @param a scalar
+     * @return a new vector
+     */
+    Vector<S> scalarMultiply(double a);
+
+    /**
+     * Returns true if any coordinate of this point is NaN; false otherwise
+     * @return  true if any coordinate of this point is NaN; false otherwise
+     */
+    boolean isNaN();
+
+    /**
+     * Returns true if any coordinate of this vector is infinite and none are NaN;
+     * false otherwise
+     * @return  true if any coordinate of this vector is infinite and none are NaN;
+     * false otherwise
+     */
+    boolean isInfinite();
+
+    /** Compute the distance between the instance and another vector according to the L<sub>1</sub> norm.
+     * <p>Calling this method is equivalent to calling:
+     * <code>q.subtract(p).getNorm1()</code> except that no intermediate
+     * vector is built</p>
+     * @param v second vector
+     * @return the distance between the instance and p according to the L<sub>1</sub> norm
+     */
+    double distance1(Vector<S> v);
+
+    /** Compute the distance between the instance and another vector.
+     * @param v second vector
+     * @return the distance between the instance and v
+     */
+    double distance(Vector<S> v);
+
+    /** Compute the distance between the instance and another vector according to the L<sub>&infin;</sub> norm.
+     * <p>Calling this method is equivalent to calling:
+     * <code>q.subtract(p).getNormInf()</code> except that no intermediate
+     * vector is built</p>
+     * @param v second vector
+     * @return the distance between the instance and p according to the L<sub>&infin;</sub> norm
+     */
+    double distanceInf(Vector<S> v);
+
+    /** Compute the square of the distance between the instance and another vector.
+     * <p>Calling this method is equivalent to calling:
+     * <code>q.subtract(p).getNormSq()</code> except that no intermediate
+     * vector is built</p>
+     * @param v second vector
+     * @return the square of the distance between the instance and p
+     */
+    double distanceSq(Vector<S> v);
+
+    /** Compute the dot-product of the instance and another vector.
+     * @param v second vector
+     * @return the dot product this.v
+     */
+    double dotProduct(Vector<S> v);
+
+    /** Get a string representation of this vector.
+     * @param format the custom format for components
+     * @return a string representation of this vector
+     */
+    String toString(final NumberFormat format);
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/package-info.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/package-info.java
new file mode 100644
index 0000000..a45caa8
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+/**
+ *
+ * <p>
+ * This package is the top level package for geometry. It provides only a few interfaces
+ * related to vectorial/affine spaces that are implemented in sub-packages.
+ * </p>
+ *
+ */
+package org.apache.commons.geometry.core;
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractRegion.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractRegion.java
new file mode 100644
index 0000000..bc23114
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractRegion.java
@@ -0,0 +1,545 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeSet;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+import org.apache.commons.geometry.core.Vector;
+
+/** Abstract class for all regions, independently of geometry type or dimension.
+
+ * @param <S> Type of the space.
+ * @param <T> Type of the sub-space.
+ */
+public abstract class AbstractRegion<S extends Space, T extends Space> implements Region<S> {
+
+    /** Inside/Outside BSP tree. */
+    private BSPTree<S> tree;
+
+    /** Tolerance below which points are considered to belong to hyperplanes. */
+    private final double tolerance;
+
+    /** Size of the instance. */
+    private double size;
+
+    /** Barycenter. */
+    private Point<S> barycenter;
+
+    /** Build a region representing the whole space.
+     * @param tolerance tolerance below which points are considered identical.
+     */
+    protected AbstractRegion(final double tolerance) {
+        this.tree      = new BSPTree<>(Boolean.TRUE);
+        this.tolerance = tolerance;
+    }
+
+    /** Build a region from an inside/outside BSP tree.
+     * <p>The leaf nodes of the BSP tree <em>must</em> have a
+     * {@code Boolean} attribute representing the inside status of
+     * the corresponding cell (true for inside cells, false for outside
+     * cells). In order to avoid building too many small objects, it is
+     * recommended to use the predefined constants
+     * {@code Boolean.TRUE} and {@code Boolean.FALSE}. The
+     * tree also <em>must</em> have either null internal nodes or
+     * internal nodes representing the boundary as specified in the
+     * {@link #getTree getTree} method).</p>
+     * @param tree inside/outside BSP tree representing the region
+     * @param tolerance tolerance below which points are considered identical.
+     */
+    protected AbstractRegion(final BSPTree<S> tree, final double tolerance) {
+        this.tree      = tree;
+        this.tolerance = tolerance;
+    }
+
+    /** Build a Region from a Boundary REPresentation (B-rep).
+     * <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
+     * its plus side.</p>
+     * <p>The boundary elements can be in any order, and can form
+     * several non-connected sets (like for example polygons with holes
+     * or a set of disjoints polyhedrons considered as a whole). In
+     * fact, the elements do not even need to be connected together
+     * (their topological connections are not used here). However, if the
+     * boundary does not really separate an inside open from an outside
+     * open (open having here its topological meaning), then subsequent
+     * calls to the {@link #checkPoint(Point) checkPoint} method will not be
+     * meaningful anymore.</p>
+     * <p>If the boundary is empty, the region will represent the whole
+     * space.</p>
+     * @param boundary collection of boundary elements, as a
+     * collection of {@link SubHyperplane SubHyperplane} objects
+     * @param tolerance tolerance below which points are considered identical.
+     */
+    protected AbstractRegion(final Collection<SubHyperplane<S>> boundary, final double tolerance) {
+
+        this.tolerance = tolerance;
+
+        if (boundary.size() == 0) {
+
+            // the tree represents the whole space
+            tree = new BSPTree<>(Boolean.TRUE);
+
+        } else {
+
+            // sort the boundary elements in decreasing size order
+            // (we don't want equal size elements to be removed, so
+            // we use a trick to fool the TreeSet)
+            final TreeSet<SubHyperplane<S>> ordered = new TreeSet<>(new Comparator<SubHyperplane<S>>() {
+                /** {@inheritDoc} */
+                @Override
+                public int compare(final SubHyperplane<S> o1, final SubHyperplane<S> o2) {
+                    final double size1 = o1.getSize();
+                    final double size2 = o2.getSize();
+                    return (size2 < size1) ? -1 : ((o1 == o2) ? 0 : +1);
+                }
+            });
+            ordered.addAll(boundary);
+
+            // build the tree top-down
+            tree = new BSPTree<>();
+            insertCuts(tree, ordered);
+
+            // set up the inside/outside flags
+            tree.visit(new BSPTreeVisitor<S>() {
+
+                /** {@inheritDoc} */
+                @Override
+                public Order visitOrder(final BSPTree<S> node) {
+                    return Order.PLUS_SUB_MINUS;
+                }
+
+                /** {@inheritDoc} */
+                @Override
+                public void visitInternalNode(final BSPTree<S> node) {
+                }
+
+                /** {@inheritDoc} */
+                @Override
+                public void visitLeafNode(final BSPTree<S> node) {
+                    if (node.getParent() == null || node == node.getParent().getMinus()) {
+                        node.setAttribute(Boolean.TRUE);
+                    } else {
+                        node.setAttribute(Boolean.FALSE);
+                    }
+                }
+            });
+
+        }
+
+    }
+
+    /** Build a convex region from an array of bounding hyperplanes.
+     * @param hyperplanes array of bounding hyperplanes (if null, an
+     * empty region will be built)
+     * @param tolerance tolerance below which points are considered identical.
+     */
+    public AbstractRegion(final Hyperplane<S>[] hyperplanes, final double tolerance) {
+        this.tolerance = tolerance;
+        if ((hyperplanes == null) || (hyperplanes.length == 0)) {
+            tree = new BSPTree<>(Boolean.FALSE);
+        } else {
+
+            // use the first hyperplane to build the right class
+            tree = hyperplanes[0].wholeSpace().getTree(false);
+
+            // chop off parts of the space
+            BSPTree<S> node = tree;
+            node.setAttribute(Boolean.TRUE);
+            for (final Hyperplane<S> hyperplane : hyperplanes) {
+                if (node.insertCut(hyperplane)) {
+                    node.setAttribute(null);
+                    node.getPlus().setAttribute(Boolean.FALSE);
+                    node = node.getMinus();
+                    node.setAttribute(Boolean.TRUE);
+                }
+            }
+
+        }
+
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public abstract AbstractRegion<S, T> buildNew(BSPTree<S> newTree);
+
+    /** Get the tolerance below which points are considered to belong to hyperplanes.
+     * @return tolerance below which points are considered to belong to hyperplanes
+     */
+    public double getTolerance() {
+        return tolerance;
+    }
+
+    /** Recursively build a tree by inserting cut sub-hyperplanes.
+     * @param node current tree node (it is a leaf node at the beginning
+     * of the call)
+     * @param boundary collection of edges belonging to the cell defined
+     * by the node
+     */
+    private void insertCuts(final BSPTree<S> node, final Collection<SubHyperplane<S>> boundary) {
+
+        final Iterator<SubHyperplane<S>> iterator = boundary.iterator();
+
+        // build the current level
+        Hyperplane<S> inserted = null;
+        while ((inserted == null) && iterator.hasNext()) {
+            inserted = iterator.next().getHyperplane();
+            if (!node.insertCut(inserted.copySelf())) {
+                inserted = null;
+            }
+        }
+
+        if (!iterator.hasNext()) {
+            return;
+        }
+
+        // distribute the remaining edges in the two sub-trees
+        final ArrayList<SubHyperplane<S>> plusList  = new ArrayList<>();
+        final ArrayList<SubHyperplane<S>> minusList = new ArrayList<>();
+        while (iterator.hasNext()) {
+            final SubHyperplane<S> other = iterator.next();
+            final SubHyperplane.SplitSubHyperplane<S> split = other.split(inserted);
+            switch (split.getSide()) {
+            case PLUS:
+                plusList.add(other);
+                break;
+            case MINUS:
+                minusList.add(other);
+                break;
+            case BOTH:
+                plusList.add(split.getPlus());
+                minusList.add(split.getMinus());
+                break;
+            default:
+                // ignore the sub-hyperplanes belonging to the cut hyperplane
+            }
+        }
+
+        // recurse through lower levels
+        insertCuts(node.getPlus(),  plusList);
+        insertCuts(node.getMinus(), minusList);
+
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public AbstractRegion<S, T> copySelf() {
+        return buildNew(tree.copySelf());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isEmpty() {
+        return isEmpty(tree);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isEmpty(final BSPTree<S> node) {
+
+        // we use a recursive function rather than the BSPTreeVisitor
+        // interface because we can stop visiting the tree as soon as we
+        // have found an inside cell
+
+        if (node.getCut() == null) {
+            // if we find an inside node, the region is not empty
+            return !((Boolean) node.getAttribute());
+        }
+
+        // check both sides of the sub-tree
+        return isEmpty(node.getMinus()) && isEmpty(node.getPlus());
+
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isFull() {
+        return isFull(tree);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isFull(final BSPTree<S> node) {
+
+        // we use a recursive function rather than the BSPTreeVisitor
+        // interface because we can stop visiting the tree as soon as we
+        // have found an outside cell
+
+        if (node.getCut() == null) {
+            // if we find an outside node, the region does not cover full space
+            return (Boolean) node.getAttribute();
+        }
+
+        // check both sides of the sub-tree
+        return isFull(node.getMinus()) && isFull(node.getPlus());
+
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean contains(final Region<S> region) {
+        return new RegionFactory<S>().difference(region, this).isEmpty();
+    }
+
+    /** {@inheritDoc}
+     */
+    @Override
+    public BoundaryProjection<S> projectToBoundary(final Point<S> point) {
+        final BoundaryProjector<S, T> projector = new BoundaryProjector<>(point);
+        getTree(true).visit(projector);
+        return projector.getProjection();
+    }
+
+    /** Check a point with respect to the region.
+     * @param point point to check
+     * @return a code representing the point status: either {@link
+     * Region.Location#INSIDE}, {@link Region.Location#OUTSIDE} or
+     * {@link Region.Location#BOUNDARY}
+     */
+//    public Location checkPoint(final Vector<S> point) {
+//        return checkPoint((Point<S>) point);
+//    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Location checkPoint(final Point<S> point) {
+        return checkPoint(tree, point);
+    }
+
+    /** Check a point with respect to the region starting at a given node.
+     * @param node root node of the region
+     * @param point point to check
+     * @return a code representing the point status: either {@link
+     * Region.Location#INSIDE INSIDE}, {@link Region.Location#OUTSIDE
+     * OUTSIDE} or {@link Region.Location#BOUNDARY BOUNDARY}
+     */
+    protected Location checkPoint(final BSPTree<S> node, final Vector<S> point) {
+        return checkPoint(node, (Point<S>) point);
+    }
+
+    /** Check a point with respect to the region starting at a given node.
+     * @param node root node of the region
+     * @param point point to check
+     * @return a code representing the point status: either {@link
+     * Region.Location#INSIDE INSIDE}, {@link Region.Location#OUTSIDE
+     * OUTSIDE} or {@link Region.Location#BOUNDARY BOUNDARY}
+     */
+    protected Location checkPoint(final BSPTree<S> node, final Point<S> point) {
+        final BSPTree<S> cell = node.getCell(point, tolerance);
+        if (cell.getCut() == null) {
+            // the point is in the interior of a cell, just check the attribute
+            return ((Boolean) cell.getAttribute()) ? Location.INSIDE : Location.OUTSIDE;
+        }
+
+        // the point is on a cut-sub-hyperplane, is it on a boundary ?
+        final Location minusCode = checkPoint(cell.getMinus(), point);
+        final Location plusCode  = checkPoint(cell.getPlus(),  point);
+        return (minusCode == plusCode) ? minusCode : Location.BOUNDARY;
+
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public BSPTree<S> getTree(final boolean includeBoundaryAttributes) {
+        if (includeBoundaryAttributes && (tree.getCut() != null) && (tree.getAttribute() == null)) {
+            // compute the boundary attributes
+            tree.visit(new BoundaryBuilder<S>());
+        }
+        return tree;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getBoundarySize() {
+        final BoundarySizeVisitor<S> visitor = new BoundarySizeVisitor<>();
+        getTree(true).visit(visitor);
+        return visitor.getSize();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getSize() {
+        if (barycenter == null) {
+            computeGeometricalProperties();
+        }
+        return size;
+    }
+
+    /** Set the size of the instance.
+     * @param size size of the instance
+     */
+    protected void setSize(final double size) {
+        this.size = size;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Point<S> getBarycenter() {
+        if (barycenter == null) {
+            computeGeometricalProperties();
+        }
+        return barycenter;
+    }
+
+    /** Set the barycenter of the instance.
+     * @param barycenter barycenter of the instance
+     */
+    protected void setBarycenter(final Vector<S> barycenter) {
+        setBarycenter((Point<S>) barycenter);
+    }
+
+    /** Set the barycenter of the instance.
+     * @param barycenter barycenter of the instance
+     */
+    protected void setBarycenter(final Point<S> barycenter) {
+        this.barycenter = barycenter;
+    }
+
+    /** Compute some geometrical properties.
+     * <p>The properties to compute are the barycenter and the size.</p>
+     */
+    protected abstract void computeGeometricalProperties();
+
+    /** {@inheritDoc} */
+    @Override
+    public SubHyperplane<S> intersection(final SubHyperplane<S> sub) {
+        return recurseIntersection(tree, sub);
+    }
+
+    /** Recursively compute the parts of a sub-hyperplane that are
+     * contained in the region.
+     * @param node current BSP tree node
+     * @param sub sub-hyperplane traversing the region
+     * @return filtered sub-hyperplane
+     */
+    private SubHyperplane<S> recurseIntersection(final BSPTree<S> node, final SubHyperplane<S> sub) {
+
+        if (node.getCut() == null) {
+            return (Boolean) node.getAttribute() ? sub.copySelf() : null;
+        }
+
+        final Hyperplane<S> hyperplane = node.getCut().getHyperplane();
+        final SubHyperplane.SplitSubHyperplane<S> split = sub.split(hyperplane);
+        if (split.getPlus() != null) {
+            if (split.getMinus() != null) {
+                // both sides
+                final SubHyperplane<S> plus  = recurseIntersection(node.getPlus(),  split.getPlus());
+                final SubHyperplane<S> minus = recurseIntersection(node.getMinus(), split.getMinus());
+                if (plus == null) {
+                    return minus;
+                } else if (minus == null) {
+                    return plus;
+                } else {
+                    return plus.reunite(minus);
+                }
+            } else {
+                // only on plus side
+                return recurseIntersection(node.getPlus(), sub);
+            }
+        } else if (split.getMinus() != null) {
+            // only on minus side
+            return recurseIntersection(node.getMinus(), sub);
+        } else {
+            // on hyperplane
+            return recurseIntersection(node.getPlus(),
+                                       recurseIntersection(node.getMinus(), sub));
+        }
+
+    }
+
+    /** Transform a region.
+     * <p>Applying a transform to a region consist in applying the
+     * transform to all the hyperplanes of the underlying BSP tree and
+     * of the boundary (and also to the sub-hyperplanes embedded in
+     * these hyperplanes) and to the barycenter. The instance is not
+     * modified, a new instance is built.</p>
+     * @param transform transform to apply
+     * @return a new region, resulting from the application of the
+     * transform to the instance
+     */
+    public AbstractRegion<S, T> applyTransform(final Transform<S, T> transform) {
+
+        // transform the tree, except for boundary attribute splitters
+        final Map<BSPTree<S>, BSPTree<S>> map = new HashMap<>();
+        final BSPTree<S> transformedTree = recurseTransform(getTree(false), transform, map);
+
+        // set up the boundary attributes splitters
+        for (final Map.Entry<BSPTree<S>, BSPTree<S>> entry : map.entrySet()) {
+            if (entry.getKey().getCut() != null) {
+                @SuppressWarnings("unchecked")
+                BoundaryAttribute<S> original = (BoundaryAttribute<S>) entry.getKey().getAttribute();
+                if (original != null) {
+                    @SuppressWarnings("unchecked")
+                    BoundaryAttribute<S> transformed = (BoundaryAttribute<S>) entry.getValue().getAttribute();
+                    for (final BSPTree<S> splitter : original.getSplitters()) {
+                        transformed.getSplitters().add(map.get(splitter));
+                    }
+                }
+            }
+        }
+
+        return buildNew(transformedTree);
+
+    }
+
+    /** Recursively transform an inside/outside BSP-tree.
+     * @param node current BSP tree node
+     * @param transform transform to apply
+     * @param map transformed nodes map
+     * @return a new tree
+     */
+    @SuppressWarnings("unchecked")
+    private BSPTree<S> recurseTransform(final BSPTree<S> node, final Transform<S, T> transform,
+                                        final Map<BSPTree<S>, BSPTree<S>> map) {
+
+        final BSPTree<S> transformedNode;
+        if (node.getCut() == null) {
+            transformedNode = new BSPTree<>(node.getAttribute());
+        } else {
+
+            final SubHyperplane<S>  sub = node.getCut();
+            final SubHyperplane<S> tSub = ((AbstractSubHyperplane<S, T>) sub).applyTransform(transform);
+            BoundaryAttribute<S> attribute = (BoundaryAttribute<S>) node.getAttribute();
+            if (attribute != null) {
+                final SubHyperplane<S> tPO = (attribute.getPlusOutside() == null) ?
+                    null : ((AbstractSubHyperplane<S, T>) attribute.getPlusOutside()).applyTransform(transform);
+                final SubHyperplane<S> tPI = (attribute.getPlusInside()  == null) ?
+                    null  : ((AbstractSubHyperplane<S, T>) attribute.getPlusInside()).applyTransform(transform);
+                // we start with an empty list of splitters, it will be filled in out of recursion
+                attribute = new BoundaryAttribute<>(tPO, tPI, new NodesSet<S>());
+            }
+
+            transformedNode = new BSPTree<>(tSub,
+                                             recurseTransform(node.getPlus(),  transform, map),
+                                             recurseTransform(node.getMinus(), transform, map),
+                                             attribute);
+        }
+
+        map.put(node, transformedNode);
+        return transformedNode;
+
+    }
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractSubHyperplane.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractSubHyperplane.java
new file mode 100644
index 0000000..08d885e
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/AbstractSubHyperplane.java
@@ -0,0 +1,189 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.geometry.core.Space;
+
+/** This class implements the dimension-independent parts of {@link SubHyperplane}.
+
+ * <p>sub-hyperplanes are obtained when parts of an {@link
+ * Hyperplane hyperplane} are chopped off by other hyperplanes that
+ * intersect it. The remaining part is a convex region. Such objects
+ * appear in {@link BSPTree BSP trees} as the intersection of a cut
+ * hyperplane with the convex region which it splits, the chopping
+ * hyperplanes are the cut hyperplanes closer to the tree root.</p>
+
+ * @param <S> Type of the embedding space.
+ * @param <T> Type of the embedded sub-space.
+ */
+public abstract class AbstractSubHyperplane<S extends Space, T extends Space>
+    implements SubHyperplane<S> {
+
+    /** Underlying hyperplane. */
+    private final Hyperplane<S> hyperplane;
+
+    /** Remaining region of the hyperplane. */
+    private final Region<T> remainingRegion;
+
+    /** Build a sub-hyperplane from an hyperplane and a region.
+     * @param hyperplane underlying hyperplane
+     * @param remainingRegion remaining region of the hyperplane
+     */
+    protected AbstractSubHyperplane(final Hyperplane<S> hyperplane,
+                                    final Region<T> remainingRegion) {
+        this.hyperplane      = hyperplane;
+        this.remainingRegion = remainingRegion;
+    }
+
+    /** Build a sub-hyperplane from an hyperplane and a region.
+     * @param hyper underlying hyperplane
+     * @param remaining remaining region of the hyperplane
+     * @return a new sub-hyperplane
+     */
+    protected abstract AbstractSubHyperplane<S, T> buildNew(final Hyperplane<S> hyper,
+                                                            final Region<T> remaining);
+
+    /** {@inheritDoc} */
+    @Override
+    public AbstractSubHyperplane<S, T> copySelf() {
+        return buildNew(hyperplane.copySelf(), remainingRegion);
+    }
+
+    /** Get the underlying hyperplane.
+     * @return underlying hyperplane
+     */
+    @Override
+    public Hyperplane<S> getHyperplane() {
+        return hyperplane;
+    }
+
+    /** Get the remaining region of the hyperplane.
+     * <p>The returned region is expressed in the canonical hyperplane
+     * frame and has the hyperplane dimension. For example a chopped
+     * hyperplane in the 3D euclidean is a 2D plane and the
+     * corresponding region is a convex 2D polygon.</p>
+     * @return remaining region of the hyperplane
+     */
+    public Region<T> getRemainingRegion() {
+        return remainingRegion;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getSize() {
+        return remainingRegion.getSize();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public AbstractSubHyperplane<S, T> reunite(final SubHyperplane<S> other) {
+        @SuppressWarnings("unchecked")
+        AbstractSubHyperplane<S, T> o = (AbstractSubHyperplane<S, T>) other;
+        return buildNew(hyperplane,
+                        new RegionFactory<T>().union(remainingRegion, o.remainingRegion));
+    }
+
+    /** Apply a transform to the instance.
+     * <p>The instance must be a (D-1)-dimension sub-hyperplane with
+     * respect to the transform <em>not</em> a (D-2)-dimension
+     * sub-hyperplane the transform knows how to transform by
+     * itself. The transform will consist in transforming first the
+     * hyperplane and then the all region using the various methods
+     * provided by the transform.</p>
+     * @param transform D-dimension transform to apply
+     * @return the transformed instance
+     */
+    public AbstractSubHyperplane<S, T> applyTransform(final Transform<S, T> transform) {
+        final Hyperplane<S> tHyperplane = transform.apply(hyperplane);
+
+        // transform the tree, except for boundary attribute splitters
+        final Map<BSPTree<T>, BSPTree<T>> map = new HashMap<>();
+        final BSPTree<T> tTree =
+            recurseTransform(remainingRegion.getTree(false), tHyperplane, transform, map);
+
+        // set up the boundary attributes splitters
+        for (final Map.Entry<BSPTree<T>, BSPTree<T>> entry : map.entrySet()) {
+            if (entry.getKey().getCut() != null) {
+                @SuppressWarnings("unchecked")
+                BoundaryAttribute<T> original = (BoundaryAttribute<T>) entry.getKey().getAttribute();
+                if (original != null) {
+                    @SuppressWarnings("unchecked")
+                    BoundaryAttribute<T> transformed = (BoundaryAttribute<T>) entry.getValue().getAttribute();
+                    for (final BSPTree<T> splitter : original.getSplitters()) {
+                        transformed.getSplitters().add(map.get(splitter));
+                    }
+                }
+            }
+        }
+
+        return buildNew(tHyperplane, remainingRegion.buildNew(tTree));
+
+    }
+
+    /** Recursively transform a BSP-tree from a sub-hyperplane.
+     * @param node current BSP tree node
+     * @param transformed image of the instance hyperplane by the transform
+     * @param transform transform to apply
+     * @param map transformed nodes map
+     * @return a new tree
+     */
+    private BSPTree<T> recurseTransform(final BSPTree<T> node,
+                                        final Hyperplane<S> transformed,
+                                        final Transform<S, T> transform,
+                                        final Map<BSPTree<T>, BSPTree<T>> map) {
+
+        final BSPTree<T> transformedNode;
+        if (node.getCut() == null) {
+            transformedNode = new BSPTree<>(node.getAttribute());
+        } else {
+
+            @SuppressWarnings("unchecked")
+            BoundaryAttribute<T> attribute = (BoundaryAttribute<T>) node.getAttribute();
+            if (attribute != null) {
+                final SubHyperplane<T> tPO = (attribute.getPlusOutside() == null) ?
+                    null : transform.apply(attribute.getPlusOutside(), hyperplane, transformed);
+                final SubHyperplane<T> tPI = (attribute.getPlusInside() == null) ?
+                    null : transform.apply(attribute.getPlusInside(), hyperplane, transformed);
+                // we start with an empty list of splitters, it will be filled in out of recursion
+                attribute = new BoundaryAttribute<>(tPO, tPI, new NodesSet<T>());
+            }
+
+            transformedNode = new BSPTree<>(transform.apply(node.getCut(), hyperplane, transformed),
+                    recurseTransform(node.getPlus(),  transformed, transform, map),
+                    recurseTransform(node.getMinus(), transformed, transform, map),
+                    attribute);
+        }
+
+        map.put(node, transformedNode);
+        return transformedNode;
+
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public abstract SplitSubHyperplane<S> split(Hyperplane<S> hyper);
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isEmpty() {
+        return remainingRegion.isEmpty();
+    }
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BSPTree.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BSPTree.java
new file mode 100644
index 0000000..cd57774
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BSPTree.java
@@ -0,0 +1,775 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+
+/** This class represent a Binary Space Partition tree.
+
+ * <p>BSP trees are an efficient way to represent space partitions and
+ * to associate attributes with each cell. Each node in a BSP tree
+ * represents a convex region which is partitioned in two convex
+ * sub-regions at each side of a cut hyperplane. The root tree
+ * contains the complete space.</p>
+
+ * <p>The main use of such partitions is to use a boolean attribute to
+ * define an inside/outside property, hence representing arbitrary
+ * polytopes (line segments in 1D, polygons in 2D and polyhedrons in
+ * 3D) and to operate on them.</p>
+
+ * <p>Another example would be to represent Voronoi tesselations, the
+ * attribute of each cell holding the defining point of the cell.</p>
+
+ * <p>The application-defined attributes are shared among copied
+ * instances and propagated to split parts. These attributes are not
+ * used by the BSP-tree algorithms themselves, so the application can
+ * use them for any purpose. Since the tree visiting method holds
+ * internal and leaf nodes differently, it is possible to use
+ * different classes for internal nodes attributes and leaf nodes
+ * attributes. This should be used with care, though, because if the
+ * tree is modified in any way after attributes have been set, some
+ * internal nodes may become leaf nodes and some leaf nodes may become
+ * internal nodes.</p>
+
+ * <p>One of the main sources for the development of this package was
+ * Bruce Naylor, John Amanatides and William Thibault paper <a
+ * href="http://www.cs.yorku.ca/~amana/research/bsptSetOp.pdf";>Merging
+ * BSP Trees Yields Polyhedral Set Operations</a> Proc. Siggraph '90,
+ * Computer Graphics 24(4), August 1990, pp 115-124, published by the
+ * Association for Computing Machinery (ACM).</p>
+
+ * @param <S> Type of the space.
+ */
+public class BSPTree<S extends Space> {
+
+    /** Cut sub-hyperplane. */
+    private SubHyperplane<S> cut;
+
+    /** Tree at the plus side of the cut hyperplane. */
+    private BSPTree<S> plus;
+
+    /** Tree at the minus side of the cut hyperplane. */
+    private BSPTree<S> minus;
+
+    /** Parent tree. */
+    private BSPTree<S> parent;
+
+    /** Application-defined attribute. */
+    private Object attribute;
+
+    /** Build a tree having only one root cell representing the whole space.
+     */
+    public BSPTree() {
+        cut       = null;
+        plus      = null;
+        minus     = null;
+        parent    = null;
+        attribute = null;
+    }
+
+    /** Build a tree having only one root cell representing the whole space.
+     * @param attribute attribute of the tree (may be null)
+     */
+    public BSPTree(final Object attribute) {
+        cut    = null;
+        plus   = null;
+        minus  = null;
+        parent = null;
+        this.attribute = attribute;
+    }
+
+    /** Build a BSPTree from its underlying elements.
+     * <p>This method does <em>not</em> perform any verification on
+     * consistency of its arguments, it should therefore be used only
+     * when then caller knows what it is doing.</p>
+     * <p>This method is mainly useful to build trees
+     * bottom-up. Building trees top-down is realized with the help of
+     * method {@link #insertCut insertCut}.</p>
+     * @param cut cut sub-hyperplane for the tree
+     * @param plus plus side sub-tree
+     * @param minus minus side sub-tree
+     * @param attribute attribute associated with the node (may be null)
+     * @see #insertCut
+     */
+    public BSPTree(final SubHyperplane<S> cut, final BSPTree<S> plus, final BSPTree<S> minus,
+                   final Object attribute) {
+        this.cut       = cut;
+        this.plus      = plus;
+        this.minus     = minus;
+        this.parent    = null;
+        this.attribute = attribute;
+        plus.parent    = this;
+        minus.parent   = this;
+    }
+
+    /** Insert a cut sub-hyperplane in a node.
+     * <p>The sub-tree starting at this node will be completely
+     * overwritten. The new cut sub-hyperplane will be built from the
+     * intersection of the provided hyperplane with the cell. If the
+     * hyperplane does intersect the cell, the cell will have two
+     * children cells with {@code null} attributes on each side of
+     * the inserted cut sub-hyperplane. If the hyperplane does not
+     * intersect the cell then <em>no</em> cut hyperplane will be
+     * inserted and the cell will be changed to a leaf cell. The
+     * attribute of the node is never changed.</p>
+     * <p>This method is mainly useful when called on leaf nodes
+     * (i.e. nodes for which {@link #getCut getCut} returns
+     * {@code null}), in this case it provides a way to build a
+     * tree top-down (whereas the {@link #BSPTree(SubHyperplane,
+     * BSPTree, BSPTree, Object) 4 arguments constructor} is devoted to
+     * build trees bottom-up).</p>
+     * @param hyperplane hyperplane to insert, it will be chopped in
+     * order to fit in the cell defined by the parent nodes of the
+     * instance
+     * @return true if a cut sub-hyperplane has been inserted (i.e. if
+     * the cell now has two leaf child nodes)
+     * @see #BSPTree(SubHyperplane, BSPTree, BSPTree, Object)
+     */
+    public boolean insertCut(final Hyperplane<S> hyperplane) {
+
+        if (cut != null) {
+            plus.parent  = null;
+            minus.parent = null;
+        }
+
+        final SubHyperplane<S> chopped = fitToCell(hyperplane.wholeHyperplane());
+        if (chopped == null || chopped.isEmpty()) {
+            cut          = null;
+            plus         = null;
+            minus        = null;
+            return false;
+        }
+
+        cut          = chopped;
+        plus         = new BSPTree<>();
+        plus.parent  = this;
+        minus        = new BSPTree<>();
+        minus.parent = this;
+        return true;
+
+    }
+
+    /** Copy the instance.
+     * <p>The instance created is completely independent of the original
+     * one. A deep copy is used, none of the underlying objects are
+     * shared (except for the nodes attributes and immutable
+     * objects).</p>
+     * @return a new tree, copy of the instance
+     */
+    public BSPTree<S> copySelf() {
+
+        if (cut == null) {
+            return new BSPTree<>(attribute);
+        }
+
+        return new BSPTree<>(cut.copySelf(), plus.copySelf(), minus.copySelf(),
+                           attribute);
+
+    }
+
+    /** Get the cut sub-hyperplane.
+     * @return cut sub-hyperplane, null if this is a leaf tree
+     */
+    public SubHyperplane<S> getCut() {
+        return cut;
+    }
+
+    /** Get the tree on the plus side of the cut hyperplane.
+     * @return tree on the plus side of the cut hyperplane, null if this
+     * is a leaf tree
+     */
+    public BSPTree<S> getPlus() {
+        return plus;
+    }
+
+    /** Get the tree on the minus side of the cut hyperplane.
+     * @return tree on the minus side of the cut hyperplane, null if this
+     * is a leaf tree
+     */
+    public BSPTree<S> getMinus() {
+        return minus;
+    }
+
+    /** Get the parent node.
+     * @return parent node, null if the node has no parents
+     */
+    public BSPTree<S> getParent() {
+        return parent;
+    }
+
+    /** Associate an attribute with the instance.
+     * @param attribute attribute to associate with the node
+     * @see #getAttribute
+     */
+    public void setAttribute(final Object attribute) {
+        this.attribute = attribute;
+    }
+
+    /** Get the attribute associated with the instance.
+     * @return attribute associated with the node or null if no
+     * attribute has been explicitly set using the {@link #setAttribute
+     * setAttribute} method
+     * @see #setAttribute
+     */
+    public Object getAttribute() {
+        return attribute;
+    }
+
+    /** Visit the BSP tree nodes.
+     * @param visitor object visiting the tree nodes
+     */
+    public void visit(final BSPTreeVisitor<S> visitor) {
+        if (cut == null) {
+            visitor.visitLeafNode(this);
+        } else {
+            switch (visitor.visitOrder(this)) {
+            case PLUS_MINUS_SUB:
+                plus.visit(visitor);
+                minus.visit(visitor);
+                visitor.visitInternalNode(this);
+                break;
+            case PLUS_SUB_MINUS:
+                plus.visit(visitor);
+                visitor.visitInternalNode(this);
+                minus.visit(visitor);
+                break;
+            case MINUS_PLUS_SUB:
+                minus.visit(visitor);
+                plus.visit(visitor);
+                visitor.visitInternalNode(this);
+                break;
+            case MINUS_SUB_PLUS:
+                minus.visit(visitor);
+                visitor.visitInternalNode(this);
+                plus.visit(visitor);
+                break;
+            case SUB_PLUS_MINUS:
+                visitor.visitInternalNode(this);
+                plus.visit(visitor);
+                minus.visit(visitor);
+                break;
+            case SUB_MINUS_PLUS:
+                visitor.visitInternalNode(this);
+                minus.visit(visitor);
+                plus.visit(visitor);
+                break;
+            }
+
+        }
+    }
+
+    /** Fit a sub-hyperplane inside the cell defined by the instance.
+     * <p>Fitting is done by chopping off the parts of the
+     * sub-hyperplane that lie outside of the cell using the
+     * cut-hyperplanes of the parent nodes of the instance.</p>
+     * @param sub sub-hyperplane to fit
+     * @return a new sub-hyperplane, guaranteed to have no part outside
+     * of the instance cell
+     */
+    private SubHyperplane<S> fitToCell(final SubHyperplane<S> sub) {
+        SubHyperplane<S> s = sub;
+        for (BSPTree<S> tree = this; tree.parent != null && s != null; tree = tree.parent) {
+            if (tree == tree.parent.plus) {
+                s = s.split(tree.parent.cut.getHyperplane()).getPlus();
+            } else {
+                s = s.split(tree.parent.cut.getHyperplane()).getMinus();
+            }
+        }
+        return s;
+    }
+
+    /** Get the cell to which a point belongs.
+     * <p>If the returned cell is a leaf node the points belongs to the
+     * interior of the node, if the cell is an internal node the points
+     * belongs to the node cut sub-hyperplane.</p>
+     * @param point point to check
+     * @param tolerance tolerance below which points close to a cut hyperplane
+     * are considered to belong to the hyperplane itself
+     * @return the tree cell to which the point belongs
+     */
+    public BSPTree<S> getCell(final Point<S> point, final double tolerance) {
+
+        if (cut == null) {
+            return this;
+        }
+
+        // position of the point with respect to the cut hyperplane
+        final double offset = cut.getHyperplane().getOffset(point);
+
+        if (Math.abs(offset) < tolerance) {
+            return this;
+        } else if (offset <= 0) {
+            // point is on the minus side of the cut hyperplane
+            return minus.getCell(point, tolerance);
+        } else {
+            // point is on the plus side of the cut hyperplane
+            return plus.getCell(point, tolerance);
+        }
+
+    }
+
+    /** Get the cells whose cut sub-hyperplanes are close to the point.
+     * @param point point to check
+     * @param maxOffset offset below which a cut sub-hyperplane is considered
+     * close to the point (in absolute value)
+     * @return close cells (may be empty if all cut sub-hyperplanes are farther
+     * than maxOffset from the point)
+     */
+    public List<BSPTree<S>> getCloseCuts(final Point<S> point, final double maxOffset) {
+        final List<BSPTree<S>> close = new ArrayList<>();
+        recurseCloseCuts(point, maxOffset, close);
+        return close;
+    }
+
+    /** Get the cells whose cut sub-hyperplanes are close to the point.
+     * @param point point to check
+     * @param maxOffset offset below which a cut sub-hyperplane is considered
+     * close to the point (in absolute value)
+     * @param close list to fill
+     */
+    private void recurseCloseCuts(final Point<S> point, final double maxOffset,
+                                  final List<BSPTree<S>> close) {
+        if (cut != null) {
+
+            // position of the point with respect to the cut hyperplane
+            final double offset = cut.getHyperplane().getOffset(point);
+
+            if (offset < -maxOffset) {
+                // point is on the minus side of the cut hyperplane
+                minus.recurseCloseCuts(point, maxOffset, close);
+            } else if (offset > maxOffset) {
+                // point is on the plus side of the cut hyperplane
+                plus.recurseCloseCuts(point, maxOffset, close);
+            } else {
+                // point is close to the cut hyperplane
+                close.add(this);
+                minus.recurseCloseCuts(point, maxOffset, close);
+                plus.recurseCloseCuts(point, maxOffset, close);
+            }
+
+        }
+    }
+
+    /** Perform condensation on a tree.
+     * <p>The condensation operation is not recursive, it must be called
+     * explicitly from leaves to root.</p>
+     */
+    private void condense() {
+        if ((cut != null) && (plus.cut == null) && (minus.cut == null) &&
+            (((plus.attribute == null) && (minus.attribute == null)) ||
+             ((plus.attribute != null) && plus.attribute.equals(minus.attribute)))) {
+            attribute = (plus.attribute == null) ? minus.attribute : plus.attribute;
+            cut       = null;
+            plus      = null;
+            minus     = null;
+        }
+    }
+
+    /** Merge a BSP tree with the instance.
+     * <p>All trees are modified (parts of them are reused in the new
+     * tree), it is the responsibility of the caller to ensure a copy
+     * has been done before if any of the former tree should be
+     * preserved, <em>no</em> such copy is done here!</p>
+     * <p>The algorithm used here is directly derived from the one
+     * described in the Naylor, Amanatides and Thibault paper (section
+     * III, Binary Partitioning of a BSP Tree).</p>
+     * @param tree other tree to merge with the instance (will be
+     * <em>unusable</em> after the operation, as well as the
+     * instance itself)
+     * @param leafMerger object implementing the final merging phase
+     * (this is where the semantic of the operation occurs, generally
+     * depending on the attribute of the leaf node)
+     * @return a new tree, result of <code>instance &lt;op&gt;
+     * tree</code>, this value can be ignored if parentTree is not null
+     * since all connections have already been established
+     */
+    public BSPTree<S> merge(final BSPTree<S> tree, final LeafMerger<S> leafMerger) {
+        return merge(tree, leafMerger, null, false);
+    }
+
+    /** Merge a BSP tree with the instance.
+     * @param tree other tree to merge with the instance (will be
+     * <em>unusable</em> after the operation, as well as the
+     * instance itself)
+     * @param leafMerger object implementing the final merging phase
+     * (this is where the semantic of the operation occurs, generally
+     * depending on the attribute of the leaf node)
+     * @param parentTree parent tree to connect to (may be null)
+     * @param isPlusChild if true and if parentTree is not null, the
+     * resulting tree should be the plus child of its parent, ignored if
+     * parentTree is null
+     * @return a new tree, result of <code>instance &lt;op&gt;
+     * tree</code>, this value can be ignored if parentTree is not null
+     * since all connections have already been established
+     */
+    private BSPTree<S> merge(final BSPTree<S> tree, final LeafMerger<S> leafMerger,
+                             final BSPTree<S> parentTree, final boolean isPlusChild) {
+        if (cut == null) {
+            // cell/tree operation
+            return leafMerger.merge(this, tree, parentTree, isPlusChild, true);
+        } else if (tree.cut == null) {
+            // tree/cell operation
+            return leafMerger.merge(tree, this, parentTree, isPlusChild, false);
+        } else {
+            // tree/tree operation
+            final BSPTree<S> merged = tree.split(cut);
+            if (parentTree != null) {
+                merged.parent = parentTree;
+                if (isPlusChild) {
+                    parentTree.plus = merged;
+                } else {
+                    parentTree.minus = merged;
+                }
+            }
+
+            // merging phase
+            plus.merge(merged.plus, leafMerger, merged, true);
+            minus.merge(merged.minus, leafMerger, merged, false);
+            merged.condense();
+            if (merged.cut != null) {
+                merged.cut = merged.fitToCell(merged.cut.getHyperplane().wholeHyperplane());
+            }
+
+            return merged;
+
+        }
+    }
+
+    /** This interface gather the merging operations between a BSP tree
+     * leaf and another BSP tree.
+     * <p>As explained in Bruce Naylor, John Amanatides and William
+     * Thibault paper <a
+     * href="http://www.cs.yorku.ca/~amana/research/bsptSetOp.pdf";>Merging
+     * BSP Trees Yields Polyhedral Set Operations</a>,
+     * the operations on {@link BSPTree BSP trees} can be expressed as a
+     * generic recursive merging operation where only the final part,
+     * when one of the operand is a leaf, is specific to the real
+     * operation semantics. For example, a tree representing a region
+     * using a boolean attribute to identify inside cells and outside
+     * cells would use four different objects to implement the final
+     * merging phase of the four set operations union, intersection,
+     * difference and symmetric difference (exclusive or).</p>
+     * @param <S> Type of the space.
+     */
+    public interface LeafMerger<S extends Space> {
+
+        /** Merge a leaf node and a tree node.
+         * <p>This method is called at the end of a recursive merging
+         * resulting from a {@code tree1.merge(tree2, leafMerger)}
+         * call, when one of the sub-trees involved is a leaf (i.e. when
+         * its cut-hyperplane is null). This is the only place where the
+         * precise semantics of the operation are required. For all upper
+         * level nodes in the tree, the merging operation is only a
+         * generic partitioning algorithm.</p>
+         * <p>Since the final operation may be non-commutative, it is
+         * important to know if the leaf node comes from the instance tree
+         * ({@code tree1}) or the argument tree
+         * ({@code tree2}). The third argument of the method is
+         * devoted to this. It can be ignored for commutative
+         * operations.</p>
+         * <p>The {@link BSPTree#insertInTree BSPTree.insertInTree} method
+         * may be useful to implement this method.</p>
+         * @param leaf leaf node (its cut hyperplane is guaranteed to be
+         * null)
+         * @param tree tree node (its cut hyperplane may be null or not)
+         * @param parentTree parent tree to connect to (may be null)
+         * @param isPlusChild if true and if parentTree is not null, the
+         * resulting tree should be the plus child of its parent, ignored if
+         * parentTree is null
+         * @param leafFromInstance if true, the leaf node comes from the
+         * instance tree ({@code tree1}) and the tree node comes from
+         * the argument tree ({@code tree2})
+         * @return the BSP tree resulting from the merging (may be one of
+         * the arguments)
+         */
+        BSPTree<S> merge(BSPTree<S> leaf, BSPTree<S> tree, BSPTree<S> parentTree,
+                         boolean isPlusChild, boolean leafFromInstance);
+
+    }
+
+    /** This interface handles the corner cases when an internal node cut sub-hyperplane vanishes.
+     * <p>
+     * Such cases happens for example when a cut sub-hyperplane is inserted into
+     * another tree (during a merge operation), and is split in several parts,
+     * some of which becomes smaller than the tolerance. The corresponding node
+     * as then no cut sub-hyperplane anymore, but does have children. This interface
+     * specifies how to handle this situation.
+     * setting
+     * </p>
+     * @param <S> Type of the space.
+     */
+    public interface VanishingCutHandler<S extends Space> {
+
+        /** Fix a node with both vanished cut and children.
+         * @param node node to fix
+         * @return fixed node
+         */
+        BSPTree<S> fixNode(BSPTree<S> node);
+
+    }
+
+    /** Split a BSP tree by an external sub-hyperplane.
+     * <p>Split a tree in two halves, on each side of the
+     * sub-hyperplane. The instance is not modified.</p>
+     * <p>The tree returned is not upward-consistent: despite all of its
+     * sub-trees cut sub-hyperplanes (including its own cut
+     * sub-hyperplane) are bounded to the current cell, it is <em>not</em>
+     * attached to any parent tree yet. This tree is intended to be
+     * later inserted into an higher level tree.</p>
+     * <p>The algorithm used here is the one given in Naylor, Amanatides
+     * and Thibault paper (section III, Binary Partitioning of a BSP
+     * Tree).</p>
+     * @param sub partitioning sub-hyperplane, must be already clipped
+     * to the convex region represented by the instance, will be used as
+     * the cut sub-hyperplane of the returned tree
+     * @return a tree having the specified sub-hyperplane as its cut
+     * sub-hyperplane, the two parts of the split instance as its two
+     * sub-trees and a null parent
+     */
+    public BSPTree<S> split(final SubHyperplane<S> sub) {
+
+        if (cut == null) {
+            return new BSPTree<>(sub, copySelf(), new BSPTree<S>(attribute), null);
+        }
+
+        final Hyperplane<S> cHyperplane = cut.getHyperplane();
+        final Hyperplane<S> sHyperplane = sub.getHyperplane();
+        final SubHyperplane.SplitSubHyperplane<S> subParts = sub.split(cHyperplane);
+        switch (subParts.getSide()) {
+        case PLUS :
+        { // the partitioning sub-hyperplane is entirely in the plus sub-tree
+            final BSPTree<S> split = plus.split(sub);
+            if (cut.split(sHyperplane).getSide() == Side.PLUS) {
+                split.plus =
+                    new BSPTree<>(cut.copySelf(), split.plus, minus.copySelf(), attribute);
+                split.plus.condense();
+                split.plus.parent = split;
+            } else {
+                split.minus =
+                    new BSPTree<>(cut.copySelf(), split.minus, minus.copySelf(), attribute);
+                split.minus.condense();
+                split.minus.parent = split;
+            }
+            return split;
+        }
+        case MINUS :
+        { // the partitioning sub-hyperplane is entirely in the minus sub-tree
+            final BSPTree<S> split = minus.split(sub);
+            if (cut.split(sHyperplane).getSide() == Side.PLUS) {
+                split.plus =
+                    new BSPTree<>(cut.copySelf(), plus.copySelf(), split.plus, attribute);
+                split.plus.condense();
+                split.plus.parent = split;
+            } else {
+                split.minus =
+                    new BSPTree<>(cut.copySelf(), plus.copySelf(), split.minus, attribute);
+                split.minus.condense();
+                split.minus.parent = split;
+            }
+            return split;
+        }
+        case BOTH :
+        {
+            final SubHyperplane.SplitSubHyperplane<S> cutParts = cut.split(sHyperplane);
+            final BSPTree<S> split =
+                new BSPTree<>(sub, plus.split(subParts.getPlus()), minus.split(subParts.getMinus()),
+                               null);
+            split.plus.cut          = cutParts.getPlus();
+            split.minus.cut         = cutParts.getMinus();
+            final BSPTree<S> tmp    = split.plus.minus;
+            split.plus.minus        = split.minus.plus;
+            split.plus.minus.parent = split.plus;
+            split.minus.plus        = tmp;
+            split.minus.plus.parent = split.minus;
+            split.plus.condense();
+            split.minus.condense();
+            return split;
+        }
+        default :
+            return cHyperplane.sameOrientationAs(sHyperplane) ?
+                   new BSPTree<>(sub, plus.copySelf(),  minus.copySelf(), attribute) :
+                   new BSPTree<>(sub, minus.copySelf(), plus.copySelf(),  attribute);
+        }
+
+    }
+
+    /** Insert the instance into another tree.
+     * <p>The instance itself is modified so its former parent should
+     * not be used anymore.</p>
+     * @param parentTree parent tree to connect to (may be null)
+     * @param isPlusChild if true and if parentTree is not null, the
+     * resulting tree should be the plus child of its parent, ignored if
+     * parentTree is null
+     * @param vanishingHandler handler to use for handling very rare corner
+     * cases of vanishing cut sub-hyperplanes in internal nodes during merging
+     * @see LeafMerger
+     */
+    public void insertInTree(final BSPTree<S> parentTree, final boolean isPlusChild,
+                             final VanishingCutHandler<S> vanishingHandler) {
+
+        // set up parent/child links
+        parent = parentTree;
+        if (parentTree != null) {
+            if (isPlusChild) {
+                parentTree.plus = this;
+            } else {
+                parentTree.minus = this;
+            }
+        }
+
+        // make sure the inserted tree lies in the cell defined by its parent nodes
+        if (cut != null) {
+
+            // explore the parent nodes from here towards tree root
+            for (BSPTree<S> tree = this; tree.parent != null; tree = tree.parent) {
+
+                // this is an hyperplane of some parent node
+                final Hyperplane<S> hyperplane = tree.parent.cut.getHyperplane();
+
+                // chop off the parts of the inserted tree that extend
+                // on the wrong side of this parent hyperplane
+                if (tree == tree.parent.plus) {
+                    cut = cut.split(hyperplane).getPlus();
+                    plus.chopOffMinus(hyperplane, vanishingHandler);
+                    minus.chopOffMinus(hyperplane, vanishingHandler);
+                } else {
+                    cut = cut.split(hyperplane).getMinus();
+                    plus.chopOffPlus(hyperplane, vanishingHandler);
+                    minus.chopOffPlus(hyperplane, vanishingHandler);
+                }
+
+                if (cut == null) {
+                    // the cut sub-hyperplane has vanished
+                    final BSPTree<S> fixed = vanishingHandler.fixNode(this);
+                    cut       = fixed.cut;
+                    plus      = fixed.plus;
+                    minus     = fixed.minus;
+                    attribute = fixed.attribute;
+                    if (cut == null) {
+                        break;
+                    }
+                }
+
+            }
+
+            // since we may have drop some parts of the inserted tree,
+            // perform a condensation pass to keep the tree structure simple
+            condense();
+
+        }
+
+    }
+
+    /** Prune a tree around a cell.
+     * <p>
+     * This method can be used to extract a convex cell from a tree.
+     * The original cell may either be a leaf node or an internal node.
+     * If it is an internal node, it's subtree will be ignored (i.e. the
+     * extracted cell will be a leaf node in all cases). The original
+     * tree to which the original cell belongs is not touched at all,
+     * a new independent tree will be built.
+     * </p>
+     * @param cellAttribute attribute to set for the leaf node
+     * corresponding to the initial instance cell
+     * @param otherLeafsAttributes attribute to set for the other leaf
+     * nodes
+     * @param internalAttributes attribute to set for the internal nodes
+     * @return a new tree (the original tree is left untouched) containing
+     * a single branch with the cell as a leaf node, and other leaf nodes
+     * as the remnants of the pruned branches
+     */
+    public BSPTree<S> pruneAroundConvexCell(final Object cellAttribute,
+                                            final Object otherLeafsAttributes,
+                                            final Object internalAttributes) {
+
+        // build the current cell leaf
+        BSPTree<S> tree = new BSPTree<>(cellAttribute);
+
+        // build the pruned tree bottom-up
+        for (BSPTree<S> current = this; current.parent != null; current = current.parent) {
+            final SubHyperplane<S> parentCut = current.parent.cut.copySelf();
+            final BSPTree<S>       sibling   = new BSPTree<>(otherLeafsAttributes);
+            if (current == current.parent.plus) {
+                tree = new BSPTree<>(parentCut, tree, sibling, internalAttributes);
+            } else {
+                tree = new BSPTree<>(parentCut, sibling, tree, internalAttributes);
+            }
+        }
+
+        return tree;
+
+    }
+
+    /** Chop off parts of the tree.
+     * <p>The instance is modified in place, all the parts that are on
+     * the minus side of the chopping hyperplane are discarded, only the
+     * parts on the plus side remain.</p>
+     * @param hyperplane chopping hyperplane
+     * @param vanishingHandler handler to use for handling very rare corner
+     * cases of vanishing cut sub-hyperplanes in internal nodes during merging
+     */
+    private void chopOffMinus(final Hyperplane<S> hyperplane, final VanishingCutHandler<S> vanishingHandler) {
+        if (cut != null) {
+
+            cut = cut.split(hyperplane).getPlus();
+            plus.chopOffMinus(hyperplane, vanishingHandler);
+            minus.chopOffMinus(hyperplane, vanishingHandler);
+
+            if (cut == null) {
+                // the cut sub-hyperplane has vanished
+                final BSPTree<S> fixed = vanishingHandler.fixNode(this);
+                cut       = fixed.cut;
+                plus      = fixed.plus;
+                minus     = fixed.minus;
+                attribute = fixed.attribute;
+            }
+
+        }
+    }
+
+    /** Chop off parts of the tree.
+     * <p>The instance is modified in place, all the parts that are on
+     * the plus side of the chopping hyperplane are discarded, only the
+     * parts on the minus side remain.</p>
+     * @param hyperplane chopping hyperplane
+     * @param vanishingHandler handler to use for handling very rare corner
+     * cases of vanishing cut sub-hyperplanes in internal nodes during merging
+     */
+    private void chopOffPlus(final Hyperplane<S> hyperplane, final VanishingCutHandler<S> vanishingHandler) {
+        if (cut != null) {
+
+            cut = cut.split(hyperplane).getMinus();
+            plus.chopOffPlus(hyperplane, vanishingHandler);
+            minus.chopOffPlus(hyperplane, vanishingHandler);
+
+            if (cut == null) {
+                // the cut sub-hyperplane has vanished
+                final BSPTree<S> fixed = vanishingHandler.fixNode(this);
+                cut       = fixed.cut;
+                plus      = fixed.plus;
+                minus     = fixed.minus;
+                attribute = fixed.attribute;
+            }
+
+        }
+    }
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BSPTreeVisitor.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BSPTreeVisitor.java
new file mode 100644
index 0000000..f7bbdbb
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BSPTreeVisitor.java
@@ -0,0 +1,112 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import org.apache.commons.geometry.core.Space;
+
+/** This interface is used to visit {@link BSPTree BSP tree} nodes.
+
+ * <p>Navigation through {@link BSPTree BSP trees} can be done using
+ * two different point of views:</p>
+ * <ul>
+ *   <li>
+ *     the first one is in a node-oriented way using the {@link
+ *     BSPTree#getPlus}, {@link BSPTree#getMinus} and {@link
+ *     BSPTree#getParent} methods. Terminal nodes without associated
+ *     {@link SubHyperplane sub-hyperplanes} can be visited this way,
+ *     there is no constraint in the visit order, and it is possible
+ *     to visit either all nodes or only a subset of the nodes
+ *   </li>
+ *   <li>
+ *     the second one is in a sub-hyperplane-oriented way using
+ *     classes implementing this interface which obeys the visitor
+ *     design pattern. The visit order is provided by the visitor as
+ *     each node is first encountered. Each node is visited exactly
+ *     once.
+ *   </li>
+ * </ul>
+
+ * @param <S> Type of the space.
+
+ * @see BSPTree
+ * @see SubHyperplane
+ */
+public interface BSPTreeVisitor<S extends Space> {
+
+    /** Enumerate for visit order with respect to plus sub-tree, minus sub-tree and cut sub-hyperplane. */
+    enum Order {
+        /** Indicator for visit order plus sub-tree, then minus sub-tree,
+         * and last cut sub-hyperplane.
+         */
+        PLUS_MINUS_SUB,
+
+        /** Indicator for visit order plus sub-tree, then cut sub-hyperplane,
+         * and last minus sub-tree.
+         */
+        PLUS_SUB_MINUS,
+
+        /** Indicator for visit order minus sub-tree, then plus sub-tree,
+         * and last cut sub-hyperplane.
+         */
+        MINUS_PLUS_SUB,
+
+        /** Indicator for visit order minus sub-tree, then cut sub-hyperplane,
+         * and last plus sub-tree.
+         */
+        MINUS_SUB_PLUS,
+
+        /** Indicator for visit order cut sub-hyperplane, then plus sub-tree,
+         * and last minus sub-tree.
+         */
+        SUB_PLUS_MINUS,
+
+        /** Indicator for visit order cut sub-hyperplane, then minus sub-tree,
+         * and last plus sub-tree.
+         */
+        SUB_MINUS_PLUS;
+    }
+
+    /** Determine the visit order for this node.
+     * <p>Before attempting to visit an internal node, this method is
+     * called to determine the desired ordering of the visit. It is
+     * guaranteed that this method will be called before {@link
+     * #visitInternalNode visitInternalNode} for a given node, it will be
+     * called exactly once for each internal node.</p>
+     * @param node BSP node guaranteed to have a non null cut sub-hyperplane
+     * @return desired visit order, must be one of
+     * {@link Order#PLUS_MINUS_SUB}, {@link Order#PLUS_SUB_MINUS},
+     * {@link Order#MINUS_PLUS_SUB}, {@link Order#MINUS_SUB_PLUS},
+     * {@link Order#SUB_PLUS_MINUS}, {@link Order#SUB_MINUS_PLUS}
+     */
+    Order visitOrder(BSPTree<S> node);
+
+    /** Visit a BSP tree node node having a non-null sub-hyperplane.
+     * <p>It is guaranteed that this method will be called after {@link
+     * #visitOrder visitOrder} has been called for a given node,
+     * it wil be called exactly once for each internal node.</p>
+     * @param node BSP node guaranteed to have a non null cut sub-hyperplane
+     * @see #visitLeafNode
+     */
+    void visitInternalNode(BSPTree<S> node);
+
+    /** Visit a leaf BSP tree node node having a null sub-hyperplane.
+     * @param node leaf BSP node having a null sub-hyperplane
+     * @see #visitInternalNode
+     */
+    void visitLeafNode(BSPTree<S> node);
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryAttribute.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryAttribute.java
new file mode 100644
index 0000000..ad6a365
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryAttribute.java
@@ -0,0 +1,97 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import org.apache.commons.geometry.core.Space;
+
+/** Class holding boundary attributes.
+ * <p>This class is used for the attributes associated with the
+ * nodes of region boundary shell trees returned by the {@link
+ * Region#getTree(boolean) Region.getTree(includeBoundaryAttributes)}
+ * when the boolean {@code includeBoundaryAttributes} parameter is
+ * set to {@code true}. It contains the parts of the node cut
+ * sub-hyperplane that belong to the boundary.</p>
+ * <p>This class is a simple placeholder, it does not provide any
+ * processing methods.</p>
+ * @param <S> Type of the space.
+ * @see Region#getTree
+ */
+public class BoundaryAttribute<S extends Space> {
+
+    /** Part of the node cut sub-hyperplane that belongs to the
+     * boundary and has the outside of the region on the plus side of
+     * its underlying hyperplane (may be null).
+     */
+    private final SubHyperplane<S> plusOutside;
+
+    /** Part of the node cut sub-hyperplane that belongs to the
+     * boundary and has the inside of the region on the plus side of
+     * its underlying hyperplane (may be null).
+     */
+    private final SubHyperplane<S> plusInside;
+
+    /** Sub-hyperplanes that were used to split the boundary part. */
+    private final NodesSet<S> splitters;
+
+    /** Simple constructor.
+     * @param plusOutside part of the node cut sub-hyperplane that
+     * belongs to the boundary and has the outside of the region on
+     * the plus side of its underlying hyperplane (may be null)
+     * @param plusInside part of the node cut sub-hyperplane that
+     * belongs to the boundary and has the inside of the region on the
+     * plus side of its underlying hyperplane (may be null)
+     * @param splitters sub-hyperplanes that were used to
+     * split the boundary part (may be null)
+     */
+    BoundaryAttribute(final SubHyperplane<S> plusOutside,
+                      final SubHyperplane<S> plusInside,
+                      final NodesSet<S> splitters) {
+        this.plusOutside = plusOutside;
+        this.plusInside  = plusInside;
+        this.splitters   = splitters;
+    }
+
+    /** Get the part of the node cut sub-hyperplane that belongs to the
+     * boundary and has the outside of the region on the plus side of
+     * its underlying hyperplane.
+     * @return part of the node cut sub-hyperplane that belongs to the
+     * boundary and has the outside of the region on the plus side of
+     * its underlying hyperplane
+     */
+    public SubHyperplane<S> getPlusOutside() {
+        return plusOutside;
+    }
+
+    /** Get the part of the node cut sub-hyperplane that belongs to the
+     * boundary and has the inside of the region on the plus side of
+     * its underlying hyperplane.
+     * @return part of the node cut sub-hyperplane that belongs to the
+     * boundary and has the inside of the region on the plus side of
+     * its underlying hyperplane
+     */
+    public SubHyperplane<S> getPlusInside() {
+        return plusInside;
+    }
+
+    /** Get the sub-hyperplanes that were used to split the boundary part.
+     * @return sub-hyperplanes that were used to split the boundary part
+     */
+    public NodesSet<S> getSplitters() {
+        return splitters;
+    }
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryBuilder.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryBuilder.java
new file mode 100644
index 0000000..816d3c2
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryBuilder.java
@@ -0,0 +1,97 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import org.apache.commons.geometry.core.Space;
+
+/** Visitor building boundary shell tree.
+ * <p>
+ * The boundary shell is represented as {@link BoundaryAttribute boundary attributes}
+ * at each internal node.
+ * </p>
+ * @param <S> Type of the space.
+ */
+class BoundaryBuilder<S extends Space> implements BSPTreeVisitor<S> {
+
+    /** {@inheritDoc} */
+    @Override
+    public Order visitOrder(BSPTree<S> node) {
+        return Order.PLUS_MINUS_SUB;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void visitInternalNode(BSPTree<S> node) {
+
+        SubHyperplane<S> plusOutside = null;
+        SubHyperplane<S> plusInside  = null;
+        NodesSet<S>      splitters   = null;
+
+        // characterize the cut sub-hyperplane,
+        // first with respect to the plus sub-tree
+        final Characterization<S> plusChar = new Characterization<>(node.getPlus(), node.getCut().copySelf());
+
+        if (plusChar.touchOutside()) {
+            // plusChar.outsideTouching() corresponds to a subset of the cut sub-hyperplane
+            // known to have outside cells on its plus side, we want to check if parts
+            // of this subset do have inside cells on their minus side
+            final Characterization<S> minusChar = new Characterization<>(node.getMinus(), plusChar.outsideTouching());
+            if (minusChar.touchInside()) {
+                // this part belongs to the boundary,
+                // it has the outside on its plus side and the inside on its minus side
+                plusOutside = minusChar.insideTouching();
+                splitters = new NodesSet<>();
+                splitters.addAll(minusChar.getInsideSplitters());
+                splitters.addAll(plusChar.getOutsideSplitters());
+            }
+        }
+
+        if (plusChar.touchInside()) {
+            // plusChar.insideTouching() corresponds to a subset of the cut sub-hyperplane
+            // known to have inside cells on its plus side, we want to check if parts
+            // of this subset do have outside cells on their minus side
+            final Characterization<S> minusChar = new Characterization<>(node.getMinus(), plusChar.insideTouching());
+            if (minusChar.touchOutside()) {
+                // this part belongs to the boundary,
+                // it has the inside on its plus side and the outside on its minus side
+                plusInside = minusChar.outsideTouching();
+                if (splitters == null) {
+                    splitters = new NodesSet<>();
+                }
+                splitters.addAll(minusChar.getOutsideSplitters());
+                splitters.addAll(plusChar.getInsideSplitters());
+            }
+        }
+
+        if (splitters != null) {
+            // the parent nodes are natural splitters for boundary sub-hyperplanes
+            for (BSPTree<S> up = node.getParent(); up != null; up = up.getParent()) {
+                splitters.add(up);
+            }
+        }
+
+        // set the boundary attribute at non-leaf nodes
+        node.setAttribute(new BoundaryAttribute<>(plusOutside, plusInside, splitters));
+
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void visitLeafNode(BSPTree<S> node) {
+    }
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryProjection.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryProjection.java
new file mode 100644
index 0000000..1d5254d
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryProjection.java
@@ -0,0 +1,82 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+
+/** Class holding the result of point projection on region boundary.
+ * <p>This class is a simple placeholder, it does not provide any
+ * processing methods.</p>
+ * <p>Instances of this class are guaranteed to be immutable</p>
+ * @param <S> Type of the space.
+ * @see AbstractRegion#projectToBoundary(Point)
+ */
+public class BoundaryProjection<S extends Space> {
+
+    /** Original point. */
+    private final Point<S> original;
+
+    /** Projected point. */
+    private final Point<S> projected;
+
+    /** Offset of the point with respect to the boundary it is projected on. */
+    private final double offset;
+
+    /** Constructor from raw elements.
+     * @param original original point
+     * @param projected projected point
+     * @param offset offset of the point with respect to the boundary it is projected on
+     */
+    public BoundaryProjection(final Point<S> original, final Point<S> projected, final double offset) {
+        this.original  = original;
+        this.projected = projected;
+        this.offset    = offset;
+    }
+
+    /** Get the original point.
+     * @return original point
+     */
+    public Point<S> getOriginal() {
+        return original;
+    }
+
+    /** Projected point.
+     * @return projected point, or null if there are no boundary
+     */
+    public Point<S> getProjected() {
+        return projected;
+    }
+
+    /** Offset of the point with respect to the boundary it is projected on.
+     * <p>
+     * The offset with respect to the boundary is negative if the {@link
+     * #getOriginal() original point} is inside the region, and positive otherwise.
+     * </p>
+     * <p>
+     * If there are no boundary, the value is set to either {@code
+     * Double.POSITIVE_INFINITY} if the region is empty (i.e. all points are
+     * outside of the region) or {@code Double.NEGATIVE_INFINITY} if the region
+     * covers the whole space (i.e. all points are inside of the region).
+     * </p>
+     * @return offset of the point with respect to the boundary it is projected on
+     */
+    public double getOffset() {
+        return offset;
+    }
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryProjector.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryProjector.java
new file mode 100644
index 0000000..390695c
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundaryProjector.java
@@ -0,0 +1,201 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+import org.apache.commons.geometry.core.partitioning.Region.Location;
+
+/** Local tree visitor to compute projection on boundary.
+ * @param <S> Type of the space.
+ * @param <T> Type of the sub-space.
+ */
+class BoundaryProjector<S extends Space, T extends Space> implements BSPTreeVisitor<S> {
+
+    /** Original point. */
+    private final Point<S> original;
+
+    /** Current best projected point. */
+    private Point<S> projected;
+
+    /** Leaf node closest to the test point. */
+    private BSPTree<S> leaf;
+
+    /** Current offset. */
+    private double offset;
+
+    /** Simple constructor.
+     * @param original original point
+     */
+    BoundaryProjector(final Point<S> original) {
+        this.original  = original;
+        this.projected = null;
+        this.leaf      = null;
+        this.offset    = Double.POSITIVE_INFINITY;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Order visitOrder(final BSPTree<S> node) {
+        // we want to visit the tree so that the first encountered
+        // leaf is the one closest to the test point
+        if (node.getCut().getHyperplane().getOffset(original) <= 0) {
+            return Order.MINUS_SUB_PLUS;
+        } else {
+            return Order.PLUS_SUB_MINUS;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void visitInternalNode(final BSPTree<S> node) {
+
+        // project the point on the cut sub-hyperplane
+        final Hyperplane<S> hyperplane = node.getCut().getHyperplane();
+        final double signedOffset = hyperplane.getOffset(original);
+        if (Math.abs(signedOffset) < offset) {
+
+            // project point
+            final Point<S> regular = hyperplane.project(original);
+
+            // get boundary parts
+            final List<Region<T>> boundaryParts = boundaryRegions(node);
+
+            // check if regular projection really belongs to the boundary
+            boolean regularFound = false;
+            for (final Region<T> part : boundaryParts) {
+                if (!regularFound && belongsToPart(regular, hyperplane, part)) {
+                    // the projected point lies in the boundary
+                    projected    = regular;
+                    offset       = Math.abs(signedOffset);
+                    regularFound = true;
+                }
+            }
+
+            if (!regularFound) {
+                // the regular projected point is not on boundary,
+                // so we have to check further if a singular point
+                // (i.e. a vertex in 2D case) is a possible projection
+                for (final Region<T> part : boundaryParts) {
+                    final Point<S> spI = singularProjection(regular, hyperplane, part);
+                    if (spI != null) {
+                        final double distance = original.distance(spI);
+                        if (distance < offset) {
+                            projected = spI;
+                            offset    = distance;
+                        }
+                    }
+                }
+
+            }
+
+        }
+
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void visitLeafNode(final BSPTree<S> node) {
+        if (leaf == null) {
+            // this is the first leaf we visit,
+            // it is the closest one to the original point
+            leaf = node;
+        }
+    }
+
+    /** Get the projection.
+     * @return projection
+     */
+    public BoundaryProjection<S> getProjection() {
+
+        // fix offset sign
+        offset = Math.copySign(offset, (Boolean) leaf.getAttribute() ? -1 : +1);
+
+        return new BoundaryProjection<>(original, projected, offset);
+
+    }
+
+    /** Extract the regions of the boundary on an internal node.
+     * @param node internal node
+     * @return regions in the node sub-hyperplane
+     */
+    private List<Region<T>> boundaryRegions(final BSPTree<S> node) {
+
+        final List<Region<T>> regions = new ArrayList<>(2);
+
+        @SuppressWarnings("unchecked")
+        final BoundaryAttribute<S> ba = (BoundaryAttribute<S>) node.getAttribute();
+        addRegion(ba.getPlusInside(),  regions);
+        addRegion(ba.getPlusOutside(), regions);
+
+        return regions;
+
+    }
+
+    /** Add a boundary region to a list.
+     * @param sub sub-hyperplane defining the region
+     * @param list to fill up
+     */
+    private void addRegion(final SubHyperplane<S> sub, final List<Region<T>> list) {
+        if (sub != null) {
+            @SuppressWarnings("unchecked")
+            final Region<T> region = ((AbstractSubHyperplane<S, T>) sub).getRemainingRegion();
+            if (region != null) {
+                list.add(region);
+            }
+        }
+    }
+
+    /** Check if a projected point lies on a boundary part.
+     * @param point projected point to check
+     * @param hyperplane hyperplane into which the point was projected
+     * @param part boundary part
+     * @return true if point lies on the boundary part
+     */
+    private boolean belongsToPart(final Point<S> point, final Hyperplane<S> hyperplane,
+                                  final Region<T> part) {
+
+        // there is a non-null sub-space, we can dive into smaller dimensions
+        @SuppressWarnings("unchecked")
+        final Embedding<S, T> embedding = (Embedding<S, T>) hyperplane;
+        return part.checkPoint(embedding.toSubSpace(point)) != Location.OUTSIDE;
+
+    }
+
+    /** Get the projection to the closest boundary singular point.
+     * @param point projected point to check
+     * @param hyperplane hyperplane into which the point was projected
+     * @param part boundary part
+     * @return projection to a singular point of boundary part (may be null)
+     */
+    private Point<S> singularProjection(final Point<S> point, final Hyperplane<S> hyperplane,
+                                        final Region<T> part) {
+
+        // there is a non-null sub-space, we can dive into smaller dimensions
+        @SuppressWarnings("unchecked")
+        final Embedding<S, T> embedding = (Embedding<S, T>) hyperplane;
+        final BoundaryProjection<T> bp = part.projectToBoundary(embedding.toSubSpace(point));
+
+        // back to initial dimension
+        return (bp.getProjected() == null) ? null : embedding.toSpace(bp.getProjected());
+
+    }
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundarySizeVisitor.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundarySizeVisitor.java
new file mode 100644
index 0000000..b305a36
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/BoundarySizeVisitor.java
@@ -0,0 +1,67 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import org.apache.commons.geometry.core.Space;
+
+/** Visitor computing the boundary size.
+ * @param <S> Type of the space.
+ */
+class BoundarySizeVisitor<S extends Space> implements BSPTreeVisitor<S> {
+
+    /** Size of the boundary. */
+    private double boundarySize;
+
+    /** Simple constructor.
+     */
+    BoundarySizeVisitor() {
+        boundarySize = 0;
+    }
+
+    /** {@inheritDoc}*/
+    @Override
+    public Order visitOrder(final BSPTree<S> node) {
+        return Order.MINUS_SUB_PLUS;
+    }
+
+    /** {@inheritDoc}*/
+    @Override
+    public void visitInternalNode(final BSPTree<S> node) {
+        @SuppressWarnings("unchecked")
+        final BoundaryAttribute<S> attribute =
+            (BoundaryAttribute<S>) node.getAttribute();
+        if (attribute.getPlusOutside() != null) {
+            boundarySize += attribute.getPlusOutside().getSize();
+        }
+        if (attribute.getPlusInside() != null) {
+            boundarySize += attribute.getPlusInside().getSize();
+        }
+    }
+
+    /** {@inheritDoc}*/
+    @Override
+    public void visitLeafNode(final BSPTree<S> node) {
+    }
+
+    /** Get the size of the boundary.
+     * @return size of the boundary
+     */
+    public double getSize() {
+        return boundarySize;
+    }
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Characterization.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Characterization.java
new file mode 100644
index 0000000..7184c96
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Characterization.java
@@ -0,0 +1,196 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Space;
+
+/** Cut sub-hyperplanes characterization with respect to inside/outside cells.
+ * @see BoundaryBuilder
+ * @param <S> Type of the space.
+ */
+class Characterization<S extends Space> {
+
+    /** Part of the cut sub-hyperplane that touch outside cells. */
+    private SubHyperplane<S> outsideTouching;
+
+    /** Part of the cut sub-hyperplane that touch inside cells. */
+    private SubHyperplane<S> insideTouching;
+
+    /** Nodes that were used to split the outside touching part. */
+    private final NodesSet<S> outsideSplitters;
+
+    /** Nodes that were used to split the outside touching part. */
+    private final NodesSet<S> insideSplitters;
+
+    /** Simple constructor.
+     * <p>Characterization consists in splitting the specified
+     * sub-hyperplane into several parts lying in inside and outside
+     * cells of the tree. The principle is to compute characterization
+     * twice for each cut sub-hyperplane in the tree, once on the plus
+     * node and once on the minus node. The parts that have the same flag
+     * (inside/inside or outside/outside) do not belong to the boundary
+     * while parts that have different flags (inside/outside or
+     * outside/inside) do belong to the boundary.</p>
+     * @param node current BSP tree node
+     * @param sub sub-hyperplane to characterize
+     */
+    Characterization(final BSPTree<S> node, final SubHyperplane<S> sub) {
+        outsideTouching  = null;
+        insideTouching   = null;
+        outsideSplitters = new NodesSet<>();
+        insideSplitters  = new NodesSet<>();
+        characterize(node, sub, new ArrayList<BSPTree<S>>());
+    }
+
+    /** Filter the parts of an hyperplane belonging to the boundary.
+     * <p>The filtering consist in splitting the specified
+     * sub-hyperplane into several parts lying in inside and outside
+     * cells of the tree. The principle is to call this method twice for
+     * each cut sub-hyperplane in the tree, once on the plus node and
+     * once on the minus node. The parts that have the same flag
+     * (inside/inside or outside/outside) do not belong to the boundary
+     * while parts that have different flags (inside/outside or
+     * outside/inside) do belong to the boundary.</p>
+     * @param node current BSP tree node
+     * @param sub sub-hyperplane to characterize
+     * @param splitters nodes that did split the current one
+     */
+    private void characterize(final BSPTree<S> node, final SubHyperplane<S> sub,
+                              final List<BSPTree<S>> splitters) {
+        if (node.getCut() == null) {
+            // we have reached a leaf node
+            final boolean inside = (Boolean) node.getAttribute();
+            if (inside) {
+                addInsideTouching(sub, splitters);
+            } else {
+                addOutsideTouching(sub, splitters);
+            }
+        } else {
+            final Hyperplane<S> hyperplane = node.getCut().getHyperplane();
+            final SubHyperplane.SplitSubHyperplane<S> split = sub.split(hyperplane);
+            switch (split.getSide()) {
+            case PLUS:
+                characterize(node.getPlus(),  sub, splitters);
+                break;
+            case MINUS:
+                characterize(node.getMinus(), sub, splitters);
+                break;
+            case BOTH:
+                splitters.add(node);
+                characterize(node.getPlus(),  split.getPlus(),  splitters);
+                characterize(node.getMinus(), split.getMinus(), splitters);
+                splitters.remove(splitters.size() - 1);
+                break;
+            default:
+                // If we reach this point, then the sub-hyperplane we're
+                // testing lies directly on this node's hyperplane. In theory,
+                // this shouldn't ever happen with correctly-formed trees. However,
+                // this does actually occur in practice, especially with manually
+                // built trees or very complex models. Rather than throwing an
+                // exception, we'll attempt to handle this situation gracefully
+                // by treating these sub-hyperplanes as if they lie on the minus
+                // side of the cut hyperplane.
+                characterize(node.getMinus(), sub, splitters);
+                break;
+            }
+        }
+    }
+
+    /** Add a part of the cut sub-hyperplane known to touch an outside cell.
+     * @param sub part of the cut sub-hyperplane known to touch an outside cell
+     * @param splitters sub-hyperplanes that did split the current one
+     */
+    private void addOutsideTouching(final SubHyperplane<S> sub,
+                                    final List<BSPTree<S>> splitters) {
+        if (outsideTouching == null) {
+            outsideTouching = sub;
+        } else {
+            outsideTouching = outsideTouching.reunite(sub);
+        }
+        outsideSplitters.addAll(splitters);
+    }
+
+    /** Add a part of the cut sub-hyperplane known to touch an inside cell.
+     * @param sub part of the cut sub-hyperplane known to touch an inside cell
+     * @param splitters sub-hyperplanes that did split the current one
+     */
+    private void addInsideTouching(final SubHyperplane<S> sub,
+                                   final List<BSPTree<S>> splitters) {
+        if (insideTouching == null) {
+            insideTouching = sub;
+        } else {
+            insideTouching = insideTouching.reunite(sub);
+        }
+        insideSplitters.addAll(splitters);
+    }
+
+    /** Check if the cut sub-hyperplane touches outside cells.
+     * @return true if the cut sub-hyperplane touches outside cells
+     */
+    public boolean touchOutside() {
+        return outsideTouching != null && !outsideTouching.isEmpty();
+    }
+
+    /** Get all the parts of the cut sub-hyperplane known to touch outside cells.
+     * @return parts of the cut sub-hyperplane known to touch outside cells
+     * (may be null or empty)
+     */
+    public SubHyperplane<S> outsideTouching() {
+        return outsideTouching;
+    }
+
+    /** Get the nodes that were used to split the outside touching part.
+     * <p>
+     * Splitting nodes are internal nodes (i.e. they have a non-null
+     * cut sub-hyperplane).
+     * </p>
+     * @return nodes that were used to split the outside touching part
+     */
+    public NodesSet<S> getOutsideSplitters() {
+        return outsideSplitters;
+    }
+
+    /** Check if the cut sub-hyperplane touches inside cells.
+     * @return true if the cut sub-hyperplane touches inside cells
+     */
+    public boolean touchInside() {
+        return insideTouching != null && !insideTouching.isEmpty();
+    }
+
+    /** Get all the parts of the cut sub-hyperplane known to touch inside cells.
+     * @return parts of the cut sub-hyperplane known to touch inside cells
+     * (may be null or empty)
+     */
+    public SubHyperplane<S> insideTouching() {
+        return insideTouching;
+    }
+
+    /** Get the nodes that were used to split the inside touching part.
+     * <p>
+     * Splitting nodes are internal nodes (i.e. they have a non-null
+     * cut sub-hyperplane).
+     * </p>
+     * @return nodes that were used to split the inside touching part
+     */
+    public NodesSet<S> getInsideSplitters() {
+        return insideSplitters;
+    }
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Embedding.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Embedding.java
new file mode 100644
index 0000000..7ed9ef5
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Embedding.java
@@ -0,0 +1,67 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+
+/** This interface defines mappers between a space and one of its sub-spaces.
+
+ * <p>Sub-spaces are the lower dimensions subsets of a n-dimensions
+ * space. The (n-1)-dimension sub-spaces are specific sub-spaces known
+ * as {@link Hyperplane hyperplanes}. This interface can be used regardless
+ * of the dimensions differences. As an example, {@link
+ * org.apache.commons.geometry.euclidean.threed.Line Line} in 3D
+ * implements Embedding&lt;{@link
+ * org.apache.commons.geometry.euclidean.threed.Cartesian3D Cartesian3D}, {@link
+ * org.apache.commons.geometry.euclidean.oned.Cartesian1D Cartesian1D}&gt;, i.e. it
+ * maps directly dimensions 3 and 1.</p>
+
+ * <p>In the 3D euclidean space, hyperplanes are 2D planes, and the 1D
+ * sub-spaces are lines.</p>
+
+ * <p>
+ * Note that this interface is <em>not</em> intended to be implemented
+ * by Apache Commons Math users, it is only intended to be implemented
+ * within the library itself. New methods may be added even for minor
+ * versions, which breaks compatibility for external implementations.
+ * </p>
+
+ * @param <S> Type of the embedding space.
+ * @param <T> Type of the embedded sub-space.
+
+ * @see Hyperplane
+ */
+public interface Embedding<S extends Space, T extends Space> {
+
+    /** Transform a space point into a sub-space point.
+     * @param point n-dimension point of the space
+     * @return (n-1)-dimension point of the sub-space corresponding to
+     * the specified space point
+     * @see #toSpace
+     */
+    Point<T> toSubSpace(Point<S> point);
+
+    /** Transform a sub-space point into a space point.
+     * @param point (n-1)-dimension point of the sub-space
+     * @return n-dimension point of the space corresponding to the
+     * specified sub-space point
+     * @see #toSubSpace
+     */
+    Point<S> toSpace(Point<T> point);
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Hyperplane.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Hyperplane.java
new file mode 100644
index 0000000..8041658
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Hyperplane.java
@@ -0,0 +1,94 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+
+/** This interface represents an hyperplane of a space.
+
+ * <p>The most prominent place where hyperplane appears in space
+ * partitioning is as cutters. Each partitioning node in a {@link
+ * BSPTree BSP tree} has a cut {@link SubHyperplane sub-hyperplane}
+ * which is either an hyperplane or a part of an hyperplane. In an
+ * n-dimensions euclidean space, an hyperplane is an (n-1)-dimensions
+ * hyperplane (for example a traditional plane in the 3D euclidean
+ * space). They can be more exotic objects in specific fields, for
+ * example a circle on the surface of the unit sphere.</p>
+
+ * <p>
+ * Note that this interface is <em>not</em> intended to be implemented
+ * by Apache Commons Math users, it is only intended to be implemented
+ * within the library itself. New methods may be added even for minor
+ * versions, which breaks compatibility for external implementations.
+ * </p>
+
+ * @param <S> Type of the space.
+ */
+public interface Hyperplane<S extends Space> {
+
+    /** Copy the instance.
+     * <p>The instance created is completely independant of the original
+     * one. A deep copy is used, none of the underlying objects are
+     * shared (except for immutable objects).</p>
+     * @return a new hyperplane, copy of the instance
+     */
+    Hyperplane<S> copySelf();
+
+    /** Get the offset (oriented distance) of a point.
+     * <p>The offset is 0 if the point is on the underlying hyperplane,
+     * it is positive if the point is on one particular side of the
+     * hyperplane, and it is negative if the point is on the other side,
+     * according to the hyperplane natural orientation.</p>
+     * @param point point to check
+     * @return offset of the point
+     */
+    double getOffset(Point<S> point);
+
+    /** Project a point to the hyperplane.
+     * @param point point to project
+     * @return projected point
+     */
+    Point<S> project(Point<S> point);
+
+    /** Get the tolerance below which points are considered to belong to the hyperplane.
+     * @return tolerance below which points are considered to belong to the hyperplane
+     */
+    double getTolerance();
+
+    /** Check if the instance has the same orientation as another hyperplane.
+     * <p>This method is expected to be called on parallel hyperplanes. The
+     * method should <em>not</em> re-check for parallelism, only for
+     * orientation, typically by testing something like the sign of the
+     * dot-products of normals.</p>
+     * @param other other hyperplane to check against the instance
+     * @return true if the instance and the other hyperplane have
+     * the same orientation
+     */
+    boolean sameOrientationAs(Hyperplane<S> other);
+
+    /** Build a sub-hyperplane covering the whole hyperplane.
+     * @return a sub-hyperplane covering the whole hyperplane
+     */
+    SubHyperplane<S> wholeHyperplane();
+
+    /** Build a region covering the whole space.
+     * @return a region containing the instance
+     */
+    Region<S> wholeSpace();
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/InsideFinder.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/InsideFinder.java
new file mode 100644
index 0000000..aec8a41
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/InsideFinder.java
@@ -0,0 +1,149 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import org.apache.commons.geometry.core.Space;
+
+/** Utility class checking if inside nodes can be found
+ * on the plus and minus sides of an hyperplane.
+ * @param <S> Type of the space.
+ */
+class InsideFinder<S extends Space> {
+
+    /** Region on which to operate. */
+    private final Region<S> region;
+
+    /** Indicator of inside leaf nodes found on the plus side. */
+    private boolean plusFound;
+
+    /** Indicator of inside leaf nodes found on the plus side. */
+    private boolean minusFound;
+
+    /** Simple constructor.
+     * @param region region on which to operate
+     */
+    InsideFinder(final Region<S> region) {
+        this.region = region;
+        plusFound  = false;
+        minusFound = false;
+    }
+
+    /** Search recursively for inside leaf nodes on each side of the given hyperplane.
+
+     * <p>The algorithm used here is directly derived from the one
+     * described in section III (<i>Binary Partitioning of a BSP
+     * Tree</i>) of the Bruce Naylor, John Amanatides and William
+     * Thibault paper <a
+     * href="http://www.cs.yorku.ca/~amana/research/bsptSetOp.pdf";>Merging
+     * BSP Trees Yields Polyhedral Set Operations</a> Proc. Siggraph
+     * '90, Computer Graphics 24(4), August 1990, pp 115-124, published
+     * by the Association for Computing Machinery (ACM)..</p>
+
+     * @param node current BSP tree node
+     * @param sub sub-hyperplane
+     */
+    public void recurseSides(final BSPTree<S> node, final SubHyperplane<S> sub) {
+
+        if (node.getCut() == null) {
+            if ((Boolean) node.getAttribute()) {
+                // this is an inside cell expanding across the hyperplane
+                plusFound  = true;
+                minusFound = true;
+            }
+            return;
+        }
+
+        final Hyperplane<S> hyperplane = node.getCut().getHyperplane();
+        final SubHyperplane.SplitSubHyperplane<S> split = sub.split(hyperplane);
+        switch (split.getSide()) {
+        case PLUS :
+            // the sub-hyperplane is entirely in the plus sub-tree
+            if (node.getCut().split(sub.getHyperplane()).getSide() == Side.PLUS) {
+                if (!region.isEmpty(node.getMinus())) {
+                    plusFound  = true;
+                }
+            } else {
+                if (!region.isEmpty(node.getMinus())) {
+                    minusFound = true;
+                }
+            }
+            if (!(plusFound && minusFound)) {
+                recurseSides(node.getPlus(), sub);
+            }
+            break;
+        case MINUS :
+            // the sub-hyperplane is entirely in the minus sub-tree
+            if (node.getCut().split(sub.getHyperplane()).getSide() == Side.PLUS) {
+                if (!region.isEmpty(node.getPlus())) {
+                    plusFound  = true;
+                }
+            } else {
+                if (!region.isEmpty(node.getPlus())) {
+                    minusFound = true;
+                }
+            }
+            if (!(plusFound && minusFound)) {
+                recurseSides(node.getMinus(), sub);
+            }
+            break;
+        case BOTH :
+            // the sub-hyperplane extends in both sub-trees
+
+            // explore first the plus sub-tree
+            recurseSides(node.getPlus(), split.getPlus());
+
+            // if needed, explore the minus sub-tree
+            if (!(plusFound && minusFound)) {
+                recurseSides(node.getMinus(), split.getMinus());
+            }
+            break;
+        default :
+            // the sub-hyperplane and the cut sub-hyperplane share the same hyperplane
+            if (node.getCut().getHyperplane().sameOrientationAs(sub.getHyperplane())) {
+                if ((node.getPlus().getCut() != null) || ((Boolean) node.getPlus().getAttribute())) {
+                    plusFound  = true;
+                }
+                if ((node.getMinus().getCut() != null) || ((Boolean) node.getMinus().getAttribute())) {
+                    minusFound = true;
+                }
+            } else {
+                if ((node.getPlus().getCut() != null) || ((Boolean) node.getPlus().getAttribute())) {
+                    minusFound = true;
+                }
+                if ((node.getMinus().getCut() != null) || ((Boolean) node.getMinus().getAttribute())) {
+                    plusFound  = true;
+                }
+            }
+        }
+
+    }
+
+    /** Check if inside leaf nodes have been found on the plus side.
+     * @return true if inside leaf nodes have been found on the plus side
+     */
+    public boolean plusFound() {
+        return plusFound;
+    }
+
+    /** Check if inside leaf nodes have been found on the minus side.
+     * @return true if inside leaf nodes have been found on the minus side
+     */
+    public boolean minusFound() {
+        return minusFound;
+    }
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/NodesSet.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/NodesSet.java
new file mode 100644
index 0000000..20ef6b7
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/NodesSet.java
@@ -0,0 +1,72 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Space;
+
+/** Set of {@link BSPTree BSP tree} nodes.
+ * @see BoundaryAttribute
+ * @param <S> Type of the space.
+ */
+public class NodesSet<S extends Space> implements Iterable<BSPTree<S>> {
+
+    /** List of sub-hyperplanes. */
+    private final List<BSPTree<S>> list;
+
+    /** Simple constructor.
+     */
+    public NodesSet() {
+        list = new ArrayList<>();
+    }
+
+    /** Add a node if not already known.
+     * @param node node to add
+     */
+    public void add(final BSPTree<S> node) {
+
+        for (final BSPTree<S> existing : list) {
+            if (node == existing) {
+                // the node is already known, don't add it
+                return;
+            }
+        }
+
+        // the node was not known, add it
+        list.add(node);
+
+    }
+
+    /** Add nodes if they are not already known.
+     * @param iterator nodes iterator
+     */
+    public void addAll(final Iterable<BSPTree<S>> iterator) {
+        for (final BSPTree<S> node : iterator) {
+            add(node);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Iterator<BSPTree<S>> iterator() {
+        return list.iterator();
+    }
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Region.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Region.java
new file mode 100644
index 0000000..63155a5
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Region.java
@@ -0,0 +1,205 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+
+/** This interface represents a region of a space as a partition.
+
+ * <p>Region are subsets of a space, they can be infinite (whole
+ * space, half space, infinite stripe ...) or finite (polygons in 2D,
+ * polyhedrons in 3D ...). Their main characteristic is to separate
+ * points that are considered to be <em>inside</em> the region from
+ * points considered to be <em>outside</em> of it. In between, there
+ * may be points on the <em>boundary</em> of the region.</p>
+
+ * <p>This implementation is limited to regions for which the boundary
+ * is composed of several {@link SubHyperplane sub-hyperplanes},
+ * including regions with no boundary at all: the whole space and the
+ * empty region. They are not necessarily finite and not necessarily
+ * path-connected. They can contain holes.</p>
+
+ * <p>Regions can be combined using the traditional sets operations :
+ * union, intersection, difference and symetric difference (exclusive
+ * or) for the binary operations, complement for the unary
+ * operation.</p>
+
+ * <p>
+ * Note that this interface is <em>not</em> intended to be implemented
+ * by Apache Commons Math users, it is only intended to be implemented
+ * within the library itself. New methods may be added even for minor
+ * versions, which breaks compatibility for external implementations.
+ * </p>
+
+ * @param <S> Type of the space.
+ */
+public interface Region<S extends Space> {
+
+    /** Enumerate for the location of a point with respect to the region. */
+    enum Location {
+        /** Code for points inside the partition. */
+        INSIDE,
+
+        /** Code for points outside of the partition. */
+        OUTSIDE,
+
+        /** Code for points on the partition boundary. */
+        BOUNDARY;
+    }
+
+    /** Build a region using the instance as a prototype.
+     * <p>This method allow to create new instances without knowing
+     * exactly the type of the region. It is an application of the
+     * prototype design pattern.</p>
+     * <p>The leaf nodes of the BSP tree <em>must</em> have a
+     * {@code Boolean} attribute representing the inside status of
+     * the corresponding cell (true for inside cells, false for outside
+     * cells). In order to avoid building too many small objects, it is
+     * recommended to use the predefined constants
+     * {@code Boolean.TRUE} and {@code Boolean.FALSE}. The
+     * tree also <em>must</em> have either null internal nodes or
+     * internal nodes representing the boundary as specified in the
+     * {@link #getTree getTree} method).</p>
+     * @param newTree inside/outside BSP tree representing the new region
+     * @return the built region
+     */
+    Region<S> buildNew(BSPTree<S> newTree);
+
+    /** Copy the instance.
+     * <p>The instance created is completely independant of the original
+     * one. A deep copy is used, none of the underlying objects are
+     * shared (except for the underlying tree {@code Boolean}
+     * attributes and immutable objects).</p>
+     * @return a new region, copy of the instance
+     */
+    Region<S> copySelf();
+
+    /** Check if the instance is empty.
+     * @return true if the instance is empty
+     */
+    boolean isEmpty();
+
+    /** Check if the sub-tree starting at a given node is empty.
+     * @param node root node of the sub-tree (<em>must</em> have {@link
+     * Region Region} tree semantics, i.e. the leaf nodes must have
+     * {@code Boolean} attributes representing an inside/outside
+     * property)
+     * @return true if the sub-tree starting at the given node is empty
+     */
+    boolean isEmpty(final BSPTree<S> node);
+
+    /** Check if the instance covers the full space.
+     * @return true if the instance covers the full space
+     */
+    boolean isFull();
+
+    /** Check if the sub-tree starting at a given node covers the full space.
+     * @param node root node of the sub-tree (<em>must</em> have {@link
+     * Region Region} tree semantics, i.e. the leaf nodes must have
+     * {@code Boolean} attributes representing an inside/outside
+     * property)
+     * @return true if the sub-tree starting at the given node covers the full space
+     */
+    boolean isFull(final BSPTree<S> node);
+
+    /** Check if the instance entirely contains another region.
+     * @param region region to check against the instance
+     * @return true if the instance contains the specified tree
+     */
+    boolean contains(final Region<S> region);
+
+    /** Check a point with respect to the region.
+     * @param point point to check
+     * @return a code representing the point status: either {@link
+     * Location#INSIDE}, {@link Location#OUTSIDE} or {@link Location#BOUNDARY}
+     */
+    Location checkPoint(final Point<S> point);
+
+    /** Project a point on the boundary of the region.
+     * @param point point to check
+     * @return projection of the point on the boundary
+     */
+    BoundaryProjection<S> projectToBoundary(final Point<S> point);
+
+    /** Get the underlying BSP tree.
+
+     * <p>Regions are represented by an underlying inside/outside BSP
+     * tree whose leaf attributes are {@code Boolean} instances
+     * representing inside leaf cells if the attribute value is
+     * {@code true} and outside leaf cells if the attribute is
+     * {@code false}. These leaf attributes are always present and
+     * guaranteed to be non null.</p>
+
+     * <p>In addition to the leaf attributes, the internal nodes which
+     * correspond to cells split by cut sub-hyperplanes may contain
+     * {@link BoundaryAttribute BoundaryAttribute} objects representing
+     * the parts of the corresponding cut sub-hyperplane that belong to
+     * the boundary. When the boundary attributes have been computed,
+     * all internal nodes are guaranteed to have non-null
+     * attributes, however some {@link BoundaryAttribute
+     * BoundaryAttribute} instances may have their {@link
+     * BoundaryAttribute#getPlusInside() getPlusInside} and {@link
+     * BoundaryAttribute#getPlusOutside() getPlusOutside} methods both
+     * returning null if the corresponding cut sub-hyperplane does not
+     * have any parts belonging to the boundary.</p>
+
+     * <p>Since computing the boundary is not always required and can be
+     * time-consuming for large trees, these internal nodes attributes
+     * are computed using lazy evaluation only when required by setting
+     * the {@code includeBoundaryAttributes} argument to
+     * {@code true}. Once computed, these attributes remain in the
+     * tree, which implies that in this case, further calls to the
+     * method for the same region will always include these attributes
+     * regardless of the value of the
+     * {@code includeBoundaryAttributes} argument.</p>
+
+     * @param includeBoundaryAttributes if true, the boundary attributes
+     * at internal nodes are guaranteed to be included (they may be
+     * included even if the argument is false, if they have already been
+     * computed due to a previous call)
+     * @return underlying BSP tree
+     * @see BoundaryAttribute
+     */
+    BSPTree<S> getTree(final boolean includeBoundaryAttributes);
+
+    /** Get the size of the boundary.
+     * @return the size of the boundary (this is 0 in 1D, a length in
+     * 2D, an area in 3D ...)
+     */
+    double getBoundarySize();
+
+    /** Get the size of the instance.
+     * @return the size of the instance (this is a length in 1D, an area
+     * in 2D, a volume in 3D ...)
+     */
+    double getSize();
+
+    /** Get the barycenter of the instance.
+     * @return an object representing the barycenter
+     */
+    Point<S> getBarycenter();
+
+    /** Get the parts of a sub-hyperplane that are contained in the region.
+     * <p>The parts of the sub-hyperplane that belong to the boundary are
+     * <em>not</em> included in the resulting parts.</p>
+     * @param sub sub-hyperplane traversing the region
+     * @return filtered sub-hyperplane
+     */
+    SubHyperplane<S> intersection(final SubHyperplane<S> sub);
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/RegionFactory.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/RegionFactory.java
new file mode 100644
index 0000000..61a888a
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/RegionFactory.java
@@ -0,0 +1,384 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+import org.apache.commons.geometry.core.partitioning.BSPTree.VanishingCutHandler;
+import org.apache.commons.geometry.core.partitioning.Region.Location;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane.SplitSubHyperplane;
+
+/** This class is a factory for {@link Region}.
+
+ * @param <S> Type of the space.
+ */
+public class RegionFactory<S extends Space> {
+
+    /** Visitor removing internal nodes attributes. */
+    private final NodesCleaner nodeCleaner;
+
+    /** Simple constructor.
+     */
+    public RegionFactory() {
+        nodeCleaner = new NodesCleaner();
+    }
+
+    /** Build a convex region from a collection of bounding hyperplanes.
+     * @param hyperplanes collection of bounding hyperplanes
+     * @return a new convex region, or null if the collection is empty
+     */
+    @SafeVarargs
+    public final Region<S> buildConvex(final Hyperplane<S> ... hyperplanes) {
+        if ((hyperplanes == null) || (hyperplanes.length == 0)) {
+            return null;
+        }
+
+        // use the first hyperplane to build the right class
+        final Region<S> region = hyperplanes[0].wholeSpace();
+
+        // chop off parts of the space
+        BSPTree<S> node = region.getTree(false);
+        node.setAttribute(Boolean.TRUE);
+        for (final Hyperplane<S> hyperplane : hyperplanes) {
+            if (node.insertCut(hyperplane)) {
+                node.setAttribute(null);
+                node.getPlus().setAttribute(Boolean.FALSE);
+                node = node.getMinus();
+                node.setAttribute(Boolean.TRUE);
+            } else {
+                // the hyperplane could not be inserted in the current leaf node
+                // either it is completely outside (which means the input hyperplanes
+                // are wrong), or it is parallel to a previous hyperplane
+                SubHyperplane<S> s = hyperplane.wholeHyperplane();
+                for (BSPTree<S> tree = node; tree.getParent() != null && s != null; tree = tree.getParent()) {
+                    final Hyperplane<S>         other = tree.getParent().getCut().getHyperplane();
+                    final SplitSubHyperplane<S> split = s.split(other);
+                    switch (split.getSide()) {
+                        case HYPER :
+                            // the hyperplane is parallel to a previous hyperplane
+                            if (!hyperplane.sameOrientationAs(other)) {
+                                // this hyperplane is opposite to the other one,
+                                // the region is thinner than the tolerance, we consider it empty
+                                return getComplement(hyperplanes[0].wholeSpace());
+                            }
+                            // the hyperplane is an extension of an already known hyperplane, we just ignore it
+                            break;
+                        case PLUS :
+                            // the hyperplane is outside of the current convex zone,
+                            // the input hyperplanes are inconsistent
+                            throw new IllegalArgumentException("Hyperplanes do not define a convex region");
+                        default :
+                            s = split.getMinus();
+                    }
+                }
+            }
+        }
+
+        return region;
+
+    }
+
+    /** Compute the union of two regions.
+     * @param region1 first region (will be unusable after the operation as
+     * parts of it will be reused in the new region)
+     * @param region2 second region (will be unusable after the operation as
+     * parts of it will be reused in the new region)
+     * @return a new region, result of {@code region1 union region2}
+     */
+    public Region<S> union(final Region<S> region1, final Region<S> region2) {
+        final BSPTree<S> tree =
+            region1.getTree(false).merge(region2.getTree(false), new UnionMerger());
+        tree.visit(nodeCleaner);
+        return region1.buildNew(tree);
+    }
+
+    /** Compute the intersection of two regions.
+     * @param region1 first region (will be unusable after the operation as
+     * parts of it will be reused in the new region)
+     * @param region2 second region (will be unusable after the operation as
+     * parts of it will be reused in the new region)
+     * @return a new region, result of {@code region1 intersection region2}
+     */
+    public Region<S> intersection(final Region<S> region1, final Region<S> region2) {
+        final BSPTree<S> tree =
+            region1.getTree(false).merge(region2.getTree(false), new IntersectionMerger());
+        tree.visit(nodeCleaner);
+        return region1.buildNew(tree);
+    }
+
+    /** Compute the symmetric difference (exclusive or) of two regions.
+     * @param region1 first region (will be unusable after the operation as
+     * parts of it will be reused in the new region)
+     * @param region2 second region (will be unusable after the operation as
+     * parts of it will be reused in the new region)
+     * @return a new region, result of {@code region1 xor region2}
+     */
+    public Region<S> xor(final Region<S> region1, final Region<S> region2) {
+        final BSPTree<S> tree =
+            region1.getTree(false).merge(region2.getTree(false), new XorMerger());
+        tree.visit(nodeCleaner);
+        return region1.buildNew(tree);
+    }
+
+    /** Compute the difference of two regions.
+     * @param region1 first region (will be unusable after the operation as
+     * parts of it will be reused in the new region)
+     * @param region2 second region (will be unusable after the operation as
+     * parts of it will be reused in the new region)
+     * @return a new region, result of {@code region1 minus region2}
+     */
+    public Region<S> difference(final Region<S> region1, final Region<S> region2) {
+        final BSPTree<S> tree =
+            region1.getTree(false).merge(region2.getTree(false), new DifferenceMerger(region1, region2));
+        tree.visit(nodeCleaner);
+        return region1.buildNew(tree);
+    }
+
+    /** Get the complement of the region (exchanged interior/exterior).
+     * @param region region to complement, it will not modified, a new
+     * region independent region will be built
+     * @return a new region, complement of the specified one
+     */
+    /** Get the complement of the region (exchanged interior/exterior).
+     * @param region region to complement, it will not modified, a new
+     * region independent region will be built
+     * @return a new region, complement of the specified one
+     */
+    public Region<S> getComplement(final Region<S> region) {
+        return region.buildNew(recurseComplement(region.getTree(false)));
+    }
+
+    /** Recursively build the complement of a BSP tree.
+     * @param node current node of the original tree
+     * @return new tree, complement of the node
+     */
+    private BSPTree<S> recurseComplement(final BSPTree<S> node) {
+
+        // transform the tree, except for boundary attribute splitters
+        final Map<BSPTree<S>, BSPTree<S>> map = new HashMap<>();
+        final BSPTree<S> transformedTree = recurseComplement(node, map);
+
+        // set up the boundary attributes splitters
+        for (final Map.Entry<BSPTree<S>, BSPTree<S>> entry : map.entrySet()) {
+            if (entry.getKey().getCut() != null) {
+                @SuppressWarnings("unchecked")
+                BoundaryAttribute<S> original = (BoundaryAttribute<S>) entry.getKey().getAttribute();
+                if (original != null) {
+                    @SuppressWarnings("unchecked")
+                    BoundaryAttribute<S> transformed = (BoundaryAttribute<S>) entry.getValue().getAttribute();
+                    for (final BSPTree<S> splitter : original.getSplitters()) {
+                        transformed.getSplitters().add(map.get(splitter));
+                    }
+                }
+            }
+        }
+
+        return transformedTree;
+
+    }
+
+    /** Recursively build the complement of a BSP tree.
+     * @param node current node of the original tree
+     * @param map transformed nodes map
+     * @return new tree, complement of the node
+     */
+    private BSPTree<S> recurseComplement(final BSPTree<S> node,
+                                         final Map<BSPTree<S>, BSPTree<S>> map) {
+
+        final BSPTree<S> transformedNode;
+        if (node.getCut() == null) {
+            transformedNode = new BSPTree<>(((Boolean) node.getAttribute()) ? Boolean.FALSE : Boolean.TRUE);
+        } else {
+
+            @SuppressWarnings("unchecked")
+            BoundaryAttribute<S> attribute = (BoundaryAttribute<S>) node.getAttribute();
+            if (attribute != null) {
+                final SubHyperplane<S> plusOutside =
+                        (attribute.getPlusInside() == null) ? null : attribute.getPlusInside().copySelf();
+                final SubHyperplane<S> plusInside  =
+                        (attribute.getPlusOutside() == null) ? null : attribute.getPlusOutside().copySelf();
+                // we start with an empty list of splitters, it will be filled in out of recursion
+                attribute = new BoundaryAttribute<>(plusOutside, plusInside, new NodesSet<S>());
+            }
+
+            transformedNode = new BSPTree<>(node.getCut().copySelf(),
+                                             recurseComplement(node.getPlus(),  map),
+                                             recurseComplement(node.getMinus(), map),
+                                             attribute);
+        }
+
+        map.put(node, transformedNode);
+        return transformedNode;
+
+    }
+
+    /** BSP tree leaf merger computing union of two regions. */
+    private class UnionMerger implements BSPTree.LeafMerger<S> {
+        /** {@inheritDoc} */
+        @Override
+        public BSPTree<S> merge(final BSPTree<S> leaf, final BSPTree<S> tree,
+                                final BSPTree<S> parentTree,
+                                final boolean isPlusChild, final boolean leafFromInstance) {
+            if ((Boolean) leaf.getAttribute()) {
+                // the leaf node represents an inside cell
+                leaf.insertInTree(parentTree, isPlusChild, new VanishingToLeaf(true));
+                return leaf;
+            }
+            // the leaf node represents an outside cell
+            tree.insertInTree(parentTree, isPlusChild, new VanishingToLeaf(false));
+            return tree;
+        }
+    }
+
+    /** BSP tree leaf merger computing intersection of two regions. */
+    private class IntersectionMerger implements BSPTree.LeafMerger<S> {
+        /** {@inheritDoc} */
+        @Override
+        public BSPTree<S> merge(final BSPTree<S> leaf, final BSPTree<S> tree,
+                                final BSPTree<S> parentTree,
+                                final boolean isPlusChild, final boolean leafFromInstance) {
+            if ((Boolean) leaf.getAttribute()) {
+                // the leaf node represents an inside cell
+                tree.insertInTree(parentTree, isPlusChild, new VanishingToLeaf(true));
+                return tree;
+            }
+            // the leaf node represents an outside cell
+            leaf.insertInTree(parentTree, isPlusChild, new VanishingToLeaf(false));
+            return leaf;
+        }
+    }
+
+    /** BSP tree leaf merger computing symmetric difference (exclusive or) of two regions. */
+    private class XorMerger implements BSPTree.LeafMerger<S> {
+        /** {@inheritDoc} */
+        @Override
+        public BSPTree<S> merge(final BSPTree<S> leaf, final BSPTree<S> tree,
+                                final BSPTree<S> parentTree, final boolean isPlusChild,
+                                final boolean leafFromInstance) {
+            BSPTree<S> t = tree;
+            if ((Boolean) leaf.getAttribute()) {
+                // the leaf node represents an inside cell
+                t = recurseComplement(t);
+            }
+            t.insertInTree(parentTree, isPlusChild, new VanishingToLeaf(true));
+            return t;
+        }
+    }
+
+    /** BSP tree leaf merger computing difference of two regions. */
+    private class DifferenceMerger implements BSPTree.LeafMerger<S>, VanishingCutHandler<S> {
+
+        /** Region to subtract from. */
+        private final Region<S> region1;
+
+        /** Region to subtract. */
+        private final Region<S> region2;
+
+        /** Simple constructor.
+         * @param region1 region to subtract from
+         * @param region2 region to subtract
+         */
+        DifferenceMerger(final Region<S> region1, final Region<S> region2) {
+            this.region1 = region1.copySelf();
+            this.region2 = region2.copySelf();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public BSPTree<S> merge(final BSPTree<S> leaf, final BSPTree<S> tree,
+                                final BSPTree<S> parentTree, final boolean isPlusChild,
+                                final boolean leafFromInstance) {
+            if ((Boolean) leaf.getAttribute()) {
+                // the leaf node represents an inside cell
+                final BSPTree<S> argTree =
+                    recurseComplement(leafFromInstance ? tree : leaf);
+                argTree.insertInTree(parentTree, isPlusChild, this);
+                return argTree;
+            }
+            // the leaf node represents an outside cell
+            final BSPTree<S> instanceTree =
+                leafFromInstance ? leaf : tree;
+            instanceTree.insertInTree(parentTree, isPlusChild, this);
+            return instanceTree;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public BSPTree<S> fixNode(final BSPTree<S> node) {
+            // get a representative point in the degenerate cell
+            final BSPTree<S> cell = node.pruneAroundConvexCell(Boolean.TRUE, Boolean.FALSE, null);
+            final Region<S> r = region1.buildNew(cell);
+            final Point<S> p = r.getBarycenter();
+            return new BSPTree<>(region1.checkPoint(p) == Location.INSIDE &&
+                                  region2.checkPoint(p) == Location.OUTSIDE);
+        }
+
+    }
+
+    /** Visitor removing internal nodes attributes. */
+    private class NodesCleaner implements  BSPTreeVisitor<S> {
+
+        /** {@inheritDoc} */
+        @Override
+        public Order visitOrder(final BSPTree<S> node) {
+            return Order.PLUS_SUB_MINUS;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void visitInternalNode(final BSPTree<S> node) {
+            node.setAttribute(null);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void visitLeafNode(final BSPTree<S> node) {
+        }
+
+    }
+
+    /** Handler replacing nodes with vanishing cuts with leaf nodes. */
+    private class VanishingToLeaf implements VanishingCutHandler<S> {
+
+        /** Inside/outside indocator to use for ambiguous nodes. */
+        private final boolean inside;
+
+        /** Simple constructor.
+         * @param inside inside/outside indicator to use for ambiguous nodes
+         */
+        VanishingToLeaf(final boolean inside) {
+            this.inside = inside;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public BSPTree<S> fixNode(final BSPTree<S> node) {
+            if (node.getPlus().getAttribute().equals(node.getMinus().getAttribute())) {
+                // no ambiguity
+                return new BSPTree<>(node.getPlus().getAttribute());
+            } else {
+                // ambiguous node
+                return new BSPTree<>(inside);
+            }
+        }
+
+    }
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
new file mode 100644
index 0000000..046defe
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Side.java
@@ -0,0 +1,36 @@
+/*
+ * 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.geometry.core.partitioning;
+
+/** Enumerate representing the location of an element with respect to an
+ * {@link Hyperplane hyperplane} of a space.
+ */
+public enum Side {
+
+    /** Code for the plus side of the hyperplane. */
+    PLUS,
+
+    /** Code for the minus side of the hyperplane. */
+    MINUS,
+
+    /** Code for elements crossing the hyperplane from plus to minus side. */
+    BOTH,
+
+    /** Code for the hyperplane itself. */
+    HYPER;
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/SubHyperplane.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/SubHyperplane.java
new file mode 100644
index 0000000..da8b24d
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/SubHyperplane.java
@@ -0,0 +1,142 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import org.apache.commons.geometry.core.Space;
+
+/** This interface represents the remaining parts of an hyperplane after
+ * other parts have been chopped off.
+
+ * <p>sub-hyperplanes are obtained when parts of an {@link
+ * Hyperplane hyperplane} are chopped off by other hyperplanes that
+ * intersect it. The remaining part is a convex region. Such objects
+ * appear in {@link BSPTree BSP trees} as the intersection of a cut
+ * hyperplane with the convex region which it splits, the chopping
+ * hyperplanes are the cut hyperplanes closer to the tree root.</p>
+
+ * <p>
+ * Note that this interface is <em>not</em> intended to be implemented
+ * by Apache Commons Math users, it is only intended to be implemented
+ * within the library itself. New methods may be added even for minor
+ * versions, which breaks compatibility for external implementations.
+ * </p>
+
+ * @param <S> Type of the embedding space.
+ */
+public interface SubHyperplane<S extends Space> {
+
+    /** Copy the instance.
+     * <p>The instance created is completely independent of the original
+     * one. A deep copy is used, none of the underlying objects are
+     * shared (except for the nodes attributes and immutable
+     * objects).</p>
+     * @return a new sub-hyperplane, copy of the instance
+     */
+    SubHyperplane<S> copySelf();
+
+    /** Get the underlying hyperplane.
+     * @return underlying hyperplane
+     */
+    Hyperplane<S> getHyperplane();
+
+    /** Check if the instance is empty.
+     * @return true if the instance is empty
+     */
+    boolean isEmpty();
+
+    /** Get the size of the instance.
+     * @return the size of the instance (this is a length in 1D, an area
+     * in 2D, a volume in 3D ...)
+     */
+    double getSize();
+
+    /** Split the instance in two parts by an hyperplane.
+     * @param hyperplane splitting hyperplane
+     * @return an object containing both the part of the instance
+     * on the plus side of the hyperplane and the part of the
+     * instance on the minus side of the hyperplane
+     */
+    SplitSubHyperplane<S> split(Hyperplane<S> hyperplane);
+
+    /** Compute the union of the instance and another sub-hyperplane.
+     * @param other other sub-hyperplane to union (<em>must</em> be in the
+     * same hyperplane as the instance)
+     * @return a new sub-hyperplane, union of the instance and other
+     */
+    SubHyperplane<S> reunite(SubHyperplane<S> other);
+
+    /** Class holding the results of the {@link #split split} method.
+     * @param <U> Type of the embedding space.
+     */
+    class SplitSubHyperplane<U extends Space> {
+
+        /** Part of the sub-hyperplane on the plus side of the splitting hyperplane. */
+        private final SubHyperplane<U> plus;
+
+        /** Part of the sub-hyperplane on the minus side of the splitting hyperplane. */
+        private final SubHyperplane<U> minus;
+
+        /** Build a SplitSubHyperplane from its parts.
+         * @param plus part of the sub-hyperplane on the plus side of the
+         * splitting hyperplane
+         * @param minus part of the sub-hyperplane on the minus side of the
+         * splitting hyperplane
+         */
+        public SplitSubHyperplane(final SubHyperplane<U> plus,
+                                  final SubHyperplane<U> minus) {
+            this.plus  = plus;
+            this.minus = minus;
+        }
+
+        /** Get the part of the sub-hyperplane on the plus side of the splitting hyperplane.
+         * @return part of the sub-hyperplane on the plus side of the splitting hyperplane
+         */
+        public SubHyperplane<U> getPlus() {
+            return plus;
+        }
+
+        /** Get the part of the sub-hyperplane on the minus side of the splitting hyperplane.
+         * @return part of the sub-hyperplane on the minus side of the splitting hyperplane
+         */
+        public SubHyperplane<U> getMinus() {
+            return minus;
+        }
+
+        /** Get the side of the split sub-hyperplane with respect to its splitter.
+         * @return {@link Side#PLUS} if only {@link #getPlus()} is neither null nor empty,
+         * {@link Side#MINUS} if only {@link #getMinus()} is neither null nor empty,
+         * {@link Side#BOTH} if both {@link #getPlus()} and {@link #getMinus()}
+         * are neither null nor empty or {@link Side#HYPER} if both {@link #getPlus()} and
+         * {@link #getMinus()} are either null or empty
+         */
+        public Side getSide() {
+            if (plus != null && !plus.isEmpty()) {
+                if (minus != null && !minus.isEmpty()) {
+                    return Side.BOTH;
+                } else {
+                    return Side.PLUS;
+                }
+            } else if (minus != null && !minus.isEmpty()) {
+                return Side.MINUS;
+            } else {
+                return Side.HYPER;
+            }
+        }
+
+    }
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Transform.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Transform.java
new file mode 100644
index 0000000..a034d6c
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/Transform.java
@@ -0,0 +1,78 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+
+
+/** This interface represents an inversible affine transform in a space.
+ * <p>Inversible affine transform include for example scalings,
+ * translations, rotations.</p>
+
+ * <p>Transforms are dimension-specific. The consistency rules between
+ * the three {@code apply} methods are the following ones for a
+ * transformed defined for dimension D:</p>
+ * <ul>
+ *   <li>
+ *     the transform can be applied to a point in the
+ *     D-dimension space using its {@link #apply(Point)}
+ *     method
+ *   </li>
+ *   <li>
+ *     the transform can be applied to a (D-1)-dimension
+ *     hyperplane in the D-dimension space using its
+ *     {@link #apply(Hyperplane)} method
+ *   </li>
+ *   <li>
+ *     the transform can be applied to a (D-2)-dimension
+ *     sub-hyperplane in a (D-1)-dimension hyperplane using
+ *     its {@link #apply(SubHyperplane, Hyperplane, Hyperplane)}
+ *     method
+ *   </li>
+ * </ul>
+
+ * @param <S> Type of the embedding space.
+ * @param <T> Type of the embedded sub-space.
+ */
+public interface Transform<S extends Space, T extends Space> {
+
+    /** Transform a point of a space.
+     * @param point point to transform
+     * @return a new object representing the transformed point
+     */
+    Point<S> apply(Point<S> point);
+
+    /** Transform an hyperplane of a space.
+     * @param hyperplane hyperplane to transform
+     * @return a new object representing the transformed hyperplane
+     */
+    Hyperplane<S> apply(Hyperplane<S> hyperplane);
+
+    /** Transform a sub-hyperplane embedded in an hyperplane.
+     * @param sub sub-hyperplane to transform
+     * @param original hyperplane in which the sub-hyperplane is
+     * defined (this is the original hyperplane, the transform has
+     * <em>not</em> been applied to it)
+     * @param transformed hyperplane in which the sub-hyperplane is
+     * defined (this is the transformed hyperplane, the transform
+     * <em>has</em> been applied to it)
+     * @return a new object representing the transformed sub-hyperplane
+     */
+    SubHyperplane<T> apply(SubHyperplane<T> sub, Hyperplane<S> original, Hyperplane<S> transformed);
+
+}
diff --git a/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/package-info.java b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/package-info.java
new file mode 100644
index 0000000..c1a1208
--- /dev/null
+++ b/commons-geometry-core/src/main/java/org/apache/commons/geometry/core/partitioning/package-info.java
@@ -0,0 +1,114 @@
+/*
+ * 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.
+ */
+/**
+ *
+ * This package provides classes to implement Binary Space Partition trees.
+ *
+ * <p>
+ * {@link org.apache.commons.geometry.partitioning.BSPTree BSP trees}
+ * are an efficient way to represent parts of space and in particular
+ * polytopes (line segments in 1D, polygons in 2D and polyhedrons in 3D)
+ * and to operate on them. The main principle is to recursively subdivide
+ * the space using simple hyperplanes (points in 1D, lines in 2D, planes
+ * in 3D).
+ * </p>
+ *
+ * <p>
+ * We start with a tree composed of a single node without any cut
+ * hyperplane: it represents the complete space, which is a convex
+ * part. If we add a cut hyperplane to this node, this represents a
+ * partition with the hyperplane at the node level and two half spaces at
+ * each side of the cut hyperplane. These half-spaces are represented by
+ * two child nodes without any cut hyperplanes associated, the plus child
+ * which represents the half space on the plus side of the cut hyperplane
+ * and the minus child on the other side. Continuing the subdivisions, we
+ * end up with a tree having internal nodes that are associated with a
+ * cut hyperplane and leaf nodes without any hyperplane which correspond
+ * to convex parts.
+ * </p>
+ *
+ * <p>
+ * When BSP trees are used to represent polytopes, the convex parts are
+ * known to be completely inside or outside the polytope as long as there
+ * is no facet in the part (which is obviously the case if the cut
+ * hyperplanes have been chosen as the underlying hyperplanes of the
+ * facets (this is called an autopartition) and if the subdivision
+ * process has been continued until all facets have been processed. It is
+ * important to note that the polytope is <em>not</em> defined by a
+ * single part, but by several convex ones. This is the property that
+ * allows BSP-trees to represent non-convex polytopes despites all parts
+ * are convex. The {@link
+ * org.apache.commons.geometry.partitioning.Region Region} class is
+ * devoted to this representation, it is build on top of the {@link
+ * org.apache.commons.geometry.partitioning.BSPTree BSPTree} class using
+ * boolean objects as the leaf nodes attributes to represent the
+ * inside/outside property of each leaf part, and also adds various
+ * methods dealing with boundaries (i.e. the separation between the
+ * inside and the outside parts).
+ * </p>
+ *
+ * <p>
+ * Rather than simply associating the internal nodes with an hyperplane,
+ * we consider <em>sub-hyperplanes</em> which correspond to the part of
+ * the hyperplane that is inside the convex part defined by all the
+ * parent nodes (this implies that the sub-hyperplane at root node is in
+ * fact a complete hyperplane, because there is no parent to bound
+ * it). Since the parts are convex, the sub-hyperplanes are convex, in
+ * 3D the convex parts are convex polyhedrons, and the sub-hyperplanes
+ * are convex polygons that cut these polyhedrons in two
+ * sub-polyhedrons. Using this definition, a BSP tree completely
+ * partitions the space. Each point either belongs to one of the
+ * sub-hyperplanes in an internal node or belongs to one of the leaf
+ * convex parts.
+ * </p>
+ *
+ * <p>
+ * In order to determine where a point is, it is sufficient to check its
+ * position with respect to the root cut hyperplane, to select the
+ * corresponding child tree and to repeat the procedure recursively,
+ * until either the point appears to be exactly on one of the hyperplanes
+ * in the middle of the tree or to be in one of the leaf parts. For
+ * this operation, it is sufficient to consider the complete hyperplanes,
+ * there is no need to check the points with the boundary of the
+ * sub-hyperplanes, because this check has in fact already been realized
+ * by the recursive descent in the tree. This is very easy to do and very
+ * efficient, especially if the tree is well balanced (the cost is
+ * <code>O(log(n))</code> where <code>n</code> is the number of facets)
+ * or if the first tree levels close to the root discriminate large parts
+ * of the total space.
+ * </p>
+ *
+ * <p>
+ * One of the main sources for the development of this package was Bruce
+ * Naylor, John Amanatides and William Thibault paper <a
+ * href="http://www.cs.yorku.ca/~amana/research/bsptSetOp.pdf";>Merging
+ * BSP Trees Yields Polyhedral Set Operations</a> Proc. Siggraph '90,
+ * Computer Graphics 24(4), August 1990, pp 115-124, published by the
+ * Association for Computing Machinery (ACM). The same paper can also be
+ * found <a
+ * href="http://www.cs.utexas.edu/users/fussell/courses/cs384g/bsp_treemerge.pdf";>here</a>.
+ * </p>
+ *
+ * <p>
+ * Note that the interfaces defined in this package are <em>not</em> intended to
+ * be implemented by Apache Commons Math users, they are only intended to be
+ * implemented within the library itself. New methods may be added even for
+ * minor versions, which breaks compatibility for external implementations.
+ * </p>
+ *
+ */
+package org.apache.commons.geometry.core.partitioning;
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/GeometryTestUtils.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/GeometryTestUtils.java
new file mode 100644
index 0000000..3dbf58a
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/GeometryTestUtils.java
@@ -0,0 +1,69 @@
+/*
+ * 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.geometry.core;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.junit.Assert;
+
+/** Class containing various geometry-related test utilities.
+ */
+public class GeometryTestUtils {
+
+    /** Asserts that the given value is positive infinity.
+     * @param value
+     */
+    public static void assertPositiveInfinity(double value) {
+        String msg = "Expected value to be positive infinity but was " + value;
+        Assert.assertTrue(msg, Double.isInfinite(value));
+        Assert.assertTrue(msg, value > 0);
+    }
+
+    /** Asserts that the given value is negative infinity..
+     * @param value
+     */
+    public static void assertNegativeInfinity(double value) {
+        String msg = "Expected value to be negative infinity but was " + value;
+        Assert.assertTrue(msg, Double.isInfinite(value));
+        Assert.assertTrue(msg, value < 0);
+    }
+
+    /**
+     * Serializes and then recovers an object from a byte array. Returns the deserialized object.
+     *
+     * @param obj  object to serialize and recover
+     * @return  the recovered, deserialized object
+     */
+    public static Object serializeAndRecover(Object obj) {
+        try {
+            // serialize the Object
+            ByteArrayOutputStream bos = new ByteArrayOutputStream();
+            ObjectOutputStream so = new ObjectOutputStream(bos);
+            so.writeObject(obj);
+
+            // deserialize the Object
+            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
+            ObjectInputStream si = new ObjectInputStream(bis);
+            return si.readObject();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/TreeBuilder.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/TreeBuilder.java
new file mode 100644
index 0000000..364d702
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/TreeBuilder.java
@@ -0,0 +1,164 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import java.io.IOException;
+import java.text.ParseException;
+import java.util.StringTokenizer;
+
+import org.apache.commons.geometry.core.Space;
+import org.apache.commons.geometry.core.partitioning.AbstractRegion;
+import org.apache.commons.geometry.core.partitioning.BSPTree;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+
+/** Local class for building an {@link AbstractRegion} tree.
+ * @param <S> Type of the space.
+ */
+public abstract class TreeBuilder<S extends Space> {
+
+    /** Keyword for tolerance. */
+    private static final String TOLERANCE = "tolerance";
+
+    /** Keyword for internal nodes. */
+    private static final String INTERNAL  = "internal";
+
+    /** Keyword for leaf nodes. */
+    private static final String LEAF      = "leaf";
+
+    /** Keyword for plus children trees. */
+    private static final String PLUS      = "plus";
+
+    /** Keyword for minus children trees. */
+    private static final String MINUS     = "minus";
+
+    /** Keyword for true flags. */
+    private static final String TRUE      = "true";
+
+    /** Keyword for false flags. */
+    private static final String FALSE     = "false";
+
+    /** Tree root. */
+    private BSPTree<S> root;
+
+    /** Tolerance. */
+    private final double tolerance;
+
+    /** Tokenizer parsing string representation. */
+    private final StringTokenizer tokenizer;
+
+    /** Simple constructor.
+     * @param type type of the expected representation
+     * @param reader reader for the string representation
+     * @exception IOException if the string cannot be read
+     * @exception ParseException if the string cannot be parsed
+     */
+    public TreeBuilder(final String type, final String s)
+        throws IOException, ParseException {
+        root = null;
+        tokenizer = new StringTokenizer(s);
+        getWord(type);
+        getWord(TOLERANCE);
+        tolerance = getNumber();
+        getWord(PLUS);
+        root = new BSPTree<>();
+        parseTree(root);
+        if (tokenizer.hasMoreTokens()) {
+            throw new ParseException("unexpected " + tokenizer.nextToken(), 0);
+        }
+    }
+
+    /** Parse a tree.
+     * @param node start node
+     * @exception IOException if the string cannot be read
+     * @exception ParseException if the string cannot be parsed
+     */
+    private void parseTree(final BSPTree<S> node)
+        throws IOException, ParseException {
+        if (INTERNAL.equals(getWord(INTERNAL, LEAF))) {
+            // this is an internal node, it has a cut sub-hyperplane (stored as a whole hyperplane)
+            // then a minus tree, then a plus tree
+            node.insertCut(parseHyperplane());
+            getWord(MINUS);
+            parseTree(node.getMinus());
+            getWord(PLUS);
+            parseTree(node.getPlus());
+        } else {
+            // this is a leaf node, it has only an inside/outside flag
+            node.setAttribute(getBoolean());
+        }
+    }
+
+    /** Get next word.
+     * @param allowed allowed values
+     * @return parsed word
+     * @exception IOException if the string cannot be read
+     * @exception ParseException if the string cannot be parsed
+     */
+    protected String getWord(final String ... allowed)
+        throws IOException, ParseException {
+        final String token = tokenizer.nextToken();
+        for (final String a : allowed) {
+            if (a.equals(token)) {
+                return token;
+            }
+        }
+        throw new ParseException(token + " != " + allowed[0], 0);
+    }
+
+    /** Get next number.
+     * @return parsed number
+     * @exception IOException if the string cannot be read
+     * @exception NumberFormatException if the string cannot be parsed
+     */
+    protected double getNumber()
+        throws IOException, NumberFormatException {
+        return Double.parseDouble(tokenizer.nextToken());
+    }
+
+    /** Get next boolean.
+     * @return parsed boolean
+     * @exception IOException if the string cannot be read
+     * @exception ParseException if the string cannot be parsed
+     */
+    protected boolean getBoolean()
+        throws IOException, ParseException {
+        return getWord(TRUE, FALSE).equals(TRUE);
+    }
+
+    /** Get the built tree.
+     * @return built tree
+     */
+    public BSPTree<S> getTree() {
+        return root;
+    }
+
+    /** Get the tolerance.
+     * @return tolerance
+     */
+    public double getTolerance() {
+        return tolerance;
+    }
+
+    /** Parse an hyperplane.
+     * @return next hyperplane from the stream
+     * @exception IOException if the string cannot be read
+     * @exception ParseException if the string cannot be parsed
+     */
+    protected abstract Hyperplane<S> parseHyperplane()
+        throws IOException, ParseException;
+
+}
\ No newline at end of file
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/TreeDumper.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/TreeDumper.java
new file mode 100644
index 0000000..532b9f9
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/TreeDumper.java
@@ -0,0 +1,106 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import java.util.Formatter;
+import java.util.Locale;
+
+import org.apache.commons.geometry.core.Space;
+import org.apache.commons.geometry.core.partitioning.BSPTree;
+import org.apache.commons.geometry.core.partitioning.BSPTreeVisitor;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+
+/** Dumping visitor.
+ * @param <S> Type of the space.
+ */
+public abstract class TreeDumper<S extends Space> implements BSPTreeVisitor<S> {
+    /** Builder for the string representation of the dumped tree. */
+    private final StringBuilder dump;
+
+    /** Formatter for strings. */
+    private final Formatter formatter;
+
+    /** Current indentation prefix. */
+    private String prefix;
+
+    /** Simple constructor.
+     * @param type type of the region to dump
+     * @param tolerance tolerance of the region
+     */
+    public TreeDumper(final String type, final double tolerance) {
+        this.dump      = new StringBuilder();
+        this.formatter = new Formatter(dump, Locale.US);
+        this.prefix    = "";
+        formatter.format("%s%n", type);
+        formatter.format("tolerance %22.15e%n", tolerance);
+    }
+
+    /** Get the string representation of the tree.
+     * @return string representation of the tree.
+     */
+    public String getDump() {
+        return dump.toString();
+    }
+
+    /** Get the formatter to use.
+     * @return formatter to use
+     */
+    protected Formatter getFormatter() {
+        return formatter;
+    }
+
+    /** Format a string representation of the hyperplane underlying a cut sub-hyperplane.
+     * @param hyperplane hyperplane to format
+     */
+    protected abstract void formatHyperplane(Hyperplane<S> hyperplane);
+
+    /** {@inheritDoc} */
+    @Override
+    public Order visitOrder(final BSPTree<S> node) {
+        return Order.SUB_MINUS_PLUS;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void visitInternalNode(final BSPTree<S> node) {
+        formatter.format("%s %s internal ", prefix, type(node));
+        formatHyperplane(node.getCut().getHyperplane());
+        formatter.format("%n");
+        prefix = prefix + "  ";
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void visitLeafNode(final BSPTree<S> node) {
+        formatter.format("%s %s leaf %s%n",
+                         prefix, type(node), node.getAttribute());
+        for (BSPTree<S> n = node;
+             n.getParent() != null && n == n.getParent().getPlus();
+             n = n.getParent()) {
+            prefix = prefix.substring(0, prefix.length() - 2);
+        }
+    }
+
+    /** Get the type of the node.
+     * @param node node to check
+     * @return "plus " or "minus" depending on the node being the plus or minus
+     * child of its parent ("plus " is arbitrarily returned for the root node)
+     */
+    private String type(final BSPTree<S> node) {
+        return (node.getParent() != null && node == node.getParent().getMinus()) ? "minus" : "plus ";
+    }
+}
diff --git a/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/TreePrinter.java b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/TreePrinter.java
new file mode 100644
index 0000000..6f79537
--- /dev/null
+++ b/commons-geometry-core/src/test/java/org/apache/commons/geometry/core/partitioning/TreePrinter.java
@@ -0,0 +1,137 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import java.util.Objects;
+
+import org.apache.commons.geometry.core.Space;
+import org.apache.commons.geometry.core.partitioning.BSPTree;
+import org.apache.commons.geometry.core.partitioning.BSPTreeVisitor;
+
+/** Base for classes that create string representations of {@link BSPTree}s.
+ * @param <S>
+ */
+public abstract class TreePrinter<S extends Space> implements BSPTreeVisitor<S> {
+
+    /** Indent per tree level */
+    protected static final String INDENT = "    ";
+
+    /** Current depth in the tree */
+    protected int depth;
+
+    /** Contains the string output */
+    protected StringBuilder output = new StringBuilder();
+
+    /** Returns a string representation of the given {@link BSPTree}.
+     * @param tree
+     * @return
+     */
+    public String writeAsString(BSPTree<S> tree) {
+        output.delete(0, output.length());
+
+        tree.visit(this);
+
+        return output.toString();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Order visitOrder(BSPTree<S> node) {
+        return Order.SUB_MINUS_PLUS;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void visitInternalNode(BSPTree<S> node) {
+        writeLinePrefix(node);
+        writeInternalNode(node);
+
+        write("\n");
+
+        ++depth;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void visitLeafNode(BSPTree<S> node) {
+        writeLinePrefix(node);
+        writeLeafNode(node);
+
+        write("\n");
+
+        BSPTree<S> cur = node;
+        while (cur.getParent() != null && cur.getParent().getPlus() == cur) {
+            --depth;
+            cur = cur.getParent();
+        }
+    }
+
+    /** Writes the prefix for the current line in the output. This includes
+     * the line indent, the plus/minus node indicator, and a string identifier
+     * for the node itself.
+     * @param node
+     */
+    protected void writeLinePrefix(BSPTree<S> node) {
+        for (int i=0; i<depth; ++i) {
+            write(INDENT);
+        }
+
+        if (node.getParent() != null) {
+            if (node.getParent().getMinus() == node) {
+                write("[-] ");
+            }
+            else {
+                write("[+] ");
+            }
+        }
+
+        write(nodeIdString(node) + " | ");
+    }
+
+    /** Returns a short string identifier for the given node.
+     * @param node
+     * @return
+     */
+    protected String nodeIdString(BSPTree<S> node) {
+        String str = Objects.toString(node);
+        int idx = str.lastIndexOf('.');
+        if (idx > -1) {
+            return str.substring(idx + 1, str.length());
+        }
+        return str;
+    }
+
+    /** Adds the given string to the output.
+     * @param str
+     */
+    protected void write(String str) {
+        output.append(str);
+    }
+
+    /** Method for subclasses to provide their own string representation
+     * of the given internal node.
+     */
+    protected abstract void writeInternalNode(BSPTree<S> node);
+
+    /** Writes a leaf node. The default implementation here simply writes
+     * the node attribute as a string.
+     * @param node
+     */
+    protected void writeLeafNode(BSPTree<S> node) {
+        write(String.valueOf(node.getAttribute()));
+    }
+}
\ No newline at end of file
diff --git a/commons-geometry-euclidean-threed/pom.xml b/commons-geometry-enclosing/pom.xml
similarity index 52%
rename from commons-geometry-euclidean-threed/pom.xml
rename to commons-geometry-enclosing/pom.xml
index 613cac8..e1ccc81 100644
--- a/commons-geometry-euclidean-threed/pom.xml
+++ b/commons-geometry-enclosing/pom.xml
@@ -27,20 +27,54 @@
   </parent>
 
   <groupId>org.apache.commons</groupId>
-  <artifactId>commons-geometry-euclidean-threed</artifactId>
+  <artifactId>commons-geometry-enclosing</artifactId>
   <version>1.0-SNAPSHOT</version>
-  <name>Apache Commons Geometry Three-Dimensional Euclidean Space</name>
+  <name>Apache Commons Geometry Enclosing</name>
 
-  <description></description>
+  <description>Algorithms for computing enclosing balls.</description>
 
   <properties>
     <!-- OSGi -->
-    <commons.osgi.symbolicName>org.apache.commons.geometry.euclidean.threed</commons.osgi.symbolicName>
-    <commons.osgi.export>org.apache.commons.geometry.euclidean.threed</commons.osgi.export>
+    <commons.osgi.symbolicName>org.apache.commons.geometry.enclosing</commons.osgi.symbolicName>
+    <commons.osgi.export>org.apache.commons.geometry.enclosing</commons.osgi.export>
     <!-- Java 9+ -->
-    <commons.automatic.module.name>org.apache.commons.geometry.euclidean.threed</commons.automatic.module.name>
+    <commons.automatic.module.name>org.apache.commons.geometry.enclosing</commons.automatic.module.name>
     <!-- Workaround to avoid duplicating config files. -->
     <geometry.parent.dir>${basedir}/..</geometry.parent.dir>
   </properties>
 
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-geometry-core</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-geometry-euclidean</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-numbers-fraction</artifactId>
+    </dependency>
+    
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-rng-client-api</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-rng-simple</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-rng-sampling</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
 </project>
diff --git a/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/Encloser.java b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/Encloser.java
new file mode 100644
index 0000000..a99023a
--- /dev/null
+++ b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/Encloser.java
@@ -0,0 +1,35 @@
+/*
+ * 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.geometry.enclosing;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+
+/** Interface for algorithms computing enclosing balls.
+ * @param <S> Space type.
+ * @param <P> Point type.
+ * @see EnclosingBall
+ */
+public interface Encloser<S extends Space, P extends Point<S>> {
+
+    /** Find a ball enclosing a list of points.
+     * @param points points to enclose
+     * @return enclosing ball
+     */
+    EnclosingBall<S, P> enclose(Iterable<P> points);
+
+}
diff --git a/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/EnclosingBall.java b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/EnclosingBall.java
new file mode 100644
index 0000000..b269747
--- /dev/null
+++ b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/EnclosingBall.java
@@ -0,0 +1,103 @@
+/*
+ * 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.geometry.enclosing;
+
+import java.io.Serializable;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+
+/** This class represents a ball enclosing some points.
+ * @param <S> Space type.
+ * @param <P> Point type.
+ * @see Space
+ * @see Point
+ * @see Encloser
+ */
+public class EnclosingBall<S extends Space, P extends Point<S>> implements Serializable {
+
+    /** Serializable UID. */
+    private static final long serialVersionUID = 20140126L;
+
+    /** Center of the ball. */
+    private final P center;
+
+    /** Radius of the ball. */
+    private final double radius;
+
+    /** Support points used to define the ball. */
+    private final P[] support;
+
+    /** Simple constructor.
+     * @param center center of the ball
+     * @param radius radius of the ball
+     * @param support support points used to define the ball
+     */
+    @SafeVarargs
+    public EnclosingBall(final P center, final double radius, final P ... support) {
+        this.center  = center;
+        this.radius  = radius;
+        this.support = support.clone();
+    }
+
+    /** Get the center of the ball.
+     * @return center of the ball
+     */
+    public P getCenter() {
+        return center;
+    }
+
+    /** Get the radius of the ball.
+     * @return radius of the ball (can be negative if the ball is empty)
+     */
+    public double getRadius() {
+        return radius;
+    }
+
+    /** Get the support points used to define the ball.
+     * @return support points used to define the ball
+     */
+    public P[] getSupport() {
+        return support.clone();
+    }
+
+    /** Get the number of support points used to define the ball.
+     * @return number of support points used to define the ball
+     */
+    public int getSupportSize() {
+        return support.length;
+    }
+
+    /** Check if a point is within the ball or at boundary.
+     * @param point point to test
+     * @return true if the point is within the ball or at boundary
+     */
+    public boolean contains(final P point) {
+        return point.distance(center) <= radius;
+    }
+
+    /** Check if a point is within an enlarged ball or at boundary.
+     * @param point point to test
+     * @param margin margin to consider
+     * @return true if the point is within the ball enlarged
+     * by the margin or at boundary
+     */
+    public boolean contains(final P point, final double margin) {
+        return point.distance(center) <= radius + margin;
+    }
+
+}
diff --git a/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/SupportBallGenerator.java b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/SupportBallGenerator.java
new file mode 100644
index 0000000..53a9229
--- /dev/null
+++ b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/SupportBallGenerator.java
@@ -0,0 +1,41 @@
+/*
+ * 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.geometry.enclosing;
+
+import java.util.List;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+
+/** Interface for generating balls based on support points.
+ * <p>
+ * This generator is used in the {@link WelzlEncloser Emo Welzl} algorithm
+ * and its derivatives.
+ * </p>
+ * @param <S> Space type.
+ * @param <P> Point type.
+ * @see EnclosingBall
+ */
+public interface SupportBallGenerator<S extends Space, P extends Point<S>> {
+
+    /** Create a ball whose boundary lies on prescribed support points.
+     * @param support support points (may be empty)
+     * @return ball whose boundary lies on the prescribed support points
+     */
+    EnclosingBall<S, P> ballOnSupport(List<P> support);
+
+}
diff --git a/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/WelzlEncloser.java b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/WelzlEncloser.java
new file mode 100644
index 0000000..c20c706
--- /dev/null
+++ b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/WelzlEncloser.java
@@ -0,0 +1,180 @@
+/*
+ * 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.geometry.enclosing;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+
+/** Class implementing Emo Welzl algorithm to find the smallest enclosing ball in linear time.
+ * <p>
+ * The class implements the algorithm described in paper <a
+ * href="http://www.inf.ethz.ch/personal/emo/PublFiles/SmallEnclDisk_LNCS555_91.pdf";>Smallest
+ * Enclosing Disks (Balls and Ellipsoids)</a> by Emo Welzl, Lecture Notes in Computer Science
+ * 555 (1991) 359-370. The pivoting improvement published in the paper <a
+ * href="http://www.inf.ethz.ch/personal/gaertner/texts/own_work/esa99_final.pdf";>Fast and
+ * Robust Smallest Enclosing Balls</a>, by Bernd Gärtner and further modified in
+ * paper <a
+ * href="http://www.idt.mdh.se/kurser/ct3340/ht12/MINICONFERENCE/FinalPapers/ircse12_submission_30.pdf";>
+ * Efficient Computation of Smallest Enclosing Balls in Three Dimensions</a> by Linus Källberg
+ * to avoid performing local copies of data have been included.
+ * </p>
+ * @param <S> Space type.
+ * @param <P> Point type.
+ */
+public class WelzlEncloser<S extends Space, P extends Point<S>> implements Encloser<S, P> {
+
+    /** Tolerance below which points are consider to be identical. */
+    private final double tolerance;
+
+    /** Generator for balls on support. */
+    private final SupportBallGenerator<S, P> generator;
+
+    /** Simple constructor.
+     * @param tolerance below which points are consider to be identical
+     * @param generator generator for balls on support
+     */
+    public WelzlEncloser(final double tolerance, final SupportBallGenerator<S, P> generator) {
+        this.tolerance = tolerance;
+        this.generator = generator;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public EnclosingBall<S, P> enclose(final Iterable<P> points) {
+
+        if (points == null || !points.iterator().hasNext()) {
+            // return an empty ball
+            return generator.ballOnSupport(new ArrayList<P>());
+        }
+
+        // Emo Welzl algorithm with Bernd Gärtner and Linus Källberg improvements
+        return pivotingBall(points);
+
+    }
+
+    /** Compute enclosing ball using Gärtner's pivoting heuristic.
+     * @param points points to be enclosed
+     * @return enclosing ball
+     */
+    private EnclosingBall<S, P> pivotingBall(final Iterable<P> points) {
+
+        final P first = points.iterator().next();
+        final List<P> extreme = new ArrayList<>(first.getSpace().getDimension() + 1);
+        final List<P> support = new ArrayList<>(first.getSpace().getDimension() + 1);
+
+        // start with only first point selected as a candidate support
+        extreme.add(first);
+        EnclosingBall<S, P> ball = moveToFrontBall(extreme, extreme.size(), support);
+
+        while (true) {
+
+            // select the point farthest to current ball
+            final P farthest = selectFarthest(points, ball);
+
+            if (ball.contains(farthest, tolerance)) {
+                // we have found a ball containing all points
+                return ball;
+            }
+
+            // recurse search, restricted to the small subset containing support and farthest point
+            support.clear();
+            support.add(farthest);
+            EnclosingBall<S, P> savedBall = ball;
+            ball = moveToFrontBall(extreme, extreme.size(), support);
+            if (ball.getRadius() < savedBall.getRadius()) {
+                // this should never happen
+                throw new IllegalStateException("Please file a bug report");
+            }
+
+            // it was an interesting point, move it to the front
+            // according to Gärtner's heuristic
+            extreme.add(0, farthest);
+
+            // prune the least interesting points
+            extreme.subList(ball.getSupportSize(), extreme.size()).clear();
+
+
+        }
+    }
+
+    /** Compute enclosing ball using Welzl's move to front heuristic.
+     * @param extreme subset of extreme points
+     * @param nbExtreme number of extreme points to consider
+     * @param support points that must belong to the ball support
+     * @return enclosing ball, for the extreme subset only
+     */
+    private EnclosingBall<S, P> moveToFrontBall(final List<P> extreme, final int nbExtreme,
+                                                final List<P> support) {
+
+        // create a new ball on the prescribed support
+        EnclosingBall<S, P> ball = generator.ballOnSupport(support);
+
+        if (ball.getSupportSize() <= ball.getCenter().getSpace().getDimension()) {
+
+            for (int i = 0; i < nbExtreme; ++i) {
+                final P pi = extreme.get(i);
+                if (!ball.contains(pi, tolerance)) {
+
+                    // we have found an outside point,
+                    // enlarge the ball by adding it to the support
+                    support.add(pi);
+                    ball = moveToFrontBall(extreme, i, support);
+                    support.remove(support.size() - 1);
+
+                    // it was an interesting point, move it to the front
+                    // according to Welzl's heuristic
+                    for (int j = i; j > 0; --j) {
+                        extreme.set(j, extreme.get(j - 1));
+                    }
+                    extreme.set(0, pi);
+
+                }
+            }
+
+        }
+
+        return ball;
+
+    }
+
+    /** Select the point farthest to the current ball.
+     * @param points points to be enclosed
+     * @param ball current ball
+     * @return farthest point
+     */
+    public P selectFarthest(final Iterable<P> points, final EnclosingBall<S, P> ball) {
+
+        final P center = ball.getCenter();
+        P farthest   = null;
+        double dMax  = -1.0;
+
+        for (final P point : points) {
+            final double d = point.distance(center);
+            if (d > dMax) {
+                farthest = point;
+                dMax     = d;
+            }
+        }
+
+        return farthest;
+
+    }
+
+}
diff --git a/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/package-info.java b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/package-info.java
new file mode 100644
index 0000000..7338211
--- /dev/null
+++ b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/enclosing/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+/**
+ *
+ * <p>
+ * This package provides interfaces and classes related to the smallest enclosing ball problem.
+ * </p>
+ *
+ */
+package org.apache.commons.geometry.enclosing;
diff --git a/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/euclidean/threed/enclosing/SphereGenerator.java b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/euclidean/threed/enclosing/SphereGenerator.java
new file mode 100644
index 0000000..5bdc69c
--- /dev/null
+++ b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/euclidean/threed/enclosing/SphereGenerator.java
@@ -0,0 +1,154 @@
+/*
+ * 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.geometry.euclidean.threed.enclosing;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.enclosing.EnclosingBall;
+import org.apache.commons.geometry.enclosing.SupportBallGenerator;
+import org.apache.commons.geometry.euclidean.threed.Cartesian3D;
+import org.apache.commons.geometry.euclidean.threed.Euclidean3D;
+import org.apache.commons.geometry.euclidean.threed.Plane;
+import org.apache.commons.geometry.euclidean.twod.Cartesian2D;
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+import org.apache.commons.geometry.euclidean.twod.enclosing.DiskGenerator;
+import org.apache.commons.numbers.fraction.BigFraction;
+
+/** Class generating an enclosing ball from its support points.
+ */
+public class SphereGenerator implements SupportBallGenerator<Euclidean3D, Cartesian3D> {
+
+    /** {@inheritDoc} */
+    @Override
+    public EnclosingBall<Euclidean3D, Cartesian3D> ballOnSupport(final List<Cartesian3D> support) {
+
+        if (support.size() < 1) {
+            return new EnclosingBall<>(Cartesian3D.ZERO, Double.NEGATIVE_INFINITY);
+        } else {
+            final Cartesian3D vA = support.get(0);
+            if (support.size() < 2) {
+                return new EnclosingBall<>(vA, 0, vA);
+            } else {
+                final Cartesian3D vB = support.get(1);
+                if (support.size() < 3) {
+                    return new EnclosingBall<>(new Cartesian3D(0.5, vA, 0.5, vB),
+                                                                    0.5 * vA.distance(vB),
+                                                                    vA, vB);
+                } else {
+                    final Cartesian3D vC = support.get(2);
+                    if (support.size() < 4) {
+
+                        // delegate to 2D disk generator
+                        final Plane p = new Plane(vA, vB, vC,
+                                                  1.0e-10 * (vA.getNorm1() + vB.getNorm1() + vC.getNorm1()));
+                        final EnclosingBall<Euclidean2D, Cartesian2D> disk =
+                                new DiskGenerator().ballOnSupport(Arrays.asList(p.toSubSpace(vA),
+                                                                                p.toSubSpace(vB),
+                                                                                p.toSubSpace(vC)));
+
+                        // convert back to 3D
+                        return new EnclosingBall<>(p.toSpace(disk.getCenter()),
+                                                                        disk.getRadius(), vA, vB, vC);
+
+                    } else {
+                        final Cartesian3D vD = support.get(3);
+                        // a sphere is 3D can be defined as:
+                        // (1)   (x - x_0)^2 + (y - y_0)^2 + (z - z_0)^2 = r^2
+                        // which can be written:
+                        // (2)   (x^2 + y^2 + z^2) - 2 x_0 x - 2 y_0 y - 2 z_0 z + (x_0^2 + y_0^2 + z_0^2 - r^2) = 0
+                        // or simply:
+                        // (3)   (x^2 + y^2 + z^2) + a x + b y + c z + d = 0
+                        // with sphere center coordinates -a/2, -b/2, -c/2
+                        // If the sphere exists, a b, c and d are a non zero solution to
+                        // [ (x^2  + y^2  + z^2)    x    y   z    1 ]   [ 1 ]   [ 0 ]
+                        // [ (xA^2 + yA^2 + zA^2)   xA   yA  zA   1 ]   [ a ]   [ 0 ]
+                        // [ (xB^2 + yB^2 + zB^2)   xB   yB  zB   1 ] * [ b ] = [ 0 ]
+                        // [ (xC^2 + yC^2 + zC^2)   xC   yC  zC   1 ]   [ c ]   [ 0 ]
+                        // [ (xD^2 + yD^2 + zD^2)   xD   yD  zD   1 ]   [ d ]   [ 0 ]
+                        // So the determinant of the matrix is zero. Computing this determinant
+                        // by expanding it using the minors m_ij of first row leads to
+                        // (4)   m_11 (x^2 + y^2 + z^2) - m_12 x + m_13 y - m_14 z + m_15 = 0
+                        // So by identifying equations (2) and (4) we get the coordinates
+                        // of center as:
+                        //      x_0 = +m_12 / (2 m_11)
+                        //      y_0 = -m_13 / (2 m_11)
+                        //      z_0 = +m_14 / (2 m_11)
+                        // Note that the minors m_11, m_12, m_13 and m_14 all have the last column
+                        // filled with 1.0, hence simplifying the computation
+                        final BigFraction[] c2 = new BigFraction[] {
+                            new BigFraction(vA.getX()), new BigFraction(vB.getX()),
+                            new BigFraction(vC.getX()), new BigFraction(vD.getX())
+                        };
+                        final BigFraction[] c3 = new BigFraction[] {
+                            new BigFraction(vA.getY()), new BigFraction(vB.getY()),
+                            new BigFraction(vC.getY()), new BigFraction(vD.getY())
+                        };
+                        final BigFraction[] c4 = new BigFraction[] {
+                            new BigFraction(vA.getZ()), new BigFraction(vB.getZ()),
+                            new BigFraction(vC.getZ()), new BigFraction(vD.getZ())
+                        };
+                        final BigFraction[] c1 = new BigFraction[] {
+                            c2[0].multiply(c2[0]).add(c3[0].multiply(c3[0])).add(c4[0].multiply(c4[0])),
+                            c2[1].multiply(c2[1]).add(c3[1].multiply(c3[1])).add(c4[1].multiply(c4[1])),
+                            c2[2].multiply(c2[2]).add(c3[2].multiply(c3[2])).add(c4[2].multiply(c4[2])),
+                            c2[3].multiply(c2[3]).add(c3[3].multiply(c3[3])).add(c4[3].multiply(c4[3]))
+                        };
+                        final BigFraction twoM11  = minor(c2, c3, c4).multiply(2);
+                        final BigFraction m12     = minor(c1, c3, c4);
+                        final BigFraction m13     = minor(c1, c2, c4);
+                        final BigFraction m14     = minor(c1, c2, c3);
+                        final BigFraction centerX = m12.divide(twoM11);
+                        final BigFraction centerY = m13.divide(twoM11).negate();
+                        final BigFraction centerZ = m14.divide(twoM11);
+                        final BigFraction dx      = c2[0].subtract(centerX);
+                        final BigFraction dy      = c3[0].subtract(centerY);
+                        final BigFraction dz      = c4[0].subtract(centerZ);
+                        final BigFraction r2      = dx.multiply(dx).add(dy.multiply(dy)).add(dz.multiply(dz));
+                        return new EnclosingBall<>(new Cartesian3D(centerX.doubleValue(),
+                                                                                     centerY.doubleValue(),
+                                                                                     centerZ.doubleValue()),
+                                                                        Math.sqrt(r2.doubleValue()),
+                                                                        vA, vB, vC, vD);
+                    }
+                }
+            }
+        }
+    }
+
+    /** Compute a dimension 4 minor, when 4<sup>th</sup> column is known to be filled with 1.0.
+     * @param c1 first column
+     * @param c2 second column
+     * @param c3 third column
+     * @return value of the minor computed has an exact fraction
+     */
+    private BigFraction minor(final BigFraction[] c1, final BigFraction[] c2, final BigFraction[] c3) {
+        return      c2[0].multiply(c3[1]).multiply(c1[2].subtract(c1[3])).
+                add(c2[0].multiply(c3[2]).multiply(c1[3].subtract(c1[1]))).
+                add(c2[0].multiply(c3[3]).multiply(c1[1].subtract(c1[2]))).
+                add(c2[1].multiply(c3[0]).multiply(c1[3].subtract(c1[2]))).
+                add(c2[1].multiply(c3[2]).multiply(c1[0].subtract(c1[3]))).
+                add(c2[1].multiply(c3[3]).multiply(c1[2].subtract(c1[0]))).
+                add(c2[2].multiply(c3[0]).multiply(c1[1].subtract(c1[3]))).
+                add(c2[2].multiply(c3[1]).multiply(c1[3].subtract(c1[0]))).
+                add(c2[2].multiply(c3[3]).multiply(c1[0].subtract(c1[1]))).
+                add(c2[3].multiply(c3[0]).multiply(c1[2].subtract(c1[1]))).
+                add(c2[3].multiply(c3[1]).multiply(c1[0].subtract(c1[2]))).
+                add(c2[3].multiply(c3[2]).multiply(c1[1].subtract(c1[0])));
+    }
+
+}
diff --git a/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/euclidean/twod/enclosing/DiskGenerator.java b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/euclidean/twod/enclosing/DiskGenerator.java
new file mode 100644
index 0000000..52b5626
--- /dev/null
+++ b/commons-geometry-enclosing/src/main/java/org/apache/commons/geometry/euclidean/twod/enclosing/DiskGenerator.java
@@ -0,0 +1,109 @@
+/*
+ * 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.geometry.euclidean.twod.enclosing;
+
+import java.util.List;
+
+import org.apache.commons.numbers.fraction.BigFraction;
+import org.apache.commons.geometry.enclosing.EnclosingBall;
+import org.apache.commons.geometry.enclosing.SupportBallGenerator;
+import org.apache.commons.geometry.euclidean.twod.Cartesian2D;
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+
+/** Class generating an enclosing ball from its support points.
+ */
+public class DiskGenerator implements SupportBallGenerator<Euclidean2D, Cartesian2D> {
+
+    /** {@inheritDoc} */
+    @Override
+    public EnclosingBall<Euclidean2D, Cartesian2D> ballOnSupport(final List<Cartesian2D> support) {
+
+        if (support.size() < 1) {
+            return new EnclosingBall<>(Cartesian2D.ZERO, Double.NEGATIVE_INFINITY);
+        } else {
+            final Cartesian2D vA = support.get(0);
+            if (support.size() < 2) {
+                return new EnclosingBall<>(vA, 0, vA);
+            } else {
+                final Cartesian2D vB = support.get(1);
+                if (support.size() < 3) {
+                    return new EnclosingBall<>(new Cartesian2D(0.5, vA, 0.5, vB),
+                                                                    0.5 * vA.distance(vB),
+                                                                    vA, vB);
+                } else {
+                    final Cartesian2D vC = support.get(2);
+                    // a disk is 2D can be defined as:
+                    // (1)   (x - x_0)^2 + (y - y_0)^2 = r^2
+                    // which can be written:
+                    // (2)   (x^2 + y^2) - 2 x_0 x - 2 y_0 y + (x_0^2 + y_0^2 - r^2) = 0
+                    // or simply:
+                    // (3)   (x^2 + y^2) + a x + b y + c = 0
+                    // with disk center coordinates -a/2, -b/2
+                    // If the disk exists, a, b and c are a non-zero solution to
+                    // [ (x^2  + y^2 )   x    y   1 ]   [ 1 ]   [ 0 ]
+                    // [ (xA^2 + yA^2)   xA   yA  1 ]   [ a ]   [ 0 ]
+                    // [ (xB^2 + yB^2)   xB   yB  1 ] * [ b ] = [ 0 ]
+                    // [ (xC^2 + yC^2)   xC   yC  1 ]   [ c ]   [ 0 ]
+                    // So the determinant of the matrix is zero. Computing this determinant
+                    // by expanding it using the minors m_ij of first row leads to
+                    // (4)   m_11 (x^2 + y^2) - m_12 x + m_13 y - m_14 = 0
+                    // So by identifying equations (2) and (4) we get the coordinates
+                    // of center as:
+                    //      x_0 = +m_12 / (2 m_11)
+                    //      y_0 = -m_13 / (2 m_11)
+                    // Note that the minors m_11, m_12 and m_13 all have the last column
+                    // filled with 1.0, hence simplifying the computation
+                    final BigFraction[] c2 = new BigFraction[] {
+                        new BigFraction(vA.getX()), new BigFraction(vB.getX()), new BigFraction(vC.getX())
+                    };
+                    final BigFraction[] c3 = new BigFraction[] {
+                        new BigFraction(vA.getY()), new BigFraction(vB.getY()), new BigFraction(vC.getY())
+                    };
+                    final BigFraction[] c1 = new BigFraction[] {
+                        c2[0].multiply(c2[0]).add(c3[0].multiply(c3[0])),
+                        c2[1].multiply(c2[1]).add(c3[1].multiply(c3[1])),
+                        c2[2].multiply(c2[2]).add(c3[2].multiply(c3[2]))
+                    };
+                    final BigFraction twoM11  = minor(c2, c3).multiply(2);
+                    final BigFraction m12     = minor(c1, c3);
+                    final BigFraction m13     = minor(c1, c2);
+                    final BigFraction centerX = m12.divide(twoM11);
+                    final BigFraction centerY = m13.divide(twoM11).negate();
+                    final BigFraction dx      = c2[0].subtract(centerX);
+                    final BigFraction dy      = c3[0].subtract(centerY);
+                    final BigFraction r2      = dx.multiply(dx).add(dy.multiply(dy));
+                    return new EnclosingBall<>(new Cartesian2D(centerX.doubleValue(),
+                                                                                 centerY.doubleValue()),
+                                                                    Math.sqrt(r2.doubleValue()),
+                                                                    vA, vB, vC);
+                }
+            }
+        }
+    }
+
+    /** Compute a dimension 3 minor, when 3<sup>d</sup> column is known to be filled with 1.0.
+     * @param c1 first column
+     * @param c2 second column
+     * @return value of the minor computed has an exact fraction
+     */
+    private BigFraction minor(final BigFraction[] c1, final BigFraction[] c2) {
+        return      c2[0].multiply(c1[2].subtract(c1[1])).
+                add(c2[1].multiply(c1[0].subtract(c1[2]))).
+                add(c2[2].multiply(c1[1].subtract(c1[0])));
+    }
+
+}
diff --git a/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/enclosing/WelzlEncloser2DTest.java b/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/enclosing/WelzlEncloser2DTest.java
new file mode 100644
index 0000000..21d9cd6
--- /dev/null
+++ b/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/enclosing/WelzlEncloser2DTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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.geometry.enclosing;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.enclosing.EnclosingBall;
+import org.apache.commons.geometry.enclosing.WelzlEncloser;
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+import org.apache.commons.geometry.euclidean.twod.enclosing.DiskGenerator;
+import org.apache.commons.geometry.euclidean.twod.Cartesian2D;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.junit.Assert;
+import org.junit.Test;
+
+
+public class WelzlEncloser2DTest {
+
+    @Test
+    public void testNullList() {
+        DiskGenerator generator = new DiskGenerator();
+        WelzlEncloser<Euclidean2D, Cartesian2D> encloser =
+                new WelzlEncloser<>(1.0e-10, generator);
+        EnclosingBall<Euclidean2D, Cartesian2D> ball = encloser.enclose(null);
+        Assert.assertTrue(ball.getRadius() < 0);
+    }
+
+    @Test
+    public void testNoPoints() {
+        DiskGenerator generator = new DiskGenerator();
+        WelzlEncloser<Euclidean2D, Cartesian2D> encloser =
+                new WelzlEncloser<>(1.0e-10, generator);
+        EnclosingBall<Euclidean2D, Cartesian2D> ball = encloser.enclose(new ArrayList<Cartesian2D>());
+        Assert.assertTrue(ball.getRadius() < 0);
+    }
+
+    @Test
+    public void testRegularPoints() {
+        List<Cartesian2D> list = buildList(22, 26, 30, 38, 64, 28,  8, 54, 11, 15);
+        checkDisk(list, Arrays.asList(list.get(2), list.get(3), list.get(4)));
+    }
+
+    @Test
+    public void testSolutionOnDiameter() {
+        List<Cartesian2D> list = buildList(22, 26, 30, 38, 64, 28,  8, 54);
+        checkDisk(list, Arrays.asList(list.get(2), list.get(3)));
+    }
+
+    @Test
+    public void testReducingBall1() {
+        List<Cartesian2D> list = buildList(0.05380958511396061, 0.57332359658700000,
+                                        0.99348810731127870, 0.02056421361521466,
+                                        0.01203950647796437, 0.99779675042261860,
+                                        0.00810189987706078, 0.00589246003827815,
+                                        0.00465180821202149, 0.99219972923046940);
+        checkDisk(list, Arrays.asList(list.get(1), list.get(3), list.get(4)));
+    }
+
+    @Test
+    public void testReducingBall2() {
+        List<Cartesian2D> list = buildList(0.016930586154703, 0.333955448537779,
+                                        0.987189104892331, 0.969778855274507,
+                                        0.983696889599935, 0.012904580013266,
+                                        0.013114499572905, 0.034740156356895);
+        checkDisk(list, Arrays.asList(list.get(1), list.get(2), list.get(3)));
+    }
+
+    @Test
+    public void testLargeSamples() {
+        UniformRandomProvider random = RandomSource.create(RandomSource.WELL_1024_A, 0xa2a63cad12c01fb2l);
+        for (int k = 0; k < 100; ++k) {
+            int nbPoints = random.nextInt(10000);
+            List<Cartesian2D> points = new ArrayList<>();
+            for (int i = 0; i < nbPoints; ++i) {
+                double x = random.nextDouble();
+                double y = random.nextDouble();
+                points.add(new Cartesian2D(x, y));
+            }
+            checkDisk(points);
+        }
+    }
+
+    private List<Cartesian2D> buildList(final double ... coordinates) {
+        List<Cartesian2D> list = new ArrayList<>(coordinates.length / 2);
+        for (int i = 0; i < coordinates.length; i += 2) {
+            list.add(new Cartesian2D(coordinates[i], coordinates[i + 1]));
+        }
+        return list;
+    }
+
+    private void checkDisk(List<Cartesian2D> points, List<Cartesian2D> refSupport) {
+
+        EnclosingBall<Euclidean2D, Cartesian2D> disk = checkDisk(points);
+
+        // compare computed disk with expected disk
+        DiskGenerator generator = new DiskGenerator();
+        EnclosingBall<Euclidean2D, Cartesian2D> expected = generator.ballOnSupport(refSupport);
+        Assert.assertEquals(refSupport.size(), disk.getSupportSize());
+        Assert.assertEquals(expected.getRadius(),        disk.getRadius(),        1.0e-10);
+        Assert.assertEquals(expected.getCenter().getX(), disk.getCenter().getX(), 1.0e-10);
+        Assert.assertEquals(expected.getCenter().getY(), disk.getCenter().getY(), 1.0e-10);
+
+        for (Cartesian2D s : disk.getSupport()) {
+            boolean found = false;
+            for (Cartesian2D rs : refSupport) {
+                if (s == rs) {
+                    found = true;
+                }
+            }
+            Assert.assertTrue(found);
+        }
+
+        // check removing any point of the support disk fails to enclose the point
+        for (int i = 0; i < disk.getSupportSize(); ++i) {
+            List<Cartesian2D> reducedSupport = new ArrayList<>();
+            int count = 0;
+            for (Cartesian2D s : disk.getSupport()) {
+                if (count++ != i) {
+                    reducedSupport.add(s);
+                }
+            }
+            EnclosingBall<Euclidean2D, Cartesian2D> reducedDisk = generator.ballOnSupport(reducedSupport);
+            boolean foundOutside = false;
+            for (int j = 0; j < points.size() && !foundOutside; ++j) {
+                if (!reducedDisk.contains(points.get(j), 1.0e-10)) {
+                    foundOutside = true;
+                }
+            }
+            Assert.assertTrue(foundOutside);
+        }
+
+    }
+
+    private EnclosingBall<Euclidean2D, Cartesian2D> checkDisk(List<Cartesian2D> points) {
+
+        WelzlEncloser<Euclidean2D, Cartesian2D> encloser =
+                new WelzlEncloser<>(1.0e-10, new DiskGenerator());
+        EnclosingBall<Euclidean2D, Cartesian2D> disk = encloser.enclose(points);
+
+        // all points are enclosed
+        for (Cartesian2D v : points) {
+            Assert.assertTrue(disk.contains(v, 1.0e-10));
+        }
+
+        for (Cartesian2D v : points) {
+            boolean inSupport = false;
+            for (Cartesian2D s : disk.getSupport()) {
+                if (v == s) {
+                    inSupport = true;
+                }
+            }
+            if (inSupport) {
+                // points on the support should be outside of reduced ball
+                Assert.assertFalse(disk.contains(v, -0.001));
+            }
+        }
+
+        return disk;
+
+    }
+
+}
diff --git a/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/enclosing/WelzlEncloser3DTest.java b/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/enclosing/WelzlEncloser3DTest.java
new file mode 100644
index 0000000..0466b48
--- /dev/null
+++ b/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/enclosing/WelzlEncloser3DTest.java
@@ -0,0 +1,187 @@
+/*
+ * 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.geometry.enclosing;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.apache.commons.rng.sampling.UnitSphereSampler;
+import org.apache.commons.geometry.enclosing.EnclosingBall;
+import org.apache.commons.geometry.enclosing.WelzlEncloser;
+import org.apache.commons.geometry.euclidean.threed.Euclidean3D;
+import org.apache.commons.geometry.euclidean.threed.enclosing.SphereGenerator;
+import org.apache.commons.geometry.euclidean.threed.Cartesian3D;
+
+
+public class WelzlEncloser3DTest {
+
+    @Test
+    public void testNullList() {
+        SphereGenerator generator = new SphereGenerator();
+        WelzlEncloser<Euclidean3D, Cartesian3D> encloser =
+                new WelzlEncloser<>(1.0e-10, generator);
+        EnclosingBall<Euclidean3D, Cartesian3D> ball = encloser.enclose(null);
+        Assert.assertTrue(ball.getRadius() < 0);
+    }
+
+    @Test
+    public void testNoPoints() {
+        SphereGenerator generator = new SphereGenerator();
+        WelzlEncloser<Euclidean3D, Cartesian3D> encloser =
+                new WelzlEncloser<>(1.0e-10, generator);
+        EnclosingBall<Euclidean3D, Cartesian3D> ball = encloser.enclose(new ArrayList<Cartesian3D>());
+        Assert.assertTrue(ball.getRadius() < 0);
+    }
+
+    @Test
+    public void testReducingBall() {
+        List<Cartesian3D> list =
+                Arrays.asList(new Cartesian3D(-7.140397329568118, -16.571661242582177,  11.714458961735405),
+                              new Cartesian3D(-7.137986707455888, -16.570767323375720,  11.708602108715928),
+                              new Cartesian3D(-7.139185068549035, -16.570891204702250,  11.715554057357394),
+                              new Cartesian3D(-7.142682716997507, -16.571609818234290,  11.710787934580328),
+                              new Cartesian3D(-7.139018392423351, -16.574405614157020,  11.710518716711425),
+                              new Cartesian3D(-7.140870659936730, -16.567993074240455,  11.710914678204503),
+                              new Cartesian3D(-7.136350173659562, -16.570498228820930,  11.713965225900928),
+                              new Cartesian3D(-7.141675762759172, -16.572852471407028,  11.714033471449508),
+                              new Cartesian3D(-7.140453077221105, -16.570212820780647,  11.708624578004980),
+                              new Cartesian3D(-7.140322188726825, -16.574152894557717,  11.710305611121410),
+                              new Cartesian3D(-7.141116131477088, -16.574061164624560,  11.712938509321699));
+        WelzlEncloser<Euclidean3D, Cartesian3D> encloser =
+                new WelzlEncloser<>(1.0e-10, new SphereGenerator());
+        EnclosingBall<Euclidean3D, Cartesian3D> ball = encloser.enclose(list);
+        Assert.assertTrue(ball.getRadius() > 0);
+    }
+
+    @Test
+    public void testInfiniteLoop() {
+        // this test used to generate an infinite loop
+        List<Cartesian3D> list =
+                Arrays.asList(new Cartesian3D( -0.89227075512164380,  -2.89317694645713900,  14.84572323743355500),
+                              new Cartesian3D( -0.92099498940693580,  -2.31086108263908940,  12.92071026467688300),
+                              new Cartesian3D( -0.85227999411005200,  -3.06314731441320730,  15.40163831651287000),
+                              new Cartesian3D( -1.77399413020785970,  -3.65630391378114260,  14.13190097751873400),
+                              new Cartesian3D(  0.33157833272465354,  -2.22813591757792160,  14.21225234159008200),
+                              new Cartesian3D( -1.53065579165484400,  -1.65692084770139570,  14.61483055714788500),
+                              new Cartesian3D( -1.08457093941217140,  -1.96100325935602980,  13.09265170575555000),
+                              new Cartesian3D(  0.30029469589708850,  -3.05470831395667370,  14.56352400426342600),
+                              new Cartesian3D( -0.95007443938638460,  -1.86810946486118360,  15.14491234340057000),
+                              new Cartesian3D( -1.89661503804130830,  -2.17004080885185860,  14.81235128513927000),
+                              new Cartesian3D( -0.72193328761607530,  -1.44513142833618270,  14.52355724218561800),
+                              new Cartesian3D( -0.26895980939606550,  -3.69512371522084140,  14.72272846327652000),
+                              new Cartesian3D( -1.53501693431786170,  -3.25055166611021900,  15.15509062584274800),
+                              new Cartesian3D( -0.71727553535519410,  -3.62284279460799100,  13.26256700929380700),
+                              new Cartesian3D( -0.30220950676137365,  -3.25410412500779070,  13.13682612771606000),
+                              new Cartesian3D( -0.04543996608267075,  -1.93081853923797750,  14.79497997883171400),
+                              new Cartesian3D( -1.53348892951571640,  -3.66688919703524900,  14.73095600812074200),
+                              new Cartesian3D( -0.98034899533935820,  -3.34004481162763960,  13.03245014017556800));
+
+        WelzlEncloser<Euclidean3D, Cartesian3D> encloser =
+                new WelzlEncloser<>(1.0e-10, new SphereGenerator());
+        EnclosingBall<Euclidean3D, Cartesian3D> ball = encloser.enclose(list);
+        Assert.assertTrue(ball.getRadius() > 0);
+    }
+
+    @Test
+    public void testLargeSamples() throws IOException {
+        final UniformRandomProvider random = RandomSource.create(RandomSource.WELL_1024_A,
+                                                                 0x35ddecfc78131e1dl);
+        final UnitSphereSampler sr = new UnitSphereSampler(3, random);
+        for (int k = 0; k < 50; ++k) {
+
+            // define the reference sphere we want to compute
+            double d = 25 * random.nextDouble();
+            double refRadius = 10 * random.nextDouble();
+            Cartesian3D refCenter = new Cartesian3D(d, new Cartesian3D(sr.nextVector()));
+            // set up a large sample inside the reference sphere
+            int nbPoints = random.nextInt(1000);
+            List<Cartesian3D> points = new ArrayList<>();
+            for (int i = 0; i < nbPoints; ++i) {
+                double r = refRadius * random.nextDouble();
+                points.add(new Cartesian3D(1.0, refCenter, r, new Cartesian3D(sr.nextVector())));
+            }
+
+            // test we find a sphere at most as large as the one used for random drawings
+            checkSphere(points, refRadius);
+
+        }
+    }
+
+    private void checkSphere(List<Cartesian3D> points, double refRadius) {
+
+        EnclosingBall<Euclidean3D, Cartesian3D> sphere = checkSphere(points);
+
+        // compare computed sphere with bounding sphere
+        Assert.assertTrue(sphere.getRadius() <= refRadius);
+
+        // check removing any point of the support Sphere fails to enclose the point
+        for (int i = 0; i < sphere.getSupportSize(); ++i) {
+            List<Cartesian3D> reducedSupport = new ArrayList<>();
+            int count = 0;
+            for (Cartesian3D s : sphere.getSupport()) {
+                if (count++ != i) {
+                    reducedSupport.add(s);
+                }
+            }
+            EnclosingBall<Euclidean3D, Cartesian3D> reducedSphere =
+                    new SphereGenerator().ballOnSupport(reducedSupport);
+            boolean foundOutside = false;
+            for (int j = 0; j < points.size() && !foundOutside; ++j) {
+                if (!reducedSphere.contains(points.get(j), 1.0e-10)) {
+                    foundOutside = true;
+                }
+            }
+            Assert.assertTrue(foundOutside);
+        }
+
+    }
+
+    private EnclosingBall<Euclidean3D, Cartesian3D> checkSphere(List<Cartesian3D> points) {
+
+        WelzlEncloser<Euclidean3D, Cartesian3D> encloser =
+                new WelzlEncloser<>(1.0e-10, new SphereGenerator());
+        EnclosingBall<Euclidean3D, Cartesian3D> Sphere = encloser.enclose(points);
+
+        // all points are enclosed
+        for (Cartesian3D v : points) {
+            Assert.assertTrue(Sphere.contains(v, 1.0e-10));
+        }
+
+        for (Cartesian3D v : points) {
+            boolean inSupport = false;
+            for (Cartesian3D s : Sphere.getSupport()) {
+                if (v == s) {
+                    inSupport = true;
+                }
+            }
+            if (inSupport) {
+                // points on the support should be outside of reduced ball
+                Assert.assertFalse(Sphere.contains(v, -0.001));
+            }
+        }
+
+        return Sphere;
+
+    }
+
+}
diff --git a/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/euclidean/threed/enclosing/SphereGeneratorTest.java b/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/euclidean/threed/enclosing/SphereGeneratorTest.java
new file mode 100644
index 0000000..1adbaa5
--- /dev/null
+++ b/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/euclidean/threed/enclosing/SphereGeneratorTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.geometry.euclidean.threed.enclosing;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.enclosing.EnclosingBall;
+import org.apache.commons.geometry.euclidean.threed.Cartesian3D;
+import org.apache.commons.geometry.euclidean.threed.Euclidean3D;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.sampling.UnitSphereSampler;
+import org.apache.commons.rng.simple.RandomSource;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class SphereGeneratorTest {
+
+    @Test
+    public void testSupport0Point() {
+        List<Cartesian3D> support = Arrays.asList(new Cartesian3D[0]);
+        EnclosingBall<Euclidean3D, Cartesian3D> sphere = new SphereGenerator().ballOnSupport(support);
+        Assert.assertTrue(sphere.getRadius() < 0);
+        Assert.assertEquals(0, sphere.getSupportSize());
+        Assert.assertEquals(0, sphere.getSupport().length);
+    }
+
+    @Test
+    public void testSupport1Point() {
+        List<Cartesian3D> support = Arrays.asList(new Cartesian3D(1, 2, 3));
+        EnclosingBall<Euclidean3D, Cartesian3D> sphere = new SphereGenerator().ballOnSupport(support);
+        Assert.assertEquals(0.0, sphere.getRadius(), 1.0e-10);
+        Assert.assertTrue(sphere.contains(support.get(0)));
+        Assert.assertTrue(sphere.contains(support.get(0), 0.5));
+        Assert.assertFalse(sphere.contains(new Cartesian3D(support.get(0).getX() + 0.1,
+                                                        support.get(0).getY() + 0.1,
+                                                        support.get(0).getZ() + 0.1),
+                                           0.001));
+        Assert.assertTrue(sphere.contains(new Cartesian3D(support.get(0).getX() + 0.1,
+                                                       support.get(0).getY() + 0.1,
+                                                       support.get(0).getZ() + 0.1),
+                                          0.5));
+        Assert.assertEquals(0, support.get(0).distance(sphere.getCenter()), 1.0e-10);
+        Assert.assertEquals(1, sphere.getSupportSize());
+        Assert.assertTrue(support.get(0) == sphere.getSupport()[0]);
+    }
+
+    @Test
+    public void testSupport2Points() {
+        List<Cartesian3D> support = Arrays.asList(new Cartesian3D(1, 0, 0),
+                                               new Cartesian3D(3, 0, 0));
+        EnclosingBall<Euclidean3D, Cartesian3D> sphere = new SphereGenerator().ballOnSupport(support);
+        Assert.assertEquals(1.0, sphere.getRadius(), 1.0e-10);
+        int i = 0;
+        for (Cartesian3D v : support) {
+            Assert.assertTrue(sphere.contains(v));
+            Assert.assertEquals(1.0, v.distance(sphere.getCenter()), 1.0e-10);
+            Assert.assertTrue(v == sphere.getSupport()[i++]);
+        }
+        Assert.assertTrue(sphere.contains(new Cartesian3D(2, 0.9, 0)));
+        Assert.assertFalse(sphere.contains(Cartesian3D.ZERO));
+        Assert.assertEquals(0.0, new Cartesian3D(2, 0, 0).distance(sphere.getCenter()), 1.0e-10);
+        Assert.assertEquals(2, sphere.getSupportSize());
+    }
+
+    @Test
+    public void testSupport3Points() {
+        List<Cartesian3D> support = Arrays.asList(new Cartesian3D(1, 0, 0),
+                                               new Cartesian3D(3, 0, 0),
+                                               new Cartesian3D(2, 2, 0));
+        EnclosingBall<Euclidean3D, Cartesian3D> sphere = new SphereGenerator().ballOnSupport(support);
+        Assert.assertEquals(5.0 / 4.0, sphere.getRadius(), 1.0e-10);
+        int i = 0;
+        for (Cartesian3D v : support) {
+            Assert.assertTrue(sphere.contains(v));
+            Assert.assertEquals(5.0 / 4.0, v.distance(sphere.getCenter()), 1.0e-10);
+            Assert.assertTrue(v == sphere.getSupport()[i++]);
+        }
+        Assert.assertTrue(sphere.contains(new Cartesian3D(2, 0.9, 0)));
+        Assert.assertFalse(sphere.contains(new Cartesian3D(0.9,  0, 0)));
+        Assert.assertFalse(sphere.contains(new Cartesian3D(3.1,  0, 0)));
+        Assert.assertTrue(sphere.contains(new Cartesian3D(2.0, -0.499, 0)));
+        Assert.assertFalse(sphere.contains(new Cartesian3D(2.0, -0.501, 0)));
+        Assert.assertTrue(sphere.contains(new Cartesian3D(2.0, 3.0 / 4.0, -1.249)));
+        Assert.assertFalse(sphere.contains(new Cartesian3D(2.0, 3.0 / 4.0, -1.251)));
+        Assert.assertEquals(0.0, new Cartesian3D(2.0, 3.0 / 4.0, 0).distance(sphere.getCenter()), 1.0e-10);
+        Assert.assertEquals(3, sphere.getSupportSize());
+    }
+
+    @Test
+    public void testSupport4Points() {
+        List<Cartesian3D> support = Arrays.asList(new Cartesian3D(17, 14,  18),
+                                               new Cartesian3D(11, 14,  22),
+                                               new Cartesian3D( 2, 22,  17),
+                                               new Cartesian3D(22, 11, -10));
+        EnclosingBall<Euclidean3D, Cartesian3D> sphere = new SphereGenerator().ballOnSupport(support);
+        Assert.assertEquals(25.0, sphere.getRadius(), 1.0e-10);
+        int i = 0;
+        for (Cartesian3D v : support) {
+            Assert.assertTrue(sphere.contains(v));
+            Assert.assertEquals(25.0, v.distance(sphere.getCenter()), 1.0e-10);
+            Assert.assertTrue(v == sphere.getSupport()[i++]);
+        }
+        Assert.assertTrue(sphere.contains (new Cartesian3D(-22.999, 2, 2)));
+        Assert.assertFalse(sphere.contains(new Cartesian3D(-23.001, 2, 2)));
+        Assert.assertTrue(sphere.contains (new Cartesian3D( 26.999, 2, 2)));
+        Assert.assertFalse(sphere.contains(new Cartesian3D( 27.001, 2, 2)));
+        Assert.assertTrue(sphere.contains (new Cartesian3D(2, -22.999, 2)));
+        Assert.assertFalse(sphere.contains(new Cartesian3D(2, -23.001, 2)));
+        Assert.assertTrue(sphere.contains (new Cartesian3D(2,  26.999, 2)));
+        Assert.assertFalse(sphere.contains(new Cartesian3D(2,  27.001, 2)));
+        Assert.assertTrue(sphere.contains (new Cartesian3D(2, 2, -22.999)));
+        Assert.assertFalse(sphere.contains(new Cartesian3D(2, 2, -23.001)));
+        Assert.assertTrue(sphere.contains (new Cartesian3D(2, 2,  26.999)));
+        Assert.assertFalse(sphere.contains(new Cartesian3D(2, 2,  27.001)));
+        Assert.assertEquals(0.0, new Cartesian3D(2.0, 2.0, 2.0).distance(sphere.getCenter()), 1.0e-10);
+        Assert.assertEquals(4, sphere.getSupportSize());
+    }
+
+    @Test
+    public void testRandom() {
+        final UniformRandomProvider random = RandomSource.create(RandomSource.WELL_1024_A,
+                                                                 0xd015982e9f31ee04l);
+        final UnitSphereSampler sr = new UnitSphereSampler(3, random);
+        for (int i = 0; i < 100; ++i) {
+            double d = 25 * random.nextDouble();
+            double refRadius = 10 * random.nextDouble();
+            Cartesian3D refCenter = new Cartesian3D(d, new Cartesian3D(sr.nextVector()));
+            List<Cartesian3D> support = new ArrayList<>();
+            for (int j = 0; j < 5; ++j) {
+                support.add(new Cartesian3D(1.0, refCenter, refRadius, new Cartesian3D(sr.nextVector())));
+            }
+            EnclosingBall<Euclidean3D, Cartesian3D> sphere = new SphereGenerator().ballOnSupport(support);
+            Assert.assertEquals(0.0, refCenter.distance(sphere.getCenter()), 4e-7 * refRadius);
+            Assert.assertEquals(refRadius, sphere.getRadius(), 1e-7 * refRadius);
+        }
+    }
+
+    @Test
+    public void testDegeneratedCase() {
+       final List<Cartesian3D> support =
+               Arrays.asList(new Cartesian3D(Math.scalb(-8039905610797991.0, -50),   //   -7.140870659936730
+                                          Math.scalb(-4663475464714142.0, -48),   //  -16.567993074240455
+                                          Math.scalb( 6592658872616184.0, -49)),  //   11.710914678204503
+                             new Cartesian3D(Math.scalb(-8036658568968473.0, -50),   //   -7.137986707455888
+                                          Math.scalb(-4664256346424880.0, -48),   //  -16.570767323375720
+                                          Math.scalb( 6591357011730307.0, -49)),  //  11.708602108715928)
+                             new Cartesian3D(Math.scalb(-8037820142977230.0, -50),   //   -7.139018392423351
+                                          Math.scalb(-4665280434237813.0, -48),   //  -16.574405614157020
+                                          Math.scalb( 6592435966112099.0, -49)),  //   11.710518716711425
+                             new Cartesian3D(Math.scalb(-8038007803611611.0, -50),   //   -7.139185068549035
+                                          Math.scalb(-4664291215918380.0, -48),   //  -16.570891204702250
+                                          Math.scalb( 6595270610894208.0, -49))); //   11.715554057357394
+        EnclosingBall<Euclidean3D, Cartesian3D> sphere = new SphereGenerator().ballOnSupport(support);
+
+        // the following values have been computed using Emacs calc with exact arithmetic from the
+        // rational representation corresponding to the scalb calls (i.e. -8039905610797991/2^50, ...)
+        // The results were converted to decimal representation rounded to 1.0e-30 when writing the reference
+        // values in this test
+        Assert.assertEquals(  0.003616820213530053297575846168, sphere.getRadius(),        1.0e-20);
+        Assert.assertEquals( -7.139325643360503322823511839511, sphere.getCenter().getX(), 1.0e-20);
+        Assert.assertEquals(-16.571096474251747245361467833760, sphere.getCenter().getY(), 1.0e-20);
+        Assert.assertEquals( 11.711945804096960876521111630800, sphere.getCenter().getZ(), 1.0e-20);
+
+        for (Cartesian3D v : support) {
+            Assert.assertTrue(sphere.contains(v, 1.0e-14));
+        }
+
+    }
+
+}
diff --git a/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/euclidean/twod/enclosing/DiskGeneratorTest.java b/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/euclidean/twod/enclosing/DiskGeneratorTest.java
new file mode 100644
index 0000000..4fbe657
--- /dev/null
+++ b/commons-geometry-enclosing/src/test/java/org/apache/commons/geometry/euclidean/twod/enclosing/DiskGeneratorTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.geometry.euclidean.twod.enclosing;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Assert;
+import org.junit.Test;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.apache.commons.rng.sampling.UnitSphereSampler;
+import org.apache.commons.geometry.enclosing.EnclosingBall;
+import org.apache.commons.geometry.euclidean.twod.enclosing.DiskGenerator;
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+import org.apache.commons.geometry.euclidean.twod.Cartesian2D;
+
+
+public class DiskGeneratorTest {
+
+    @Test
+    public void testSupport0Point() {
+        List<Cartesian2D> support = Arrays.asList(new Cartesian2D[0]);
+        EnclosingBall<Euclidean2D, Cartesian2D> disk = new DiskGenerator().ballOnSupport(support);
+        Assert.assertTrue(disk.getRadius() < 0);
+        Assert.assertEquals(0, disk.getSupportSize());
+        Assert.assertEquals(0, disk.getSupport().length);
+    }
+
+    @Test
+    public void testSupport1Point() {
+        List<Cartesian2D> support = Arrays.asList(new Cartesian2D(1, 2));
+        EnclosingBall<Euclidean2D, Cartesian2D> disk = new DiskGenerator().ballOnSupport(support);
+        Assert.assertEquals(0.0, disk.getRadius(), 1.0e-10);
+        Assert.assertTrue(disk.contains(support.get(0)));
+        Assert.assertTrue(disk.contains(support.get(0), 0.5));
+        Assert.assertFalse(disk.contains(new Cartesian2D(support.get(0).getX() + 0.1,
+                                                      support.get(0).getY() - 0.1),
+                                         0.001));
+        Assert.assertTrue(disk.contains(new Cartesian2D(support.get(0).getX() + 0.1,
+                                                     support.get(0).getY() - 0.1),
+                                        0.5));
+        Assert.assertEquals(0, support.get(0).distance(disk.getCenter()), 1.0e-10);
+        Assert.assertEquals(1, disk.getSupportSize());
+        Assert.assertTrue(support.get(0) == disk.getSupport()[0]);
+    }
+
+    @Test
+    public void testSupport2Points() {
+        List<Cartesian2D> support = Arrays.asList(new Cartesian2D(1, 0),
+                                               new Cartesian2D(3, 0));
+        EnclosingBall<Euclidean2D, Cartesian2D> disk = new DiskGenerator().ballOnSupport(support);
+        Assert.assertEquals(1.0, disk.getRadius(), 1.0e-10);
+        int i = 0;
+        for (Cartesian2D v : support) {
+            Assert.assertTrue(disk.contains(v));
+            Assert.assertEquals(1.0, v.distance(disk.getCenter()), 1.0e-10);
+            Assert.assertTrue(v == disk.getSupport()[i++]);
+        }
+        Assert.assertTrue(disk.contains(new Cartesian2D(2, 0.9)));
+        Assert.assertFalse(disk.contains(Cartesian2D.ZERO));
+        Assert.assertEquals(0.0, new Cartesian2D(2, 0).distance(disk.getCenter()), 1.0e-10);
+        Assert.assertEquals(2, disk.getSupportSize());
+    }
+
+    @Test
+    public void testSupport3Points() {
+        List<Cartesian2D> support = Arrays.asList(new Cartesian2D(1, 0),
+                                               new Cartesian2D(3, 0),
+                                               new Cartesian2D(2, 2));
+        EnclosingBall<Euclidean2D, Cartesian2D> disk = new DiskGenerator().ballOnSupport(support);
+        Assert.assertEquals(5.0 / 4.0, disk.getRadius(), 1.0e-10);
+        int i = 0;
+        for (Cartesian2D v : support) {
+            Assert.assertTrue(disk.contains(v));
+            Assert.assertEquals(5.0 / 4.0, v.distance(disk.getCenter()), 1.0e-10);
+            Assert.assertTrue(v == disk.getSupport()[i++]);
+        }
+        Assert.assertTrue(disk.contains(new Cartesian2D(2, 0.9)));
+        Assert.assertFalse(disk.contains(new Cartesian2D(0.9,  0)));
+        Assert.assertFalse(disk.contains(new Cartesian2D(3.1,  0)));
+        Assert.assertTrue(disk.contains(new Cartesian2D(2.0, -0.499)));
+        Assert.assertFalse(disk.contains(new Cartesian2D(2.0, -0.501)));
+        Assert.assertEquals(0.0, new Cartesian2D(2.0, 3.0 / 4.0).distance(disk.getCenter()), 1.0e-10);
+        Assert.assertEquals(3, disk.getSupportSize());
+    }
+
+    @Test
+    public void testRandom() {
+        final UniformRandomProvider random = RandomSource.create(RandomSource.WELL_1024_A,
+                                                                 0x12faa818373ffe90l);
+        final UnitSphereSampler sr = new UnitSphereSampler(2, random);
+        for (int i = 0; i < 500; ++i) {
+            double d = 25 * random.nextDouble();
+            double refRadius = 10 * random.nextDouble();
+            Cartesian2D refCenter = new Cartesian2D(d, new Cartesian2D(sr.nextVector()));
+            List<Cartesian2D> support = new ArrayList<>();
+            for (int j = 0; j < 3; ++j) {
+                support.add(new Cartesian2D(1.0, refCenter, refRadius, new Cartesian2D(sr.nextVector())));
+            }
+            EnclosingBall<Euclidean2D, Cartesian2D> disk = new DiskGenerator().ballOnSupport(support);
+            Assert.assertEquals(0.0, refCenter.distance(disk.getCenter()), 3e-9 * refRadius);
+            Assert.assertEquals(refRadius, disk.getRadius(), 7e-10 * refRadius);
+        }
+
+    }
+}
diff --git a/commons-geometry-euclidean-twod/pom.xml b/commons-geometry-euclidean-twod/pom.xml
deleted file mode 100644
index c077816..0000000
--- a/commons-geometry-euclidean-twod/pom.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-<?xml version="1.0"?>
-<!--
-   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.
--->
-<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd";
-         xmlns="http://maven.apache.org/POM/4.0.0";
-         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";>
-  <modelVersion>4.0.0</modelVersion>
-
-  <parent>
-    <groupId>org.apache.commons</groupId>
-    <artifactId>commons-geometry-parent</artifactId>
-    <version>1.0-SNAPSHOT</version>
-  </parent>
-
-  <groupId>org.apache.commons</groupId>
-  <artifactId>commons-geometry-euclidean-twod</artifactId>
-  <version>1.0-SNAPSHOT</version>
-  <name>Apache Commons Geometry Two-Dimensional Euclidean Space</name>
-
-  <description></description>
-
-  <properties>
-    <!-- OSGi -->
-    <commons.osgi.symbolicName>org.apache.commons.geometry.euclidean.twod</commons.osgi.symbolicName>
-    <commons.osgi.export>org.apache.commons.geometry.euclidean.twod</commons.osgi.export>
-    <!-- Java 9+ -->
-    <commons.automatic.module.name>org.apache.commons.geometry.euclidean.twod</commons.automatic.module.name>
-    <!-- Workaround to avoid duplicating config files. -->
-    <geometry.parent.dir>${basedir}/..</geometry.parent.dir>
-  </properties>
-
-</project>
diff --git a/commons-geometry-euclidean/pom.xml b/commons-geometry-euclidean/pom.xml
new file mode 100644
index 0000000..6dfece6
--- /dev/null
+++ b/commons-geometry-euclidean/pom.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0"?>
+<!--
+   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.
+-->
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd";
+         xmlns="http://maven.apache.org/POM/4.0.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";>
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.commons</groupId>
+    <artifactId>commons-geometry-parent</artifactId>
+    <version>1.0-SNAPSHOT</version>
+  </parent>
+
+  <groupId>org.apache.commons</groupId>
+  <artifactId>commons-geometry-euclidean</artifactId>
+  <version>1.0-SNAPSHOT</version>
+  <name>Apache Commons Geometry Euclidean</name>
+
+  <description>Geometric primitives for euclidean space.</description>
+
+  <properties>
+    <!-- OSGi -->
+    <commons.osgi.symbolicName>org.apache.commons.geometry.euclidean</commons.osgi.symbolicName>
+    <commons.osgi.export>org.apache.commons.geometry.euclidean</commons.osgi.export>
+    <!-- Java 9+ -->
+    <commons.automatic.module.name>org.apache.commons.geometry.euclidean</commons.automatic.module.name>
+    <!-- Workaround to avoid duplicating config files. -->
+    <geometry.parent.dir>${basedir}/..</geometry.parent.dir>
+  </properties>
+  
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-geometry-core</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-numbers-core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-numbers-arrays</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-numbers-angle</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-geometry-core</artifactId>
+      <version>${project.version}</version>
+      <classifier>tests</classifier>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-rng-client-api</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-rng-simple</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-rng-sampling</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Cartesian1D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Cartesian1D.java
new file mode 100644
index 0000000..4517616
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Cartesian1D.java
@@ -0,0 +1,382 @@
+/*
+ * 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.geometry.euclidean.oned;
+
+import java.text.NumberFormat;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+import org.apache.commons.geometry.core.Vector;
+
+/** This class represents a 1D point or a 1D vector.
+ * <p>An instance of Cartesian1D represents the point with the corresponding
+ * Cartesian coordinates.</p>
+ * <p>An instance of Cartesian1D also represents the vector which begins at
+ * the origin and ends at the point corresponding to the coordinates.</p>
+ * <p>Instances of this class are guaranteed to be immutable.</p>
+ */
+public class Cartesian1D extends Vector1D implements Point<Euclidean1D> {
+
+    /** Origin (coordinates: 0). */
+    public static final Cartesian1D ZERO = new Cartesian1D(0.0);
+
+    /** Unit (coordinates: 1). */
+    public static final Cartesian1D ONE  = new Cartesian1D(1.0);
+
+    // CHECKSTYLE: stop ConstantName
+    /** A vector with all coordinates set to NaN. */
+    public static final Cartesian1D NaN = new Cartesian1D(Double.NaN);
+    // CHECKSTYLE: resume ConstantName
+
+    /** A vector with all coordinates set to positive infinity. */
+    public static final Cartesian1D POSITIVE_INFINITY =
+        new Cartesian1D(Double.POSITIVE_INFINITY);
+
+    /** A vector with all coordinates set to negative infinity. */
+    public static final Cartesian1D NEGATIVE_INFINITY =
+        new Cartesian1D(Double.NEGATIVE_INFINITY);
+
+    /** Serializable UID. */
+    private static final long serialVersionUID = 7556674948671647925L;
+
+    /** Abscissa. */
+    private final double x;
+
+    /** Simple constructor.
+     * Build a vector from its coordinates
+     * @param x abscissa
+     * @see #getX()
+     */
+    public Cartesian1D(double x) {
+        this.x = x;
+    }
+
+    /** Multiplicative constructor
+     * Build a vector from another one and a scale factor.
+     * The vector built will be a * u
+     * @param a scale factor
+     * @param u base (unscaled) vector
+     */
+    public Cartesian1D(double a, Cartesian1D u) {
+        this.x = a * u.x;
+    }
+
+    /** Linear constructor
+     * Build a vector from two other ones and corresponding scale factors.
+     * The vector built will be a1 * u1 + a2 * u2
+     * @param a1 first scale factor
+     * @param u1 first base (unscaled) vector
+     * @param a2 second scale factor
+     * @param u2 second base (unscaled) vector
+     */
+    public Cartesian1D(double a1, Cartesian1D u1, double a2, Cartesian1D u2) {
+        this.x = a1 * u1.x + a2 * u2.x;
+    }
+
+    /** Linear constructor
+     * Build a vector from three other ones and corresponding scale factors.
+     * The vector built will be a1 * u1 + a2 * u2 + a3 * u3
+     * @param a1 first scale factor
+     * @param u1 first base (unscaled) vector
+     * @param a2 second scale factor
+     * @param u2 second base (unscaled) vector
+     * @param a3 third scale factor
+     * @param u3 third base (unscaled) vector
+     */
+    public Cartesian1D(double a1, Cartesian1D u1, double a2, Cartesian1D u2,
+                   double a3, Cartesian1D u3) {
+        this.x = a1 * u1.x + a2 * u2.x + a3 * u3.x;
+    }
+
+    /** Linear constructor
+     * Build a vector from four other ones and corresponding scale factors.
+     * The vector built will be a1 * u1 + a2 * u2 + a3 * u3 + a4 * u4
+     * @param a1 first scale factor
+     * @param u1 first base (unscaled) vector
+     * @param a2 second scale factor
+     * @param u2 second base (unscaled) vector
+     * @param a3 third scale factor
+     * @param u3 third base (unscaled) vector
+     * @param a4 fourth scale factor
+     * @param u4 fourth base (unscaled) vector
+     */
+    public Cartesian1D(double a1, Cartesian1D u1, double a2, Cartesian1D u2,
+                   double a3, Cartesian1D u3, double a4, Cartesian1D u4) {
+        this.x = a1 * u1.x + a2 * u2.x + a3 * u3.x + a4 * u4.x;
+    }
+
+    /** Get the abscissa of the vector.
+     * @return abscissa of the vector
+     * @see #Cartesian1D(double)
+     */
+    @Override
+    public double getX() {
+        return x;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Space getSpace() {
+        return Euclidean1D.getInstance();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian1D getZero() {
+        return ZERO;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getNorm1() {
+        return Math.abs(x);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getNorm() {
+        return Math.abs(x);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getNormSq() {
+        return x * x;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getNormInf() {
+        return Math.abs(x);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian1D add(Vector<Euclidean1D> v) {
+        Cartesian1D v1 = (Cartesian1D) v;
+        return new Cartesian1D(x + v1.getX());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian1D add(double factor, Vector<Euclidean1D> v) {
+        Cartesian1D v1 = (Cartesian1D) v;
+        return new Cartesian1D(x + factor * v1.getX());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian1D subtract(Vector<Euclidean1D> p) {
+        Cartesian1D p3 = (Cartesian1D) p;
+        return new Cartesian1D(x - p3.x);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian1D subtract(double factor, Vector<Euclidean1D> v) {
+        Cartesian1D v1 = (Cartesian1D) v;
+        return new Cartesian1D(x - factor * v1.getX());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian1D normalize() throws IllegalStateException {
+        double s = getNorm();
+        if (s == 0) {
+            throw new IllegalStateException("Norm is zero");
+        }
+        return scalarMultiply(1 / s);
+    }
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian1D negate() {
+        return new Cartesian1D(-x);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian1D scalarMultiply(double a) {
+        return new Cartesian1D(a * x);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isNaN() {
+        return Double.isNaN(x);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isInfinite() {
+        return !isNaN() && Double.isInfinite(x);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double distance1(Vector<Euclidean1D> p) {
+        Cartesian1D p1 = (Cartesian1D) p;
+        final double dx = Math.abs(p1.x - x);
+        return dx;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double distance(Point<Euclidean1D> p) {
+        return distance((Cartesian1D) p);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double distance(Vector<Euclidean1D> v) {
+        return distance((Cartesian1D) v);
+    }
+
+    /** Compute the distance between the instance and other coordinates.
+     * @param c other coordinates
+     * @return the distance between the instance and c
+     */
+    public double distance(Cartesian1D c) {
+        final double dx = c.x - x;
+        return Math.abs(dx);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double distanceInf(Vector<Euclidean1D> p) {
+        Cartesian1D p1 = (Cartesian1D) p;
+        final double dx = Math.abs(p1.x - x);
+        return dx;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double distanceSq(Vector<Euclidean1D> p) {
+        Cartesian1D p1 = (Cartesian1D) p;
+        final double dx = p1.x - x;
+        return dx * dx;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double dotProduct(final Vector<Euclidean1D> v) {
+        final Cartesian1D v1 = (Cartesian1D) v;
+        return x * v1.x;
+    }
+
+    /** Compute the distance between two points according to the L<sub>2</sub> norm.
+     * <p>Calling this method is equivalent to calling:
+     * <code>p1.subtract(p2).getNorm()</code> except that no intermediate
+     * vector is built</p>
+     * @param p1 first vector
+     * @param p2 second vector
+     * @return the distance between p1 and p2 according to the L<sub>2</sub> norm
+     */
+    public static double distance(Cartesian1D p1, Cartesian1D p2) {
+        return p1.distance(p2);
+    }
+
+    /** Compute the distance between two points according to the L<sub>&infin;</sub> norm.
+     * <p>Calling this method is equivalent to calling:
+     * <code>p1.subtract(p2).getNormInf()</code> except that no intermediate
+     * vector is built</p>
+     * @param p1 first vector
+     * @param p2 second vector
+     * @return the distance between p1 and p2 according to the L<sub>&infin;</sub> norm
+     */
+    public static double distanceInf(Cartesian1D p1, Cartesian1D p2) {
+        return p1.distanceInf(p2);
+    }
+
+    /** Compute the square of the distance between two points.
+     * <p>Calling this method is equivalent to calling:
+     * <code>p1.subtract(p2).getNormSq()</code> except that no intermediate
+     * vector is built</p>
+     * @param p1 first vector
+     * @param p2 second vector
+     * @return the square of the distance between p1 and p2
+     */
+    public static double distanceSq(Cartesian1D p1, Cartesian1D p2) {
+        return p1.distanceSq(p2);
+    }
+
+    /**
+     * Test for the equality of two 1D vectors.
+     * <p>
+     * If all coordinates of two 1D vectors are exactly the same, and none are
+     * <code>Double.NaN</code>, the two 1D vectors are considered to be equal.
+     * </p>
+     * <p>
+     * <code>NaN</code> coordinates are considered to affect globally the vector
+     * and be equals to each other - i.e, if either (or all) coordinates of the
+     * 1D vector are equal to <code>Double.NaN</code>, the 1D vector is equal to
+     * {@link #NaN}.
+     * </p>
+     *
+     * @param other Object to test for equality to this
+     * @return true if two 1D vector objects are equal, false if
+     *         object is null, not an instance of Cartesian1D, or
+     *         not equal to this Cartesian1D instance
+     *
+     */
+    @Override
+    public boolean equals(Object other) {
+
+        if (this == other) {
+            return true;
+        }
+
+        if (other instanceof Cartesian1D) {
+            final Cartesian1D rhs = (Cartesian1D)other;
+            if (rhs.isNaN()) {
+                return this.isNaN();
+            }
+
+            return x == rhs.x;
+        }
+        return false;
+    }
+
+    /**
+     * Get a hashCode for the 1D vector.
+     * <p>
+     * All NaN values have the same hash code.</p>
+     *
+     * @return a hash code value for this object
+     */
+    @Override
+    public int hashCode() {
+        if (isNaN()) {
+            return 7785;
+        }
+        return 997 * Double.hashCode(x);
+    }
+
+    /** Get a string representation of this vector.
+     * @return a string representation of this vector
+     */
+    @Override
+    public String toString() {
+        return toString(NumberFormat.getInstance());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString(final NumberFormat format) {
+        return "{" + format.format(x) + "}";
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Euclidean1D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Euclidean1D.java
new file mode 100644
index 0000000..394e317
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Euclidean1D.java
@@ -0,0 +1,80 @@
+/*
+ * 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.geometry.euclidean.oned;
+
+import java.io.Serializable;
+
+import org.apache.commons.geometry.core.Space;
+
+/**
+ * This class implements a one-dimensional space.
+ */
+public class Euclidean1D implements Serializable, Space {
+
+    /** Serializable version identifier. */
+    private static final long serialVersionUID = -1178039568877797126L;
+
+    /** Private constructor for the singleton.
+     */
+    private Euclidean1D() {
+    }
+
+    /** Get the unique instance.
+     * @return the unique instance
+     */
+    public static Euclidean1D getInstance() {
+        return LazyHolder.INSTANCE;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getDimension() {
+        return 1;
+    }
+
+    /** {@inheritDoc}
+     * <p>
+     * As the 1-dimension Euclidean space does not have proper sub-spaces,
+     * this method always throws a {@link UnsupportedOperationException}
+     * </p>
+     * @return nothing
+     * @throws UnsupportedOperationException in all cases
+     */
+    @Override
+    public Space getSubSpace() throws UnsupportedOperationException {
+        throw new UnsupportedOperationException("One-dimensional space does not have a subspace");
+    }
+
+    // CHECKSTYLE: stop HideUtilityClassConstructor
+    /** Holder for the instance.
+     * <p>We use here the Initialization On Demand Holder Idiom.</p>
+     */
+    private static class LazyHolder {
+        /** Cached field instance. */
+        private static final Euclidean1D INSTANCE = new Euclidean1D();
+    }
+    // CHECKSTYLE: resume HideUtilityClassConstructor
+
+    /** Handle deserialization of the singleton.
+     * @return the singleton instance
+     */
+    private Object readResolve() {
+        // return the singleton instance
+        return LazyHolder.INSTANCE;
+    }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Interval.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Interval.java
new file mode 100644
index 0000000..c4c39af
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Interval.java
@@ -0,0 +1,89 @@
+/*
+ * 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.geometry.euclidean.oned;
+
+import org.apache.commons.geometry.core.partitioning.Region.Location;
+
+/** This class represents a 1D interval.
+ * @see IntervalsSet
+ */
+public class Interval {
+
+    /** The lower bound of the interval. */
+    private final double lower;
+
+    /** The upper bound of the interval. */
+    private final double upper;
+
+    /** Simple constructor.
+     * @param lower lower bound of the interval
+     * @param upper upper bound of the interval
+     */
+    public Interval(final double lower, final double upper) {
+        if (upper < lower) {
+            throw new IllegalArgumentException("Endpoints do not specify an interval: [{" + upper + "}, {" + lower + "}]");
+        }
+        this.lower = lower;
+        this.upper = upper;
+    }
+
+    /** Get the lower bound of the interval.
+     * @return lower bound of the interval
+     */
+    public double getInf() {
+        return lower;
+    }
+
+    /** Get the upper bound of the interval.
+     * @return upper bound of the interval
+     */
+    public double getSup() {
+        return upper;
+    }
+
+    /** Get the size of the interval.
+     * @return size of the interval
+     */
+    public double getSize() {
+        return upper - lower;
+    }
+
+    /** Get the barycenter of the interval.
+     * @return barycenter of the interval
+     */
+    public double getBarycenter() {
+        return 0.5 * (lower + upper);
+    }
+
+    /** Check a point with respect to the interval.
+     * @param point point to check
+     * @param tolerance tolerance below which points are considered to
+     * belong to the boundary
+     * @return a code representing the point status: either {@link
+     * Location#INSIDE}, {@link Location#OUTSIDE} or {@link Location#BOUNDARY}
+     */
+    public Location checkPoint(final double point, final double tolerance) {
+        if (point < lower - tolerance || point > upper + tolerance) {
+            return Location.OUTSIDE;
+        } else if (point > lower + tolerance && point < upper - tolerance) {
+            return Location.INSIDE;
+        } else {
+            return Location.BOUNDARY;
+        }
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/IntervalsSet.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/IntervalsSet.java
new file mode 100644
index 0000000..3bdcd17
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/IntervalsSet.java
@@ -0,0 +1,619 @@
+/*
+ * 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.geometry.euclidean.oned;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.partitioning.AbstractRegion;
+import org.apache.commons.geometry.core.partitioning.BSPTree;
+import org.apache.commons.geometry.core.partitioning.BoundaryProjection;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+
+/** This class represents a 1D region: a set of intervals.
+ */
+public class IntervalsSet extends AbstractRegion<Euclidean1D, Euclidean1D> implements Iterable<double[]> {
+
+    /** Build an intervals set representing the whole real line.
+     * @param tolerance tolerance below which points are considered identical.
+     */
+    public IntervalsSet(final double tolerance) {
+        super(tolerance);
+    }
+
+    /** Build an intervals set corresponding to a single interval.
+     * @param lower lower bound of the interval, must be lesser or equal
+     * to {@code upper} (may be {@code Double.NEGATIVE_INFINITY})
+     * @param upper upper bound of the interval, must be greater or equal
+     * to {@code lower} (may be {@code Double.POSITIVE_INFINITY})
+     * @param tolerance tolerance below which points are considered identical.
+     */
+    public IntervalsSet(final double lower, final double upper, final double tolerance) {
+        super(buildTree(lower, upper, tolerance), tolerance);
+    }
+
+    /** Build an intervals set from an inside/outside BSP tree.
+     * <p>The leaf nodes of the BSP tree <em>must</em> have a
+     * {@code Boolean} attribute representing the inside status of
+     * the corresponding cell (true for inside cells, false for outside
+     * cells). In order to avoid building too many small objects, it is
+     * recommended to use the predefined constants
+     * {@code Boolean.TRUE} and {@code Boolean.FALSE}</p>
+     * @param tree inside/outside BSP tree representing the intervals set
+     * @param tolerance tolerance below which points are considered identical.
+     */
+    public IntervalsSet(final BSPTree<Euclidean1D> tree, final double tolerance) {
+        super(tree, tolerance);
+    }
+
+    /** Build an intervals set from a Boundary REPresentation (B-rep).
+     * <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
+     * its plus side.</p>
+     * <p>The boundary elements can be in any order, and can form
+     * several non-connected sets (like for example polygons with holes
+     * or a set of disjoints polyhedrons considered as a whole). In
+     * fact, the elements do not even need to be connected together
+     * (their topological connections are not used here). However, if the
+     * boundary does not really separate an inside open from an outside
+     * open (open having here its topological meaning), then subsequent
+     * calls to the {@link
+     * org.apache.commons.geometry.core.partitioning.Region#checkPoint(org.apache.commons.geometry.core.Point)
+     * checkPoint} method will not be meaningful anymore.</p>
+     * <p>If the boundary is empty, the region will represent the whole
+     * space.</p>
+     * @param boundary collection of boundary elements
+     * @param tolerance tolerance below which points are considered identical.
+     */
+    public IntervalsSet(final Collection<SubHyperplane<Euclidean1D>> boundary,
+                        final double tolerance) {
+        super(boundary, tolerance);
+    }
+
+    /** Build an inside/outside tree representing a single interval.
+     * @param lower lower bound of the interval, must be lesser or equal
+     * to {@code upper} (may be {@code Double.NEGATIVE_INFINITY})
+     * @param upper upper bound of the interval, must be greater or equal
+     * to {@code lower} (may be {@code Double.POSITIVE_INFINITY})
+     * @param tolerance tolerance below which points are considered identical.
+     * @return the built tree
+     */
+    private static BSPTree<Euclidean1D> buildTree(final double lower, final double upper,
+                                                  final double tolerance) {
+        if (Double.isInfinite(lower) && (lower < 0)) {
+            if (Double.isInfinite(upper) && (upper > 0)) {
+                // the tree must cover the whole real line
+                return new BSPTree<>(Boolean.TRUE);
+            }
+            // the tree must be open on the negative infinity side
+            final SubHyperplane<Euclidean1D> upperCut =
+                new OrientedPoint(new Cartesian1D(upper), true, tolerance).wholeHyperplane();
+            return new BSPTree<>(upperCut,
+                               new BSPTree<Euclidean1D>(Boolean.FALSE),
+                               new BSPTree<Euclidean1D>(Boolean.TRUE),
+                               null);
+        }
+        final SubHyperplane<Euclidean1D> lowerCut =
+            new OrientedPoint(new Cartesian1D(lower), false, tolerance).wholeHyperplane();
+        if (Double.isInfinite(upper) && (upper > 0)) {
+            // the tree must be open on the positive infinity side
+            return new BSPTree<>(lowerCut,
+                                            new BSPTree<Euclidean1D>(Boolean.FALSE),
+                                            new BSPTree<Euclidean1D>(Boolean.TRUE),
+                                            null);
+        }
+
+        // the tree must be bounded on the two sides
+        final SubHyperplane<Euclidean1D> upperCut =
+            new OrientedPoint(new Cartesian1D(upper), true, tolerance).wholeHyperplane();
+        return new BSPTree<>(lowerCut,
+                                        new BSPTree<Euclidean1D>(Boolean.FALSE),
+                                        new BSPTree<>(upperCut,
+                                                                 new BSPTree<Euclidean1D>(Boolean.FALSE),
+                                                                 new BSPTree<Euclidean1D>(Boolean.TRUE),
+                                                                 null),
+                                        null);
+
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public IntervalsSet buildNew(final BSPTree<Euclidean1D> tree) {
+        return new IntervalsSet(tree, getTolerance());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void computeGeometricalProperties() {
+        if (getTree(false).getCut() == null) {
+            setBarycenter((Point<Euclidean1D>) Cartesian1D.NaN);
+            setSize(((Boolean) getTree(false).getAttribute()) ? Double.POSITIVE_INFINITY : 0);
+        } else {
+            double size = 0.0;
+            double sum = 0.0;
+            for (final Interval interval : asList()) {
+                size += interval.getSize();
+                sum  += interval.getSize() * interval.getBarycenter();
+            }
+            setSize(size);
+            if (Double.isInfinite(size)) {
+                setBarycenter((Point<Euclidean1D>) Cartesian1D.NaN);
+            } else if (size > 0) {
+                setBarycenter((Point<Euclidean1D>) new Cartesian1D(sum / size));
+            } else {
+                setBarycenter((Point<Euclidean1D>) ((OrientedPoint) getTree(false).getCut().getHyperplane()).getLocation());
+            }
+        }
+    }
+
+    /** Get the lowest value belonging to the instance.
+     * @return lowest value belonging to the instance
+     * ({@code Double.NEGATIVE_INFINITY} if the instance doesn't
+     * have any low bound, {@code Double.POSITIVE_INFINITY} if the
+     * instance is empty)
+     */
+    public double getInf() {
+        BSPTree<Euclidean1D> node = getTree(false);
+        double  inf  = Double.POSITIVE_INFINITY;
+        while (node.getCut() != null) {
+            final OrientedPoint op = (OrientedPoint) node.getCut().getHyperplane();
+            inf  = op.getLocation().getX();
+            node = op.isDirect() ? node.getMinus() : node.getPlus();
+        }
+        return ((Boolean) node.getAttribute()) ? Double.NEGATIVE_INFINITY : inf;
+    }
+
+    /** Get the highest value belonging to the instance.
+     * @return highest value belonging to the instance
+     * ({@code Double.POSITIVE_INFINITY} if the instance doesn't
+     * have any high bound, {@code Double.NEGATIVE_INFINITY} if the
+     * instance is empty)
+     */
+    public double getSup() {
+        BSPTree<Euclidean1D> node = getTree(false);
+        double  sup  = Double.NEGATIVE_INFINITY;
+        while (node.getCut() != null) {
+            final OrientedPoint op = (OrientedPoint) node.getCut().getHyperplane();
+            sup  = op.getLocation().getX();
+            node = op.isDirect() ? node.getPlus() : node.getMinus();
+        }
+        return ((Boolean) node.getAttribute()) ? Double.POSITIVE_INFINITY : sup;
+    }
+
+    /** {@inheritDoc}
+     */
+    @Override
+    public BoundaryProjection<Euclidean1D> projectToBoundary(final Point<Euclidean1D> point) {
+
+        // get position of test point
+        final double x = ((Cartesian1D) point).getX();
+
+        double previous = Double.NEGATIVE_INFINITY;
+        for (final double[] a : this) {
+            if (x < a[0]) {
+                // the test point lies between the previous and the current intervals
+                // offset will be positive
+                final double previousOffset = x - previous;
+                final double currentOffset  = a[0] - x;
+                if (previousOffset < currentOffset) {
+                    return new BoundaryProjection<>(point, finiteOrNullPoint(previous), previousOffset);
+                } else {
+                    return new BoundaryProjection<>(point, finiteOrNullPoint(a[0]), currentOffset);
+                }
+            } else if (x <= a[1]) {
+                // the test point lies within the current interval
+                // offset will be negative
+                final double offset0 = a[0] - x;
+                final double offset1 = x - a[1];
+                if (offset0 < offset1) {
+                    return new BoundaryProjection<>(point, finiteOrNullPoint(a[1]), offset1);
+                } else {
+                    return new BoundaryProjection<>(point, finiteOrNullPoint(a[0]), offset0);
+                }
+            }
+            previous = a[1];
+        }
+
+        // the test point if past the last sub-interval
+        return new BoundaryProjection<>(point, finiteOrNullPoint(previous), x - previous);
+
+    }
+
+    /** Build a finite point.
+     * @param x abscissa of the point
+     * @return a new point for finite abscissa, null otherwise
+     */
+    private Cartesian1D finiteOrNullPoint(final double x) {
+        return Double.isInfinite(x) ? null : new Cartesian1D(x);
+    }
+
+    /** Build an ordered list of intervals representing the instance.
+     * <p>This method builds this intervals set as an ordered list of
+     * {@link Interval Interval} elements. If the intervals set has no
+     * lower limit, the first interval will have its low bound equal to
+     * {@code Double.NEGATIVE_INFINITY}. If the intervals set has
+     * no upper limit, the last interval will have its upper bound equal
+     * to {@code Double.POSITIVE_INFINITY}. An empty tree will
+     * build an empty list while a tree representing the whole real line
+     * will build a one element list with both bounds being
+     * infinite.</p>
+     * @return a new ordered list containing {@link Interval Interval}
+     * elements
+     */
+    public List<Interval> asList() {
+        final List<Interval> list = new ArrayList<>();
+        for (final double[] a : this) {
+            list.add(new Interval(a[0], a[1]));
+        }
+        return list;
+    }
+
+    /** Get the first leaf node of a tree.
+     * @param root tree root
+     * @return first leaf node
+     */
+    private BSPTree<Euclidean1D> getFirstLeaf(final BSPTree<Euclidean1D> root) {
+
+        if (root.getCut() == null) {
+            return root;
+        }
+
+        // find the smallest internal node
+        BSPTree<Euclidean1D> smallest = null;
+        for (BSPTree<Euclidean1D> n = root; n != null; n = previousInternalNode(n)) {
+            smallest = n;
+        }
+
+        return leafBefore(smallest);
+
+    }
+
+    /** Get the node corresponding to the first interval boundary.
+     * @return smallest internal node,
+     * or null if there are no internal nodes (i.e. the set is either empty or covers the real line)
+     */
+    private BSPTree<Euclidean1D> getFirstIntervalBoundary() {
+
+        // start search at the tree root
+        BSPTree<Euclidean1D> node = getTree(false);
+        if (node.getCut() == null) {
+            return null;
+        }
+
+        // walk tree until we find the smallest internal node
+        node = getFirstLeaf(node).getParent();
+
+        // walk tree until we find an interval boundary
+        while (node != null && !(isIntervalStart(node) || isIntervalEnd(node))) {
+            node = nextInternalNode(node);
+        }
+
+        return node;
+
+    }
+
+    /** Check if an internal node corresponds to the start abscissa of an interval.
+     * @param node internal node to check
+     * @return true if the node corresponds to the start abscissa of an interval
+     */
+    private boolean isIntervalStart(final BSPTree<Euclidean1D> node) {
+
+        if ((Boolean) leafBefore(node).getAttribute()) {
+            // it has an inside cell before it, it may end an interval but not start it
+            return false;
+        }
+
+        if (!(Boolean) leafAfter(node).getAttribute()) {
+            // it has an outside cell after it, it is a dummy cut away from real intervals
+            return false;
+        }
+
+        // the cell has an outside before and an inside after it
+        // it is the start of an interval
+        return true;
+
+    }
+
+    /** Check if an internal node corresponds to the end abscissa of an interval.
+     * @param node internal node to check
+     * @return true if the node corresponds to the end abscissa of an interval
+     */
+    private boolean isIntervalEnd(final BSPTree<Euclidean1D> node) {
+
+        if (!(Boolean) leafBefore(node).getAttribute()) {
+            // it has an outside cell before it, it may start an interval but not end it
+            return false;
+        }
+
+        if ((Boolean) leafAfter(node).getAttribute()) {
+            // it has an inside cell after it, it is a dummy cut in the middle of an interval
+            return false;
+        }
+
+        // the cell has an inside before and an outside after it
+        // it is the end of an interval
+        return true;
+
+    }
+
+    /** Get the next internal node.
+     * @param node current internal node
+     * @return next internal node in ascending order, or null
+     * if this is the last internal node
+     */
+    private BSPTree<Euclidean1D> nextInternalNode(BSPTree<Euclidean1D> node) {
+
+        if (childAfter(node).getCut() != null) {
+            // the next node is in the sub-tree
+            return leafAfter(node).getParent();
+        }
+
+        // there is nothing left deeper in the tree, we backtrack
+        while (isAfterParent(node)) {
+            node = node.getParent();
+        }
+        return node.getParent();
+
+    }
+
+    /** Get the previous internal node.
+     * @param node current internal node
+     * @return previous internal node in ascending order, or null
+     * if this is the first internal node
+     */
+    private BSPTree<Euclidean1D> previousInternalNode(BSPTree<Euclidean1D> node) {
+
+        if (childBefore(node).getCut() != null) {
+            // the next node is in the sub-tree
+            return leafBefore(node).getParent();
+        }
+
+        // there is nothing left deeper in the tree, we backtrack
+        while (isBeforeParent(node)) {
+            node = node.getParent();
+        }
+        return node.getParent();
+
+    }
+
+    /** Find the leaf node just before an internal node.
+     * @param node internal node at which the sub-tree starts
+     * @return leaf node just before the internal node
+     */
+    private BSPTree<Euclidean1D> leafBefore(BSPTree<Euclidean1D> node) {
+
+        node = childBefore(node);
+        while (node.getCut() != null) {
+            node = childAfter(node);
+        }
+
+        return node;
+
+    }
+
+    /** Find the leaf node just after an internal node.
+     * @param node internal node at which the sub-tree starts
+     * @return leaf node just after the internal node
+     */
+    private BSPTree<Euclidean1D> leafAfter(BSPTree<Euclidean1D> node) {
+
+        node = childAfter(node);
+        while (node.getCut() != null) {
+            node = childBefore(node);
+        }
+
+        return node;
+
+    }
+
+    /** Check if a node is the child before its parent in ascending order.
+     * @param node child node considered
+     * @return true is the node has a parent end is before it in ascending order
+     */
+    private boolean isBeforeParent(final BSPTree<Euclidean1D> node) {
+        final BSPTree<Euclidean1D> parent = node.getParent();
+        if (parent == null) {
+            return false;
+        } else {
+            return node == childBefore(parent);
+        }
+    }
+
+    /** Check if a node is the child after its parent in ascending order.
+     * @param node child node considered
+     * @return true is the node has a parent end is after it in ascending order
+     */
+    private boolean isAfterParent(final BSPTree<Euclidean1D> node) {
+        final BSPTree<Euclidean1D> parent = node.getParent();
+        if (parent == null) {
+            return false;
+        } else {
+            return node == childAfter(parent);
+        }
+    }
+
+    /** Find the child node just before an internal node.
+     * @param node internal node at which the sub-tree starts
+     * @return child node just before the internal node
+     */
+    private BSPTree<Euclidean1D> childBefore(BSPTree<Euclidean1D> node) {
+        if (isDirect(node)) {
+            // smaller abscissas are on minus side, larger abscissas are on plus side
+            return node.getMinus();
+        } else {
+            // smaller abscissas are on plus side, larger abscissas are on minus side
+            return node.getPlus();
+        }
+    }
+
+    /** Find the child node just after an internal node.
+     * @param node internal node at which the sub-tree starts
+     * @return child node just after the internal node
+     */
+    private BSPTree<Euclidean1D> childAfter(BSPTree<Euclidean1D> node) {
+        if (isDirect(node)) {
+            // smaller abscissas are on minus side, larger abscissas are on plus side
+            return node.getPlus();
+        } else {
+            // smaller abscissas are on plus side, larger abscissas are on minus side
+            return node.getMinus();
+        }
+    }
+
+    /** Check if an internal node has a direct oriented point.
+     * @param node internal node to check
+     * @return true if the oriented point is direct
+     */
+    private boolean isDirect(final BSPTree<Euclidean1D> node) {
+        return ((OrientedPoint) node.getCut().getHyperplane()).isDirect();
+    }
+
+    /** Get the abscissa of an internal node.
+     * @param node internal node to check
+     * @return abscissa
+     */
+    private double getAngle(final BSPTree<Euclidean1D> node) {
+        return ((OrientedPoint) node.getCut().getHyperplane()).getLocation().getX();
+    }
+
+    /** {@inheritDoc}
+     * <p>
+     * The iterator returns the limit values of sub-intervals in ascending order.
+     * </p>
+     * <p>
+     * The iterator does <em>not</em> support the optional {@code remove} operation.
+     * </p>
+     */
+    @Override
+    public Iterator<double[]> iterator() {
+        return new SubIntervalsIterator();
+    }
+
+    /** Local iterator for sub-intervals. */
+    private class SubIntervalsIterator implements Iterator<double[]> {
+
+        /** Current node. */
+        private BSPTree<Euclidean1D> current;
+
+        /** Sub-interval no yet returned. */
+        private double[] pending;
+
+        /** Simple constructor.
+         */
+        SubIntervalsIterator() {
+
+            current = getFirstIntervalBoundary();
+
+            if (current == null) {
+                // all the leaf tree nodes share the same inside/outside status
+                if ((Boolean) getFirstLeaf(getTree(false)).getAttribute()) {
+                    // it is an inside node, it represents the full real line
+                    pending = new double[] {
+                        Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY
+                    };
+                } else {
+                    pending = null;
+                }
+            } else if (isIntervalEnd(current)) {
+                // the first boundary is an interval end,
+                // so the first interval starts at infinity
+                pending = new double[] {
+                    Double.NEGATIVE_INFINITY, getAngle(current)
+                };
+            } else {
+                selectPending();
+            }
+        }
+
+        /** Walk the tree to select the pending sub-interval.
+         */
+        private void selectPending() {
+
+            // look for the start of the interval
+            BSPTree<Euclidean1D> start = current;
+            while (start != null && !isIntervalStart(start)) {
+                start = nextInternalNode(start);
+            }
+
+            if (start == null) {
+                // we have exhausted the iterator
+                current = null;
+                pending = null;
+                return;
+            }
+
+            // look for the end of the interval
+            BSPTree<Euclidean1D> end = start;
+            while (end != null && !isIntervalEnd(end)) {
+                end = nextInternalNode(end);
+            }
+
+            if (end != null) {
+
+                // we have identified the interval
+                pending = new double[] {
+                    getAngle(start), getAngle(end)
+                };
+
+                // prepare search for next interval
+                current = end;
+
+            } else {
+
+                // the final interval is open toward infinity
+                pending = new double[] {
+                    getAngle(start), Double.POSITIVE_INFINITY
+                };
+
+                // there won't be any other intervals
+                current = null;
+
+            }
+
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public boolean hasNext() {
+            return pending != null;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public double[] next() {
+            if (pending == null) {
+                throw new NoSuchElementException();
+            }
+            final double[] next = pending;
+            selectPending();
+            return next;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void remove() {
+            throw new UnsupportedOperationException();
+        }
+
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java
new file mode 100644
index 0000000..b1d0d89
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/OrientedPoint.java
@@ -0,0 +1,140 @@
+/*
+ * 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.geometry.euclidean.oned;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Vector;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+
+/** This class represents a 1D oriented hyperplane.
+ * <p>An hyperplane in 1D is a simple point, its orientation being a
+ * boolean.</p>
+ * <p>Instances of this class are guaranteed to be immutable.</p>
+ */
+public class OrientedPoint implements Hyperplane<Euclidean1D> {
+
+    /** Vector location. */
+    private final Cartesian1D location;
+
+    /** Orientation. */
+    private boolean direct;
+
+    /** Tolerance below which points are considered to belong to the hyperplane. */
+    private final double tolerance;
+
+    /** Simple constructor.
+     * @param location location of the hyperplane
+     * @param direct if true, the plus side of the hyperplane is towards
+     * abscissas greater than {@code location}
+     * @param tolerance tolerance below which points are considered to belong to the hyperplane
+     */
+    public OrientedPoint(final Cartesian1D location, final boolean direct, final double tolerance) {
+        this.location  = location;
+        this.direct    = direct;
+        this.tolerance = tolerance;
+    }
+
+    /** Copy the instance.
+     * <p>Since instances are immutable, this method directly returns
+     * the instance.</p>
+     * @return the instance itself
+     */
+    @Override
+    public OrientedPoint copySelf() {
+        return this;
+    }
+
+    /** Get the offset (oriented distance) of a vector.
+     * @param vector vector to check
+     * @return offset of the vector
+     */
+    public double getOffset(Vector<Euclidean1D> vector) {
+        return getOffset((Point<Euclidean1D>) vector);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getOffset(final Point<Euclidean1D> point) {
+        final double delta = ((Cartesian1D) point).getX() - location.getX();
+        return direct ? delta : -delta;
+    }
+
+    /** Build a region covering the whole hyperplane.
+     * <p>Since this class represent zero dimension spaces which does
+     * not have lower dimension sub-spaces, this method returns a dummy
+     * implementation of a {@link
+     * org.apache.commons.geometry.core.partitioning.SubHyperplane SubHyperplane}.
+     * This implementation is only used to allow the {@link
+     * org.apache.commons.geometry.core.partitioning.SubHyperplane
+     * SubHyperplane} class implementation to work properly, it should
+     * <em>not</em> be used otherwise.</p>
+     * @return a dummy sub hyperplane
+     */
+    @Override
+    public SubOrientedPoint wholeHyperplane() {
+        return new SubOrientedPoint(this, null);
+    }
+
+    /** Build a region covering the whole space.
+     * @return a region containing the instance (really an {@link
+     * IntervalsSet IntervalsSet} instance)
+     */
+    @Override
+    public IntervalsSet wholeSpace() {
+        return new IntervalsSet(tolerance);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean sameOrientationAs(final Hyperplane<Euclidean1D> other) {
+        return !(direct ^ ((OrientedPoint) other).direct);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Point<Euclidean1D> project(Point<Euclidean1D> point) {
+        return location;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getTolerance() {
+        return tolerance;
+    }
+
+    /** Get the hyperplane location on the real line.
+     * @return the hyperplane location
+     */
+    public Cartesian1D getLocation() {
+        return location;
+    }
+
+    /** Check if the hyperplane orientation is direct.
+     * @return true if the plus side of the hyperplane is towards
+     * abscissae greater than hyperplane location
+     */
+    public boolean isDirect() {
+        return direct;
+    }
+
+    /** Revert the instance.
+     */
+    public void revertSelf() {
+        direct = !direct;
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/SubOrientedPoint.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/SubOrientedPoint.java
new file mode 100644
index 0000000..8e05e5a
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/SubOrientedPoint.java
@@ -0,0 +1,76 @@
+/*
+ * 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.geometry.euclidean.oned;
+
+import org.apache.commons.geometry.core.partitioning.AbstractSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Region;
+
+/** This class represents sub-hyperplane for {@link OrientedPoint}.
+ * <p>An hyperplane in 1D is a simple point, its orientation being a
+ * boolean.</p>
+ * <p>Instances of this class are guaranteed to be immutable.</p>
+ */
+public class SubOrientedPoint extends AbstractSubHyperplane<Euclidean1D, Euclidean1D> {
+
+    /** Simple constructor.
+     * @param hyperplane underlying hyperplane
+     * @param remainingRegion remaining region of the hyperplane
+     */
+    public SubOrientedPoint(final Hyperplane<Euclidean1D> hyperplane,
+                            final Region<Euclidean1D> remainingRegion) {
+        super(hyperplane, remainingRegion);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getSize() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isEmpty() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected AbstractSubHyperplane<Euclidean1D, Euclidean1D> buildNew(final Hyperplane<Euclidean1D> hyperplane,
+                                                                       final Region<Euclidean1D> remainingRegion) {
+        return new SubOrientedPoint(hyperplane, remainingRegion);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public SplitSubHyperplane<Euclidean1D> split(final Hyperplane<Euclidean1D> hyperplane) {
+        final OrientedPoint thisHyperplane = (OrientedPoint) getHyperplane();
+        final double global = hyperplane.getOffset(thisHyperplane.getLocation());
+
+        // use the tolerance value from our parent hyperplane to determine equality
+        final double tolerance = thisHyperplane.getTolerance();
+
+        if (global < -tolerance) {
+            return new SplitSubHyperplane<Euclidean1D>(null, this);
+        } else if (global > tolerance) {
+            return new SplitSubHyperplane<Euclidean1D>(this, null);
+        } else {
+            return new SplitSubHyperplane<Euclidean1D>(null, null);
+        }
+    }
+
+}
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
new file mode 100644
index 0000000..e9fdaf1
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/Vector1D.java
@@ -0,0 +1,31 @@
+/*
+ * 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.geometry.euclidean.oned;
+
+import org.apache.commons.geometry.core.Vector;
+
+/** This class represents a 1D vector.
+ */
+public abstract class Vector1D implements Vector<Euclidean1D> {
+
+    /** Get the abscissa of the vector.
+     * @return abscissa of the vector
+     * @see Cartesian1D#Cartesian1D(double)
+     */
+    public abstract double getX();
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/package-info.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/package-info.java
new file mode 100644
index 0000000..f987815
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/oned/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+/**
+ *
+ * <p>
+ * This package provides basic 1D geometry components.
+ * </p>
+ *
+ */
+package org.apache.commons.geometry.euclidean.oned;
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Cartesian3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Cartesian3D.java
new file mode 100644
index 0000000..79b975f
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Cartesian3D.java
@@ -0,0 +1,621 @@
+/*
+ * 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.geometry.euclidean.threed;
+
+import java.io.Serializable;
+import java.text.NumberFormat;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+import org.apache.commons.geometry.core.Vector;
+import org.apache.commons.numbers.arrays.LinearCombination;
+
+/**
+ * This class represents points or vectors in a three-dimensional space.
+ * <p>An instance of Cartesian3D represents the point with the corresponding
+ * coordinates.</p>
+ * <p>An instance of Cartesian3D also represents the vector which begins at
+ * the origin and ends at the point corresponding to the coordinates.</p>
+ * <p>Instance of this class are guaranteed to be immutable.</p>
+ */
+public class Cartesian3D extends Vector3D implements Serializable, Point<Euclidean3D> {
+
+    /** Null vector (coordinates: 0, 0, 0). */
+    public static final Cartesian3D ZERO   = new Cartesian3D(0, 0, 0);
+
+    /** First canonical vector (coordinates: 1, 0, 0). */
+    public static final Cartesian3D PLUS_I = new Cartesian3D(1, 0, 0);
+
+    /** Opposite of the first canonical vector (coordinates: -1, 0, 0). */
+    public static final Cartesian3D MINUS_I = new Cartesian3D(-1, 0, 0);
+
+    /** Second canonical vector (coordinates: 0, 1, 0). */
+    public static final Cartesian3D PLUS_J = new Cartesian3D(0, 1, 0);
+
+    /** Opposite of the second canonical vector (coordinates: 0, -1, 0). */
+    public static final Cartesian3D MINUS_J = new Cartesian3D(0, -1, 0);
+
+    /** Third canonical vector (coordinates: 0, 0, 1). */
+    public static final Cartesian3D PLUS_K = new Cartesian3D(0, 0, 1);
+
+    /** Opposite of the third canonical vector (coordinates: 0, 0, -1).  */
+    public static final Cartesian3D MINUS_K = new Cartesian3D(0, 0, -1);
+
+    // CHECKSTYLE: stop ConstantName
+    /** A vector with all coordinates set to NaN. */
+    public static final Cartesian3D NaN = new Cartesian3D(Double.NaN, Double.NaN, Double.NaN);
+    // CHECKSTYLE: resume ConstantName
+
+    /** A vector with all coordinates set to positive infinity. */
+    public static final Cartesian3D POSITIVE_INFINITY =
+        new Cartesian3D(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
+
+    /** A vector with all coordinates set to negative infinity. */
+    public static final Cartesian3D NEGATIVE_INFINITY =
+        new Cartesian3D(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY);
+
+    /** Serializable version identifier. */
+    private static final long serialVersionUID = 1313493323784566947L;
+
+    /** Error message when norms are zero. */
+    private static final String ZERO_NORM_MSG = "Norm is zero";
+
+    /** Abscissa. */
+    private final double x;
+
+    /** Ordinate. */
+    private final double y;
+
+    /** Height. */
+    private final double z;
+
+    /** Simple constructor.
+     * Build a vector from its coordinates
+     * @param x abscissa
+     * @param y ordinate
+     * @param z height
+     * @see #getX()
+     * @see #getY()
+     * @see #getZ()
+     */
+    public Cartesian3D(double x, double y, double z) {
+        this.x = x;
+        this.y = y;
+        this.z = z;
+    }
+
+    /** Simple constructor.
+     * Build a vector from its coordinates
+     * @param v coordinates array
+     * @exception DimensionMismatchException if array does not have 3 elements
+     * @see #toArray()
+     */
+    public Cartesian3D(double[] v) throws IllegalArgumentException {
+        if (v.length != 3) {
+            throw new IllegalArgumentException("Dimension mismatch: " + v.length + " != 3");
+        }
+        this.x = v[0];
+        this.y = v[1];
+        this.z = v[2];
+    }
+
+    /** Simple constructor.
+     * Build a vector from its azimuthal coordinates
+     * @param alpha azimuth (&alpha;) around Z
+     *              (0 is +X, &pi;/2 is +Y, &pi; is -X and 3&pi;/2 is -Y)
+     * @param delta elevation (&delta;) above (XY) plane, from -&pi;/2 to +&pi;/2
+     * @see #getAlpha()
+     * @see #getDelta()
+     */
+    public Cartesian3D(double alpha, double delta) {
+        double cosDelta = Math.cos(delta);
+        this.x = Math.cos(alpha) * cosDelta;
+        this.y = Math.sin(alpha) * cosDelta;
+        this.z = Math.sin(delta);
+    }
+
+    /** Multiplicative constructor
+     * Build a vector from another one and a scale factor.
+     * The vector built will be a * u
+     * @param a scale factor
+     * @param u base (unscaled) vector
+     */
+    public Cartesian3D(double a, Cartesian3D u) {
+        this.x = a * u.x;
+        this.y = a * u.y;
+        this.z = a * u.z;
+    }
+
+    /** Linear constructor
+     * Build a vector from two other ones and corresponding scale factors.
+     * The vector built will be a1 * u1 + a2 * u2
+     * @param a1 first scale factor
+     * @param u1 first base (unscaled) vector
+     * @param a2 second scale factor
+     * @param u2 second base (unscaled) vector
+     */
+    public Cartesian3D(double a1, Cartesian3D u1, double a2, Cartesian3D u2) {
+        this.x = LinearCombination.value(a1, u1.x, a2, u2.x);
+        this.y = LinearCombination.value(a1, u1.y, a2, u2.y);
+        this.z = LinearCombination.value(a1, u1.z, a2, u2.z);
+    }
+
+    /** Linear constructor
+     * Build a vector from three other ones and corresponding scale factors.
+     * The vector built will be a1 * u1 + a2 * u2 + a3 * u3
+     * @param a1 first scale factor
+     * @param u1 first base (unscaled) vector
+     * @param a2 second scale factor
+     * @param u2 second base (unscaled) vector
+     * @param a3 third scale factor
+     * @param u3 third base (unscaled) vector
+     */
+    public Cartesian3D(double a1, Cartesian3D u1, double a2, Cartesian3D u2,
+                    double a3, Cartesian3D u3) {
+        this.x = LinearCombination.value(a1, u1.x, a2, u2.x, a3, u3.x);
+        this.y = LinearCombination.value(a1, u1.y, a2, u2.y, a3, u3.y);
+        this.z = LinearCombination.value(a1, u1.z, a2, u2.z, a3, u3.z);
+    }
+
+    /** Linear constructor
+     * Build a vector from four other ones and corresponding scale factors.
+     * The vector built will be a1 * u1 + a2 * u2 + a3 * u3 + a4 * u4
+     * @param a1 first scale factor
+     * @param u1 first base (unscaled) vector
+     * @param a2 second scale factor
+     * @param u2 second base (unscaled) vector
+     * @param a3 third scale factor
+     * @param u3 third base (unscaled) vector
+     * @param a4 fourth scale factor
+     * @param u4 fourth base (unscaled) vector
+     */
+    public Cartesian3D(double a1, Cartesian3D u1, double a2, Cartesian3D u2,
+                    double a3, Cartesian3D u3, double a4, Cartesian3D u4) {
+        this.x = LinearCombination.value(a1, u1.x, a2, u2.x, a3, u3.x, a4, u4.x);
+        this.y = LinearCombination.value(a1, u1.y, a2, u2.y, a3, u3.y, a4, u4.y);
+        this.z = LinearCombination.value(a1, u1.z, a2, u2.z, a3, u3.z, a4, u4.z);
+    }
+
+    /** Get the abscissa of the vector.
+     * @return abscissa of the vector
+     * @see #Cartesian3D(double, double, double)
+     */
+    @Override
+    public double getX() {
+        return x;
+    }
+
+    /** Get the ordinate of the vector.
+     * @return ordinate of the vector
+     * @see #Cartesian3D(double, double, double)
+     */
+    @Override
+    public double getY() {
+        return y;
+    }
+
+    /** Get the height of the vector.
+     * @return height of the vector
+     * @see #Cartesian3D(double, double, double)
+     */
+    @Override
+    public double getZ() {
+        return z;
+    }
+
+    /** Get the vector coordinates as a dimension 3 array.
+     * @return vector coordinates
+     * @see #Cartesian3D(double[])
+     */
+    public double[] toArray() {
+        return new double[] { x, y, z };
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Space getSpace() {
+        return Euclidean3D.getInstance();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian3D getZero() {
+        return ZERO;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getNorm1() {
+        return Math.abs(x) + Math.abs(y) + Math.abs(z);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getNorm() {
+        // there are no cancellation problems here, so we use the straightforward formula
+        return Math.sqrt (x * x + y * y + z * z);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getNormSq() {
+        // there are no cancellation problems here, so we use the straightforward formula
+        return x * x + y * y + z * z;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getNormInf() {
+        return Math.max(Math.max(Math.abs(x), Math.abs(y)), Math.abs(z));
+    }
+
+    /** Get the azimuth of the vector.
+     * @return azimuth (&alpha;) of the vector, between -&pi; and +&pi;
+     * @see #Cartesian3D(double, double)
+     */
+    public double getAlpha() {
+        return Math.atan2(y, x);
+    }
+
+    /** Get the elevation of the vector.
+     * @return elevation (&delta;) of the vector, between -&pi;/2 and +&pi;/2
+     * @see #Cartesian3D(double, double)
+     */
+    public double getDelta() {
+        return Math.asin(z / getNorm());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian3D add(final Vector<Euclidean3D> v) {
+        final Cartesian3D v3 = (Cartesian3D) v;
+        return new Cartesian3D(x + v3.x, y + v3.y, z + v3.z);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian3D add(double factor, final Vector<Euclidean3D> v) {
+        return new Cartesian3D(1, this, factor, (Cartesian3D) v);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian3D subtract(final Vector<Euclidean3D> v) {
+        final Cartesian3D v3 = (Cartesian3D) v;
+        return new Cartesian3D(x - v3.x, y - v3.y, z - v3.z);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian3D subtract(final double factor, final Vector<Euclidean3D> v) {
+        return new Cartesian3D(1, this, -factor, (Cartesian3D) v);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian3D normalize() throws IllegalStateException {
+        double s = getNorm();
+        if (s == 0) {
+            throw new IllegalStateException(ZERO_NORM_MSG);
+        }
+        return scalarMultiply(1 / s);
+    }
+
+    /** Get a vector orthogonal to the instance.
+     * <p>There are an infinite number of normalized vectors orthogonal
+     * to the instance. This method picks up one of them almost
+     * arbitrarily. It is useful when one needs to compute a reference
+     * frame with one of the axes in a predefined direction. The
+     * following example shows how to build a frame having the k axis
+     * aligned with the known vector u :
+     * <pre><code>
+     *   Cartesian3D k = u.normalize();
+     *   Cartesian3D i = k.orthogonal();
+     *   Cartesian3D j = Cartesian3D.crossProduct(k, i);
+     * </code></pre>
+     * @return a new normalized vector orthogonal to the instance
+     * @exception IllegalStateException if the norm of the instance is zero
+     */
+    public Cartesian3D orthogonal() throws IllegalStateException {
+
+        double threshold = 0.6 * getNorm();
+        if (threshold == 0) {
+            throw new IllegalStateException(ZERO_NORM_MSG);
+        }
+
+        if (Math.abs(x) <= threshold) {
+            double inverse  = 1 / Math.sqrt(y * y + z * z);
+            return new Cartesian3D(0, inverse * z, -inverse * y);
+        } else if (Math.abs(y) <= threshold) {
+            double inverse  = 1 / Math.sqrt(x * x + z * z);
+            return new Cartesian3D(-inverse * z, 0, inverse * x);
+        }
+        double inverse  = 1 / Math.sqrt(x * x + y * y);
+        return new Cartesian3D(inverse * y, -inverse * x, 0);
+
+    }
+
+    /** Compute the angular separation between two vectors.
+     * <p>This method computes the angular separation between two
+     * vectors using the dot product for well separated vectors and the
+     * cross product for almost aligned vectors. This allows to have a
+     * good accuracy in all cases, even for vectors very close to each
+     * other.</p>
+     * @param v1 first vector
+     * @param v2 second vector
+     * @return angular separation between v1 and v2
+     * @exception IllegalArgumentException if either vector has a zero norm
+     */
+    public static double angle(Cartesian3D v1, Cartesian3D v2) throws IllegalArgumentException {
+
+        double normProduct = v1.getNorm() * v2.getNorm();
+        if (normProduct == 0) {
+            throw new IllegalArgumentException(ZERO_NORM_MSG);
+        }
+
+        double dot = v1.dotProduct(v2);
+        double threshold = normProduct * 0.9999;
+        if ((dot < -threshold) || (dot > threshold)) {
+            // the vectors are almost aligned, compute using the sine
+            Cartesian3D v3 = crossProduct(v1, v2);
+            if (dot >= 0) {
+                return Math.asin(v3.getNorm() / normProduct);
+            }
+            return Math.PI - Math.asin(v3.getNorm() / normProduct);
+        }
+
+        // the vectors are sufficiently separated to use the cosine
+        return Math.acos(dot / normProduct);
+
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian3D negate() {
+        return new Cartesian3D(-x, -y, -z);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian3D scalarMultiply(double a) {
+        return new Cartesian3D(a * x, a * y, a * z);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isNaN() {
+        return Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(z);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isInfinite() {
+        return !isNaN() && (Double.isInfinite(x) || Double.isInfinite(y) || Double.isInfinite(z));
+    }
+
+    /**
+     * Test for the equality of two 3D vectors.
+     * <p>
+     * If all coordinates of two 3D vectors are exactly the same, and none are
+     * <code>Double.NaN</code>, the two 3D vectors are considered to be equal.
+     * </p>
+     * <p>
+     * <code>NaN</code> coordinates are considered to affect globally the vector
+     * and be equals to each other - i.e, if either (or all) coordinates of the
+     * 3D vector are equal to <code>Double.NaN</code>, the 3D vector is equal to
+     * {@link #NaN}.
+     * </p>
+     *
+     * @param other Object to test for equality to this
+     * @return true if two 3D vector objects are equal, false if
+     *         object is null, not an instance of Cartesian3D, or
+     *         not equal to this Cartesian3D instance
+     *
+     */
+    @Override
+    public boolean equals(Object other) {
+
+        if (this == other) {
+            return true;
+        }
+
+        if (other instanceof Cartesian3D) {
+            final Cartesian3D rhs = (Cartesian3D)other;
+            if (rhs.isNaN()) {
+                return this.isNaN();
+            }
+
+            return (x == rhs.x) && (y == rhs.y) && (z == rhs.z);
+        }
+        return false;
+    }
+
+    /**
+     * Get a hashCode for the 3D vector.
+     * <p>
+     * All NaN values have the same hash code.</p>
+     *
+     * @return a hash code value for this object
+     */
+    @Override
+    public int hashCode() {
+        if (isNaN()) {
+            return 642;
+        }
+        return 643 * (164 * Double.hashCode(x) +  3 * Double.hashCode(y) +  Double.hashCode(z));
+    }
+
+    /** {@inheritDoc}
+     * <p>
+     * The implementation uses specific multiplication and addition
+     * algorithms to preserve accuracy and reduce cancellation effects.
+     * It should be very accurate even for nearly orthogonal vectors.
+     * </p>
+     * @see LinearCombination#value(double, double, double, double, double, double)
+     */
+    @Override
+    public double dotProduct(final Vector<Euclidean3D> v) {
+        final Cartesian3D v3 = (Cartesian3D) v;
+        return LinearCombination.value(x, v3.x, y, v3.y, z, v3.z);
+    }
+
+    /** Compute the cross-product of the instance with another vector.
+     * @param v other vector
+     * @return the cross product this ^ v as a new Cartesian3D
+     */
+    public Cartesian3D crossProduct(final Vector<Euclidean3D> v) {
+        final Cartesian3D v3 = (Cartesian3D) v;
+        return new Cartesian3D(LinearCombination.value(y, v3.z, -z, v3.y),
+                            LinearCombination.value(z, v3.x, -x, v3.z),
+                            LinearCombination.value(x, v3.y, -y, v3.x));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double distance1(Vector<Euclidean3D> v) {
+        final Cartesian3D v3 = (Cartesian3D) v;
+        final double dx = Math.abs(v3.x - x);
+        final double dy = Math.abs(v3.y - y);
+        final double dz = Math.abs(v3.z - z);
+        return dx + dy + dz;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double distance(Point<Euclidean3D> p) {
+        return distance((Cartesian3D) p);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double distance(Vector<Euclidean3D> v) {
+        return distance((Cartesian3D) v);
+    }
+
+    /** Compute the distance between the instance and other coordinates.
+     * @param c other coordinates
+     * @return the distance between the instance and c
+     */
+    public double distance(Cartesian3D c) {
+        final double dx = c.x - x;
+        final double dy = c.y - y;
+        final double dz = c.z - z;
+        return Math.sqrt(dx * dx + dy * dy + dz * dz);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double distanceInf(Vector<Euclidean3D> v) {
+        final Cartesian3D v3 = (Cartesian3D) v;
+        final double dx = Math.abs(v3.x - x);
+        final double dy = Math.abs(v3.y - y);
+        final double dz = Math.abs(v3.z - z);
+        return Math.max(Math.max(dx, dy), dz);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double distanceSq(Vector<Euclidean3D> v) {
+        final Cartesian3D v3 = (Cartesian3D) v;
+        final double dx = v3.x - x;
+        final double dy = v3.y - y;
+        final double dz = v3.z - z;
+        return dx * dx + dy * dy + dz * dz;
+    }
+
+    /** Compute the dot-product of two vectors.
+     * @param v1 first vector
+     * @param v2 second vector
+     * @return the dot product v1.v2
+     */
+    public static double dotProduct(Cartesian3D v1, Cartesian3D v2) {
+        return v1.dotProduct(v2);
+    }
+
+    /** Compute the cross-product of two vectors.
+     * @param v1 first vector
+     * @param v2 second vector
+     * @return the cross product v1 ^ v2 as a new Vector
+     */
+    public static Cartesian3D crossProduct(final Cartesian3D v1, final Cartesian3D v2) {
+        return v1.crossProduct(v2);
+    }
+
+    /** Compute the distance between two vectors according to the L<sub>1</sub> norm.
+     * <p>Calling this method is equivalent to calling:
+     * <code>v1.subtract(v2).getNorm1()</code> except that no intermediate
+     * vector is built</p>
+     * @param v1 first vector
+     * @param v2 second vector
+     * @return the distance between v1 and v2 according to the L<sub>1</sub> norm
+     */
+    public static double distance1(Cartesian3D v1, Cartesian3D v2) {
+        return v1.distance1(v2);
+    }
+
+    /** Compute the distance between two vectors according to the L<sub>2</sub> norm.
+     * <p>Calling this method is equivalent to calling:
+     * <code>v1.subtract(v2).getNorm()</code> except that no intermediate
+     * vector is built</p>
+     * @param v1 first vector
+     * @param v2 second vector
+     * @return the distance between v1 and v2 according to the L<sub>2</sub> norm
+     */
+    public static double distance(Cartesian3D v1, Cartesian3D v2) {
+        return v1.distance(v2);
+    }
+
+    /** Compute the distance between two vectors according to the L<sub>&infin;</sub> norm.
+     * <p>Calling this method is equivalent to calling:
+     * <code>v1.subtract(v2).getNormInf()</code> except that no intermediate
+     * vector is built</p>
+     * @param v1 first vector
+     * @param v2 second vector
+     * @return the distance between v1 and v2 according to the L<sub>&infin;</sub> norm
+     */
+    public static double distanceInf(Cartesian3D v1, Cartesian3D v2) {
+        return v1.distanceInf(v2);
+    }
+
+    /** Compute the square of the distance between two vectors.
+     * <p>Calling this method is equivalent to calling:
+     * <code>v1.subtract(v2).getNormSq()</code> except that no intermediate
+     * vector is built</p>
+     * @param v1 first vector
+     * @param v2 second vector
+     * @return the square of the distance between v1 and v2
+     */
+    public static double distanceSq(Cartesian3D v1, Cartesian3D v2) {
+        return v1.distanceSq(v2);
+    }
+
+    /** Get a string representation of this vector.
+     * @return a string representation of this vector
+     */
+    @Override
+    public String toString() {
+        return toString(NumberFormat.getInstance());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString(final NumberFormat format) {
+        return "{" + format.format(x) + "; " + format.format(y) + "; " + format.format(z) + "}";
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Euclidean3D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Euclidean3D.java
new file mode 100644
index 0000000..4988476
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Euclidean3D.java
@@ -0,0 +1,75 @@
+/*
+ * 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.geometry.euclidean.threed;
+
+import java.io.Serializable;
+
+import org.apache.commons.geometry.core.Space;
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+
+/**
+ * This class implements a three-dimensional space.
+ */
+public class Euclidean3D implements Serializable, Space {
+
+    /** Serializable version identifier. */
+    private static final long serialVersionUID = 6249091865814886817L;
+
+    /** Private constructor for the singleton.
+     */
+    private Euclidean3D() {
+    }
+
+    /** Get the unique instance.
+     * @return the unique instance
+     */
+    public static Euclidean3D getInstance() {
+        return LazyHolder.INSTANCE;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getDimension() {
+        return 3;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Euclidean2D getSubSpace() {
+        return Euclidean2D.getInstance();
+    }
+
+    // CHECKSTYLE: stop HideUtilityClassConstructor
+    /** Holder for the instance.
+     * <p>We use here the Initialization On Demand Holder Idiom.</p>
+     */
+    private static class LazyHolder {
+        /** Cached field instance. */
+        private static final Euclidean3D INSTANCE = new Euclidean3D();
+    }
+    // CHECKSTYLE: resume HideUtilityClassConstructor
+
+    /** Handle deserialization of the singleton.
+     * @return the singleton instance
+     */
+    private Object readResolve() {
+        // return the singleton instance
+        return LazyHolder.INSTANCE;
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line.java
new file mode 100644
index 0000000..2024032
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Line.java
@@ -0,0 +1,274 @@
+/*
+ * 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.geometry.euclidean.threed;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Vector;
+import org.apache.commons.geometry.core.partitioning.Embedding;
+import org.apache.commons.geometry.euclidean.oned.Cartesian1D;
+import org.apache.commons.geometry.euclidean.oned.Euclidean1D;
+import org.apache.commons.geometry.euclidean.oned.IntervalsSet;
+import org.apache.commons.numbers.core.Precision;
+
+/** The class represent lines in a three dimensional space.
+
+ * <p>Each oriented line is intrinsically associated with an abscissa
+ * which is a coordinate on the line. The point at abscissa 0 is the
+ * orthogonal projection of the origin on the line, another equivalent
+ * way to express this is to say that it is the point of the line
+ * which is closest to the origin. Abscissa increases in the line
+ * direction.</p>0
+ */
+public class Line implements Embedding<Euclidean3D, Euclidean1D> {
+
+    /** Line direction. */
+    private Cartesian3D direction;
+
+    /** Line point closest to the origin. */
+    private Cartesian3D zero;
+
+    /** Tolerance below which points are considered identical. */
+    private final double tolerance;
+
+    /** Build a line from two points.
+     * @param p1 first point belonging to the line (this can be any point)
+     * @param p2 second point belonging to the line (this can be any point, different from p1)
+     * @param tolerance tolerance below which points are considered identical
+     * @exception IllegalArgumentException if the points are equal
+     */
+    public Line(final Cartesian3D p1, final Cartesian3D p2, final double tolerance)
+        throws IllegalArgumentException {
+        reset(p1, p2);
+        this.tolerance = tolerance;
+    }
+
+    /** Copy constructor.
+     * <p>The created instance is completely independent from the
+     * original instance, it is a deep copy.</p>
+     * @param line line to copy
+     */
+    public Line(final Line line) {
+        this.direction = line.direction;
+        this.zero      = line.zero;
+        this.tolerance = line.tolerance;
+    }
+
+    /** Reset the instance as if built from two points.
+     * @param p1 first point belonging to the line (this can be any point)
+     * @param p2 second point belonging to the line (this can be any point, different from p1)
+     * @exception IllegalArgumentException if the points are equal
+     */
+    public void reset(final Cartesian3D p1, final Cartesian3D p2) throws IllegalStateException {
+        final Cartesian3D delta = p2.subtract(p1);
+        final double norm2 = delta.getNormSq();
+        if (norm2 == 0.0) {
+            throw new IllegalArgumentException("Points are equal");
+        }
+        this.direction = new Cartesian3D(1.0 / Math.sqrt(norm2), delta);
+        zero = new Cartesian3D(1.0, p1, -p1.dotProduct(delta) / norm2, delta);
+    }
+
+    /** Get the tolerance below which points are considered identical.
+     * @return tolerance below which points are considered identical
+     */
+    public double getTolerance() {
+        return tolerance;
+    }
+
+    /** Get a line with reversed direction.
+     * @return a new instance, with reversed direction
+     */
+    public Line revert() {
+        final Line reverted = new Line(this);
+        reverted.direction = reverted.direction.negate();
+        return reverted;
+    }
+
+    /** Get the normalized direction vector.
+     * @return normalized direction vector
+     */
+    public Cartesian3D getDirection() {
+        return direction;
+    }
+
+    /** Get the line point closest to the origin.
+     * @return line point closest to the origin
+     */
+    public Cartesian3D getOrigin() {
+        return zero;
+    }
+
+    /** Get the abscissa of a point with respect to the line.
+     * <p>The abscissa is 0 if the projection of the point and the
+     * projection of the frame origin on the line are the same
+     * point.</p>
+     * @param point point to check
+     * @return abscissa of the point
+     */
+    public double getAbscissa(final Cartesian3D point) {
+        return point.subtract(zero).dotProduct(direction);
+    }
+
+    /** Get one point from the line.
+     * @param abscissa desired abscissa for the point
+     * @return one point belonging to the line, at specified abscissa
+     */
+    public Cartesian3D pointAt(final double abscissa) {
+        return new Cartesian3D(1.0, zero, abscissa, direction);
+    }
+
+    /** Transform a space point into a sub-space point.
+     * @param vector n-dimension point of the space
+     * @return (n-1)-dimension point of the sub-space corresponding to
+     * the specified space point
+     */
+    public Cartesian1D toSubSpace(Vector<Euclidean3D> vector) {
+        return toSubSpace((Point<Euclidean3D>) vector);
+    }
+
+    /** Transform a sub-space point into a space point.
+     * @param vector (n-1)-dimension point of the sub-space
+     * @return n-dimension point of the space corresponding to the
+     * specified sub-space point
+     */
+    public Cartesian3D toSpace(Vector<Euclidean1D> vector) {
+        return toSpace((Point<Euclidean1D>) vector);
+    }
+
+    /** {@inheritDoc}
+     * @see #getAbscissa(Cartesian3D)
+     */
+    @Override
+    public Cartesian1D toSubSpace(final Point<Euclidean3D> point) {
+        return toSubSpace((Cartesian3D) point);
+    }
+
+    /** {@inheritDoc}
+     * @see #pointAt(double)
+     */
+    @Override
+    public Cartesian3D toSpace(final Point<Euclidean1D> point) {
+        return toSpace((Cartesian1D) point);
+    }
+
+    /** Transform a space point into a sub-space point.
+     * @param point n-dimension point of the space
+     * @return (n-1)-dimension point of the sub-space corresponding to
+     * the specified space point
+     */
+    public Cartesian1D toSubSpace(final Cartesian3D point) {
+        return new Cartesian1D(getAbscissa(point));
+    }
+
+    /** Transform a sub-space point into a space point.
+     * @param point (n-1)-dimension point of the sub-space
+     * @return n-dimension point of the space corresponding to the
+     * specified sub-space point
+     */
+    public Cartesian3D toSpace(final Cartesian1D point) {
+        return pointAt(point.getX());
+    }
+
+    /** Check if the instance is similar to another line.
+     * <p>Lines are considered similar if they contain the same
+     * points. This does not mean they are equal since they can have
+     * opposite directions.</p>
+     * @param line line to which instance should be compared
+     * @return true if the lines are similar
+     */
+    public boolean isSimilarTo(final Line line) {
+        final double angle = Cartesian3D.angle(direction, line.direction);
+        return ((angle < tolerance) || (angle > (Math.PI - tolerance))) && contains(line.zero);
+    }
+
+    /** Check if the instance contains a point.
+     * @param p point to check
+     * @return true if p belongs to the line
+     */
+    public boolean contains(final Cartesian3D p) {
+        return distance(p) < tolerance;
+    }
+
+    /** Compute the distance between the instance and a point.
+     * @param p to check
+     * @return distance between the instance and the point
+     */
+    public double distance(final Cartesian3D p) {
+        final Cartesian3D d = p.subtract(zero);
+        final Cartesian3D n = new Cartesian3D(1.0, d, -d.dotProduct(direction), direction);
+        return n.getNorm();
+    }
+
+    /** Compute the shortest distance between the instance and another line.
+     * @param line line to check against the instance
+     * @return shortest distance between the instance and the line
+     */
+    public double distance(final Line line) {
+
+        final Cartesian3D normal = Cartesian3D.crossProduct(direction, line.direction);
+        final double n = normal.getNorm();
+        if (n < Precision.SAFE_MIN) {
+            // lines are parallel
+            return distance(line.zero);
+        }
+
+        // signed separation of the two parallel planes that contains the lines
+        final double offset = line.zero.subtract(zero).dotProduct(normal) / n;
+
+        return Math.abs(offset);
+
+    }
+
+    /** Compute the point of the instance closest to another line.
+     * @param line line to check against the instance
+     * @return point of the instance closest to another line
+     */
+    public Cartesian3D closestPoint(final Line line) {
+
+        final double cos = direction.dotProduct(line.direction);
+        final double n = 1 - cos * cos;
+        if (n < Precision.EPSILON) {
+            // the lines are parallel
+            return zero;
+        }
+
+        final Cartesian3D delta0 = line.zero.subtract(zero);
+        final double a        = delta0.dotProduct(direction);
+        final double b        = delta0.dotProduct(line.direction);
+
+        return new Cartesian3D(1, zero, (a - b * cos) / n, direction);
+
+    }
+
+    /** Get the intersection point of the instance and another line.
+     * @param line other line
+     * @return intersection point of the instance and the other line
+     * or null if there are no intersection points
+     */
+    public Cartesian3D intersection(final Line line) {
+        final Cartesian3D closest = closestPoint(line);
+        return line.contains(closest) ? closest : null;
+    }
+
+    /** Build a sub-line covering the whole line.
+     * @return a sub-line covering the whole line
+     */
+    public SubLine wholeLine() {
+        return new SubLine(this, new IntervalsSet(tolerance));
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/OutlineExtractor.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/OutlineExtractor.java
new file mode 100644
index 0000000..3f25191
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/OutlineExtractor.java
@@ -0,0 +1,264 @@
+/*
+ * 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.geometry.euclidean.threed;
+
+import java.util.ArrayList;
+
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+import org.apache.commons.geometry.euclidean.twod.PolygonsSet;
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.partitioning.AbstractSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.BSPTree;
+import org.apache.commons.geometry.core.partitioning.BSPTreeVisitor;
+import org.apache.commons.geometry.core.partitioning.BoundaryAttribute;
+import org.apache.commons.geometry.core.partitioning.RegionFactory;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.euclidean.twod.Cartesian2D;
+
+/** Extractor for {@link PolygonsSet polyhedrons sets} outlines.
+ * <p>This class extracts the 2D outlines from {{@link PolygonsSet
+ * polyhedrons sets} in a specified projection plane.</p>
+ */
+public class OutlineExtractor {
+
+    /** Abscissa axis of the projection plane. */
+    private final Cartesian3D u;
+
+    /** Ordinate axis of the projection plane. */
+    private final Cartesian3D v;
+
+    /** Normal of the projection plane (viewing direction). */
+    private final Cartesian3D w;
+
+    /** Build an extractor for a specific projection plane.
+     * @param u abscissa axis of the projection point
+     * @param v ordinate axis of the projection point
+     */
+    public OutlineExtractor(final Cartesian3D u, final Cartesian3D v) {
+        this.u = u;
+        this.v = v;
+        w = Cartesian3D.crossProduct(u, v);
+    }
+
+    /** Extract the outline of a polyhedrons set.
+     * @param polyhedronsSet polyhedrons set whose outline must be extracted
+     * @return an outline, as an array of loops.
+     */
+    public Cartesian2D[][] getOutline(final PolyhedronsSet polyhedronsSet) {
+
+        // project all boundary facets into one polygons set
+        final BoundaryProjector projector = new BoundaryProjector(polyhedronsSet.getTolerance());
+        polyhedronsSet.getTree(true).visit(projector);
+        final PolygonsSet projected = projector.getProjected();
+
+        // Remove the spurious intermediate vertices from the outline
+        final Cartesian2D[][] outline = projected.getVertices();
+        for (int i = 0; i < outline.length; ++i) {
+            final Cartesian2D[] rawLoop = outline[i];
+            int end = rawLoop.length;
+            int j = 0;
+            while (j < end) {
+                if (pointIsBetween(rawLoop, end, j)) {
+                    // the point should be removed
+                    for (int k = j; k < (end - 1); ++k) {
+                        rawLoop[k] = rawLoop[k + 1];
+                    }
+                    --end;
+                } else {
+                    // the point remains in the loop
+                    ++j;
+                }
+            }
+            if (end != rawLoop.length) {
+                // resize the array
+                outline[i] = new Cartesian2D[end];
+                System.arraycopy(rawLoop, 0, outline[i], 0, end);
+            }
+        }
+
+        return outline;
+
+    }
+
+    /** Check if a point is geometrically between its neighbor in an array.
+     * <p>The neighbors are computed considering the array is a loop
+     * (i.e. point at index (n-1) is before point at index 0)</p>
+     * @param loop points array
+     * @param n number of points to consider in the array
+     * @param i index of the point to check (must be between 0 and n-1)
+     * @return true if the point is exactly between its neighbors
+     */
+    private boolean pointIsBetween(final Cartesian2D[] loop, final int n, final int i) {
+        final Cartesian2D previous = loop[(i + n - 1) % n];
+        final Cartesian2D current  = loop[i];
+        final Cartesian2D next     = loop[(i + 1) % n];
+        final double dx1       = current.getX() - previous.getX();
+        final double dy1       = current.getY() - previous.getY();
+        final double dx2       = next.getX()    - current.getX();
+        final double dy2       = next.getY()    - current.getY();
+        final double cross     = dx1 * dy2 - dx2 * dy1;
+        final double dot       = dx1 * dx2 + dy1 * dy2;
+        final double d1d2      = Math.sqrt((dx1 * dx1 + dy1 * dy1) * (dx2 * dx2 + dy2 * dy2));
+        return (Math.abs(cross) <= (1.0e-6 * d1d2)) && (dot >= 0.0);
+    }
+
+    /** Visitor projecting the boundary facets on a plane. */
+    private class BoundaryProjector implements BSPTreeVisitor<Euclidean3D> {
+
+        /** Projection of the polyhedrons set on the plane. */
+        private PolygonsSet projected;
+
+        /** Tolerance below which points are considered identical. */
+        private final double tolerance;
+
+        /** Simple constructor.
+         * @param tolerance tolerance below which points are considered identical
+         */
+        BoundaryProjector(final double tolerance) {
+            this.projected = new PolygonsSet(new BSPTree<Euclidean2D>(Boolean.FALSE), tolerance);
+            this.tolerance = tolerance;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Order visitOrder(final BSPTree<Euclidean3D> node) {
+            return Order.MINUS_SUB_PLUS;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void visitInternalNode(final BSPTree<Euclidean3D> node) {
+            @SuppressWarnings("unchecked")
+            final BoundaryAttribute<Euclidean3D> attribute =
+                (BoundaryAttribute<Euclidean3D>) node.getAttribute();
+            if (attribute.getPlusOutside() != null) {
+                addContribution(attribute.getPlusOutside(), false);
+            }
+            if (attribute.getPlusInside() != null) {
+                addContribution(attribute.getPlusInside(), true);
+            }
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void visitLeafNode(final BSPTree<Euclidean3D> node) {
+        }
+
+        /** Add he contribution of a boundary facet.
+         * @param facet boundary facet
+         * @param reversed if true, the facet has the inside on its plus side
+         */
+        private void addContribution(final SubHyperplane<Euclidean3D> facet, final boolean reversed) {
+
+            // extract the vertices of the facet
+            @SuppressWarnings("unchecked")
+            final AbstractSubHyperplane<Euclidean3D, Euclidean2D> absFacet =
+                (AbstractSubHyperplane<Euclidean3D, Euclidean2D>) facet;
+            final Plane plane    = (Plane) facet.getHyperplane();
+
+            final double scal = plane.getNormal().dotProduct(w);
+            if (Math.abs(scal) > 1.0e-3) {
+                Cartesian2D[][] vertices =
+                    ((PolygonsSet) absFacet.getRemainingRegion()).getVertices();
+
+                if ((scal < 0) ^ reversed) {
+                    // the facet is seen from the inside,
+                    // we need to invert its boundary orientation
+                    final Cartesian2D[][] newVertices = new Cartesian2D[vertices.length][];
+                    for (int i = 0; i < vertices.length; ++i) {
+                        final Cartesian2D[] loop = vertices[i];
+                        final Cartesian2D[] newLoop = new Cartesian2D[loop.length];
+                        if (loop[0] == null) {
+                            newLoop[0] = null;
+                            for (int j = 1; j < loop.length; ++j) {
+                                newLoop[j] = loop[loop.length - j];
+                            }
+                        } else {
+                            for (int j = 0; j < loop.length; ++j) {
+                                newLoop[j] = loop[loop.length - (j + 1)];
+                            }
+                        }
+                        newVertices[i] = newLoop;
+                    }
+
+                    // use the reverted vertices
+                    vertices = newVertices;
+
+                }
+
+                // compute the projection of the facet in the outline plane
+                final ArrayList<SubHyperplane<Euclidean2D>> edges = new ArrayList<>();
+                for (Cartesian2D[] loop : vertices) {
+                    final boolean closed = loop[0] != null;
+                    int previous         = closed ? (loop.length - 1) : 1;
+                    Cartesian3D previous3D  = plane.toSpace(loop[previous]);
+                    int current          = (previous + 1) % loop.length;
+                    Cartesian2D pPoint       = new Cartesian2D(previous3D.dotProduct(u),
+                                                         previous3D.dotProduct(v));
+                    while (current < loop.length) {
+
+                        final Cartesian3D current3D = plane.toSpace((Point<Euclidean2D>) loop[current]);
+                        final Cartesian2D  cPoint    = new Cartesian2D(current3D.dotProduct(u),
+                                                                 current3D.dotProduct(v));
+                        final org.apache.commons.geometry.euclidean.twod.Line line =
+                            new org.apache.commons.geometry.euclidean.twod.Line(pPoint, cPoint, tolerance);
+                        SubHyperplane<Euclidean2D> edge = line.wholeHyperplane();
+
+                        if (closed || (previous != 1)) {
+                            // the previous point is a real vertex
+                            // it defines one bounding point of the edge
+                            final double angle = line.getAngle() + 0.5 * Math.PI;
+                            final org.apache.commons.geometry.euclidean.twod.Line l =
+                                new org.apache.commons.geometry.euclidean.twod.Line(pPoint, angle, tolerance);
+                            edge = edge.split(l).getPlus();
+                        }
+
+                        if (closed || (current != (loop.length - 1))) {
+                            // the current point is a real vertex
+                            // it defines one bounding point of the edge
+                            final double angle = line.getAngle() + 0.5 * Math.PI;
+                            final org.apache.commons.geometry.euclidean.twod.Line l =
+                                new org.apache.commons.geometry.euclidean.twod.Line(cPoint, angle, tolerance);
+                            edge = edge.split(l).getMinus();
+                        }
+
+                        edges.add(edge);
+
+                        previous   = current++;
+                        previous3D = current3D;
+                        pPoint     = cPoint;
+
+                    }
+                }
+                final PolygonsSet projectedFacet = new PolygonsSet(edges, tolerance);
+
+                // add the contribution of the facet to the global outline
+                projected = (PolygonsSet) new RegionFactory<Euclidean2D>().union(projected, projectedFacet);
+
+            }
+        }
+
+        /** Get the projection of the polyhedrons set on the plane.
+         * @return projection of the polyhedrons set on the plane
+         */
+        public PolygonsSet getProjected() {
+            return projected;
+        }
+
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Plane.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Plane.java
new file mode 100644
index 0000000..8a54620
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Plane.java
@@ -0,0 +1,498 @@
+/*
+ * 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.geometry.euclidean.threed;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Vector;
+import org.apache.commons.geometry.core.partitioning.Embedding;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.euclidean.oned.Cartesian1D;
+import org.apache.commons.geometry.euclidean.twod.Cartesian2D;
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+import org.apache.commons.geometry.euclidean.twod.PolygonsSet;
+
+/** The class represent planes in a three dimensional space.
+ */
+public class Plane implements Hyperplane<Euclidean3D>, Embedding<Euclidean3D, Euclidean2D> {
+
+    /** Offset of the origin with respect to the plane. */
+    private double originOffset;
+
+    /** Origin of the plane frame. */
+    private Cartesian3D origin;
+
+    /** First vector of the plane frame (in plane). */
+    private Cartesian3D u;
+
+    /** Second vector of the plane frame (in plane). */
+    private Cartesian3D v;
+
+    /** Third vector of the plane frame (plane normal). */
+    private Cartesian3D w;
+
+    /** Tolerance below which points are considered identical. */
+    private final double tolerance;
+
+    /** Build a plane normal to a given direction and containing the origin.
+     * @param normal normal direction to the plane
+     * @param tolerance tolerance below which points are considered identical
+     * @exception IllegalArgumentException if the normal norm is too small
+     */
+    public Plane(final Cartesian3D normal, final double tolerance)
+        throws IllegalArgumentException {
+        setNormal(normal);
+        this.tolerance = tolerance;
+        originOffset = 0;
+        setFrame();
+    }
+
+    /** Build a plane from a point and a normal.
+     * @param p point belonging to the plane
+     * @param normal normal direction to the plane
+     * @param tolerance tolerance below which points are considered identical
+     * @exception IllegalArgumentException if the normal norm is too small
+     */
+    public Plane(final Cartesian3D p, final Cartesian3D normal, final double tolerance)
+        throws IllegalArgumentException {
+        setNormal(normal);
+        this.tolerance = tolerance;
+        originOffset = -p.dotProduct(w);
+        setFrame();
+    }
+
+    /** Build a plane from three points.
+     * <p>The plane is oriented in the direction of
+     * {@code (p2-p1) ^ (p3-p1)}</p>
+     * @param p1 first point belonging to the plane
+     * @param p2 second point belonging to the plane
+     * @param p3 third point belonging to the plane
+     * @param tolerance tolerance below which points are considered identical
+     * @exception IllegalArgumentException if the points do not constitute a plane
+     */
+    public Plane(final Cartesian3D p1, final Cartesian3D p2, final Cartesian3D p3, final double tolerance)
+        throws IllegalArgumentException {
+        this(p1, p2.subtract(p1).crossProduct(p3.subtract(p1)), tolerance);
+    }
+
+    /** Copy constructor.
+     * <p>The instance created is completely independent of the original
+     * one. A deep copy is used, none of the underlying object are
+     * shared.</p>
+     * @param plane plane to copy
+     */
+    public Plane(final Plane plane) {
+        originOffset = plane.originOffset;
+        origin       = plane.origin;
+        u            = plane.u;
+        v            = plane.v;
+        w            = plane.w;
+        tolerance    = plane.tolerance;
+    }
+
+    /** Copy the instance.
+     * <p>The instance created is completely independant of the original
+     * one. A deep copy is used, none of the underlying objects are
+     * shared (except for immutable objects).</p>
+     * @return a new hyperplane, copy of the instance
+     */
+    @Override
+    public Plane copySelf() {
+        return new Plane(this);
+    }
+
+    /** Reset the instance as if built from a point and a normal.
+     * @param p point belonging to the plane
+     * @param normal normal direction to the plane
+     * @exception IllegalArgumentException if the normal norm is too small
+     */
+    public void reset(final Cartesian3D p, final Cartesian3D normal) throws IllegalArgumentException {
+        setNormal(normal);
+        originOffset = -p.dotProduct(w);
+        setFrame();
+    }
+
+    /** Reset the instance from another one.
+     * <p>The updated instance is completely independant of the original
+     * one. A deep reset is used none of the underlying object is
+     * shared.</p>
+     * @param original plane to reset from
+     */
+    public void reset(final Plane original) {
+        originOffset = original.originOffset;
+        origin       = original.origin;
+        u            = original.u;
+        v            = original.v;
+        w            = original.w;
+    }
+
+    /** Set the normal vactor.
+     * @param normal normal direction to the plane (will be copied)
+     * @exception IllegalArgumentException if the normal norm is too close to zero
+     */
+    private void setNormal(final Cartesian3D normal) throws IllegalArgumentException {
+        final double norm = normal.getNorm();
+        if (norm < 1.0e-10) {
+            throw new IllegalArgumentException("Norm is zero");
+        }
+        w = new Cartesian3D(1.0 / norm, normal);
+    }
+
+    /** Reset the plane frame.
+     */
+    private void setFrame() {
+        origin = new Cartesian3D(-originOffset, w);
+        u = w.orthogonal();
+        v = Cartesian3D.crossProduct(w, u);
+    }
+
+    /** Get the origin point of the plane frame.
+     * <p>The point returned is the orthogonal projection of the
+     * 3D-space origin in the plane.</p>
+     * @return the origin point of the plane frame (point closest to the
+     * 3D-space origin)
+     */
+    public Cartesian3D getOrigin() {
+        return origin;
+    }
+
+    /** Get the normalized normal vector.
+     * <p>The frame defined by ({@link #getU getU}, {@link #getV getV},
+     * {@link #getNormal getNormal}) is a rigth-handed orthonormalized
+     * frame).</p>
+     * @return normalized normal vector
+     * @see #getU
+     * @see #getV
+     */
+    public Cartesian3D getNormal() {
+        return w;
+    }
+
+    /** Get the plane first canonical vector.
+     * <p>The frame defined by ({@link #getU getU}, {@link #getV getV},
+     * {@link #getNormal getNormal}) is a rigth-handed orthonormalized
+     * frame).</p>
+     * @return normalized first canonical vector
+     * @see #getV
+     * @see #getNormal
+     */
+    public Cartesian3D getU() {
+        return u;
+    }
+
+    /** Get the plane second canonical vector.
+     * <p>The frame defined by ({@link #getU getU}, {@link #getV getV},
+     * {@link #getNormal getNormal}) is a rigth-handed orthonormalized
+     * frame).</p>
+     * @return normalized second canonical vector
+     * @see #getU
+     * @see #getNormal
+     */
+    public Cartesian3D getV() {
+        return v;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Point<Euclidean3D> project(Point<Euclidean3D> point) {
+        return toSpace(toSubSpace(point));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getTolerance() {
+        return tolerance;
+    }
+
+    /** Revert the plane.
+     * <p>Replace the instance by a similar plane with opposite orientation.</p>
+     * <p>The new plane frame is chosen in such a way that a 3D point that had
+     * {@code (x, y)} in-plane coordinates and {@code z} offset with
+     * respect to the plane and is unaffected by the change will have
+     * {@code (y, x)} in-plane coordinates and {@code -z} offset with
+     * respect to the new plane. This means that the {@code u} and {@code v}
+     * vectors returned by the {@link #getU} and {@link #getV} methods are exchanged,
+     * and the {@code w} vector returned by the {@link #getNormal} method is
+     * reversed.</p>
+     */
+    public void revertSelf() {
+        final Cartesian3D tmp = u;
+        u = v;
+        v = tmp;
+        w = w.negate();
+        originOffset = -originOffset;
+    }
+
+    /** Transform a space vector into a sub-space vector.
+     * @param vector n-dimension vector of the space
+     * @return (n-1)-dimension vector of the sub-space corresponding to
+     * the specified space vector
+     */
+    public Cartesian2D toSubSpace(Vector<Euclidean3D> vector) {
+        return toSubSpace((Cartesian3D) vector);
+    }
+
+    /** Transform a sub-space point into a space point.
+     * @param vector (n-1)-dimension point of the sub-space
+     * @return n-dimension point of the space corresponding to the
+     * specified sub-space point
+     */
+    public Cartesian3D toSpace(Vector<Euclidean2D> vector) {
+        return toSpace((Cartesian2D) vector);
+    }
+
+    /** Transform a 3D space point into an in-plane point.
+     * @param point point of the space (must be a {@link Cartesian3D} instance)
+     * @return in-plane point
+     * @see #toSpace
+     */
+    @Override
+    public Cartesian2D toSubSpace(final Point<Euclidean3D> point) {
+        return toSubSpace((Cartesian3D) point);
+    }
+
+    /** Transform an in-plane point into a 3D space point.
+     * @param point in-plane point (must be a {@link Cartesian2D} instance)
+     * @return 3D space point
+     * @see #toSubSpace
+     */
+    @Override
+    public Cartesian3D toSpace(final Point<Euclidean2D> point) {
+        return toSpace((Cartesian2D) point);
+    }
+
+    /** Transform a 3D space point into an in-plane point.
+     * @param point point of the space
+     * @return in-plane point
+     * @see #toSpace
+     */
+    public Cartesian2D toSubSpace(final Cartesian3D point) {
+        return new Cartesian2D(point.dotProduct(u), point.dotProduct(v));
+    }
+
+    /** Transform an in-plane point into a 3D space point.
+     * @param point in-plane point
+     * @return 3D space point
+     * @see #toSubSpace
+     */
+    public Cartesian3D toSpace(final Cartesian2D point) {
+        return new Cartesian3D(point.getX(), u, point.getY(), v, -originOffset, w);
+    }
+
+    /** Get one point from the 3D-space.
+     * @param inPlane desired in-plane coordinates for the point in the
+     * plane
+     * @param offset desired offset for the point
+     * @return one point in the 3D-space, with given coordinates and offset
+     * relative to the plane
+     */
+    public Cartesian3D getPointAt(final Cartesian2D inPlane, final double offset) {
+        return new Cartesian3D(inPlane.getX(), u, inPlane.getY(), v, offset - originOffset, w);
+    }
+
+    /** Check if the instance is similar to another plane.
+     * <p>Planes are considered similar if they contain the same
+     * points. This does not mean they are equal since they can have
+     * opposite normals.</p>
+     * @param plane plane to which the instance is compared
+     * @return true if the planes are similar
+     */
+    public boolean isSimilarTo(final Plane plane) {
+        final double angle = Cartesian3D.angle(w, plane.w);
+        return ((angle < 1.0e-10) && (Math.abs(originOffset - plane.originOffset) < tolerance)) ||
+               ((angle > (Math.PI - 1.0e-10)) && (Math.abs(originOffset + plane.originOffset) < tolerance));
+    }
+
+    /** Rotate the plane around the specified point.
+     * <p>The instance is not modified, a new instance is created.</p>
+     * @param center rotation center
+     * @param rotation vectorial rotation operator
+     * @return a new plane
+     */
+    public Plane rotate(final Cartesian3D center, final Rotation rotation) {
+
+        final Cartesian3D delta = origin.subtract(center);
+        final Plane plane = new Plane(center.add(rotation.applyTo(delta)),
+                                      rotation.applyTo(w), tolerance);
+
+        // make sure the frame is transformed as desired
+        plane.u = rotation.applyTo(u);
+        plane.v = rotation.applyTo(v);
+
+        return plane;
+
+    }
+
+    /** Translate the plane by the specified amount.
+     * <p>The instance is not modified, a new instance is created.</p>
+     * @param translation translation to apply
+     * @return a new plane
+     */
+    public Plane translate(final Cartesian3D translation) {
+
+        final Plane plane = new Plane(origin.add(translation), w, tolerance);
+
+        // make sure the frame is transformed as desired
+        plane.u = u;
+        plane.v = v;
+
+        return plane;
+
+    }
+
+    /** Get the intersection of a line with the instance.
+     * @param line line intersecting the instance
+     * @return intersection point between between the line and the
+     * instance (null if the line is parallel to the instance)
+     */
+    public Cartesian3D intersection(final Line line) {
+        final Cartesian3D direction = line.getDirection();
+        final double   dot       = w.dotProduct(direction);
+        if (Math.abs(dot) < 1.0e-10) {
+            return null;
+        }
+        final Cartesian3D point = line.toSpace(Cartesian1D.ZERO);
+        final double   k     = -(originOffset + w.dotProduct(point)) / dot;
+        return new Cartesian3D(1.0, point, k, direction);
+    }
+
+    /** Build the line shared by the instance and another plane.
+     * @param other other plane
+     * @return line at the intersection of the instance and the
+     * other plane (really a {@link Line Line} instance)
+     */
+    public Line intersection(final Plane other) {
+        final Cartesian3D direction = Cartesian3D.crossProduct(w, other.w);
+        if (direction.getNorm() < tolerance) {
+            return null;
+        }
+        final Cartesian3D point = intersection(this, other, new Plane(direction, tolerance));
+        return new Line(point, point.add(direction), tolerance);
+    }
+
+    /** Get the intersection point of three planes.
+     * @param plane1 first plane1
+     * @param plane2 second plane2
+     * @param plane3 third plane2
+     * @return intersection point of three planes, null if some planes are parallel
+     */
+    public static Cartesian3D intersection(final Plane plane1, final Plane plane2, final Plane plane3) {
+
+        // coefficients of the three planes linear equations
+        final double a1 = plane1.w.getX();
+        final double b1 = plane1.w.getY();
+        final double c1 = plane1.w.getZ();
+        final double d1 = plane1.originOffset;
+
+        final double a2 = plane2.w.getX();
+        final double b2 = plane2.w.getY();
+        final double c2 = plane2.w.getZ();
+        final double d2 = plane2.originOffset;
+
+        final double a3 = plane3.w.getX();
+        final double b3 = plane3.w.getY();
+        final double c3 = plane3.w.getZ();
+        final double d3 = plane3.originOffset;
+
+        // direct Cramer resolution of the linear system
+        // (this is still feasible for a 3x3 system)
+        final double a23         = b2 * c3 - b3 * c2;
+        final double b23         = c2 * a3 - c3 * a2;
+        final double c23         = a2 * b3 - a3 * b2;
+        final double determinant = a1 * a23 + b1 * b23 + c1 * c23;
+        if (Math.abs(determinant) < 1.0e-10) {
+            return null;
+        }
+
+        final double r = 1.0 / determinant;
+        return new Cartesian3D(
+                            (-a23 * d1 - (c1 * b3 - c3 * b1) * d2 - (c2 * b1 - c1 * b2) * d3) * r,
+                            (-b23 * d1 - (c3 * a1 - c1 * a3) * d2 - (c1 * a2 - c2 * a1) * d3) * r,
+                            (-c23 * d1 - (b1 * a3 - b3 * a1) * d2 - (b2 * a1 - b1 * a2) * d3) * r);
+
+    }
+
+    /** Build a region covering the whole hyperplane.
+     * @return a region covering the whole hyperplane
+     */
+    @Override
+    public SubPlane wholeHyperplane() {
+        return new SubPlane(this, new PolygonsSet(tolerance));
+    }
+
+    /** Build a region covering the whole space.
+     * @return a region containing the instance (really a {@link
+     * PolyhedronsSet PolyhedronsSet} instance)
+     */
+    @Override
+    public PolyhedronsSet wholeSpace() {
+        return new PolyhedronsSet(tolerance);
+    }
+
+    /** Check if the instance contains a point.
+     * @param p point to check
+     * @return true if p belongs to the plane
+     */
+    public boolean contains(final Cartesian3D p) {
+        return Math.abs(getOffset(p)) < tolerance;
+    }
+
+    /** Get the offset (oriented distance) of a parallel plane.
+     * <p>This method should be called only for parallel planes otherwise
+     * the result is not meaningful.</p>
+     * <p>The offset is 0 if both planes are the same, it is
+     * positive if the plane is on the plus side of the instance and
+     * negative if it is on the minus side, according to its natural
+     * orientation.</p>
+     * @param plane plane to check
+     * @return offset of the plane
+     */
+    public double getOffset(final Plane plane) {
+        return originOffset + (sameOrientationAs(plane) ? -plane.originOffset : plane.originOffset);
+    }
+
+    /** Get the offset (oriented distance) of a vector.
+     * @param vector vector to check
+     * @return offset of the vector
+     */
+//    public double getOffset(Vector<Euclidean3D> vector) {
+//        return getOffset((Point<Euclidean3D>) vector);
+//    }
+
+    /** Get the offset (oriented distance) of a point.
+     * <p>The offset is 0 if the point is on the underlying hyperplane,
+     * it is positive if the point is on one particular side of the
+     * hyperplane, and it is negative if the point is on the other side,
+     * according to the hyperplane natural orientation.</p>
+     * @param point point to check
+     * @return offset of the point
+     */
+    @Override
+    public double getOffset(final Point<Euclidean3D> point) {
+        return ((Cartesian3D) point).dotProduct(w) + originOffset;
+    }
+
+    /** Check if the instance has the same orientation as another hyperplane.
+     * @param other other hyperplane to check against the instance
+     * @return true if the instance and the other hyperplane have
+     * the same orientation
+     */
+    @Override
+    public boolean sameOrientationAs(final Hyperplane<Euclidean3D> other) {
+        return (((Plane) other).w).dotProduct(w) > 0.0;
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PolyhedronsSet.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PolyhedronsSet.java
new file mode 100644
index 0000000..6cb2771
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/PolyhedronsSet.java
@@ -0,0 +1,705 @@
+/*
+ * 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.geometry.euclidean.threed;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.partitioning.AbstractRegion;
+import org.apache.commons.geometry.core.partitioning.BSPTree;
+import org.apache.commons.geometry.core.partitioning.BSPTreeVisitor;
+import org.apache.commons.geometry.core.partitioning.BoundaryAttribute;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Region;
+import org.apache.commons.geometry.core.partitioning.RegionFactory;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Transform;
+import org.apache.commons.geometry.euclidean.oned.Euclidean1D;
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+import org.apache.commons.geometry.euclidean.twod.PolygonsSet;
+import org.apache.commons.geometry.euclidean.twod.SubLine;
+import org.apache.commons.geometry.euclidean.twod.Cartesian2D;
+
+/** This class represents a 3D region: a set of polyhedrons.
+ */
+public class PolyhedronsSet extends AbstractRegion<Euclidean3D, Euclidean2D> {
+
+    /** Build a polyhedrons set representing the whole real line.
+     * @param tolerance tolerance below which points are considered identical
+     */
+    public PolyhedronsSet(final double tolerance) {
+        super(tolerance);
+    }
+
+    /** Build a polyhedrons set from a BSP tree.
+     * <p>The leaf nodes of the BSP tree <em>must</em> have a
+     * {@code Boolean} attribute representing the inside status of
+     * the corresponding cell (true for inside cells, false for outside
+     * cells). In order to avoid building too many small objects, it is
+     * recommended to use the predefined constants
+     * {@code Boolean.TRUE} and {@code Boolean.FALSE}</p>
+     * <p>
+     * This constructor is aimed at expert use, as building the tree may
+     * be a difficult task. It is not intended for general use and for
+     * performances reasons does not check thoroughly its input, as this would
+     * require walking the full tree each time. Failing to provide a tree with
+     * the proper attributes, <em>will</em> therefore generate problems like
+     * {@link NullPointerException} or {@link ClassCastException} only later on.
+     * This limitation is known and explains why this constructor is for expert
+     * use only. The caller does have the responsibility to provided correct arguments.
+     * </p>
+     * @param tree inside/outside BSP tree representing the region
+     * @param tolerance tolerance below which points are considered identical
+     */
+    public PolyhedronsSet(final BSPTree<Euclidean3D> tree, final double tolerance) {
+        super(tree, tolerance);
+    }
+
+    /** 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
+     * its plus side.</p>
+     * <p>The boundary elements can be in any order, and can form
+     * several non-connected sets (like for example polyhedrons with holes
+     * or a set of disjoint polyhedrons considered as a whole). In
+     * fact, the elements do not even need to be connected together
+     * (their topological connections are not used here). However, if the
+     * boundary does not really separate an inside open from an outside
+     * open (open having here its topological meaning), then subsequent
+     * calls to the {@link Region#checkPoint(Point) checkPoint} method will
+     * not be meaningful anymore.</p>
+     * <p>If the boundary is empty, the region will represent the whole
+     * space.</p>
+     * @param boundary collection of boundary elements, as a
+     * collection of {@link SubHyperplane SubHyperplane} objects
+     * @param tolerance tolerance below which points are considered identical
+     */
+    public PolyhedronsSet(final Collection<SubHyperplane<Euclidean3D>> boundary,
+                          final double tolerance) {
+        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 IllegalArgumentException if some basic sanity checks fail
+     */
+    public PolyhedronsSet(final List<Cartesian3D> 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
+     * @param yMin low bound along the y direction
+     * @param yMax high bound along the y direction
+     * @param zMin low bound along the z direction
+     * @param zMax high bound along the z direction
+     * @param tolerance tolerance below which points are considered identical
+     */
+    public PolyhedronsSet(final double xMin, final double xMax,
+                          final double yMin, final double yMax,
+                          final double zMin, final double zMax,
+                          final double tolerance) {
+        super(buildBoundary(xMin, xMax, yMin, yMax, zMin, zMax, tolerance), tolerance);
+    }
+
+    /** Build a parallellepipedic box boundary.
+     * @param xMin low bound along the x direction
+     * @param xMax high bound along the x direction
+     * @param yMin low bound along the y direction
+     * @param yMax high bound along the y direction
+     * @param zMin low bound along the z direction
+     * @param zMax high bound along the z direction
+     * @param tolerance tolerance below which points are considered identical
+     * @return boundary tree
+     */
+    private static BSPTree<Euclidean3D> buildBoundary(final double xMin, final double xMax,
+                                                      final double yMin, final double yMax,
+                                                      final double zMin, final double zMax,
+                                                      final double tolerance) {
+        if ((xMin >= xMax - tolerance) || (yMin >= yMax - tolerance) || (zMin >= zMax - tolerance)) {
+            // too thin box, build an empty polygons set
+            return new BSPTree<>(Boolean.FALSE);
+        }
+        final Plane pxMin = new Plane(new Cartesian3D(xMin, 0,    0),   Cartesian3D.MINUS_I, tolerance);
+        final Plane pxMax = new Plane(new Cartesian3D(xMax, 0,    0),   Cartesian3D.PLUS_I,  tolerance);
+        final Plane pyMin = new Plane(new Cartesian3D(0,    yMin, 0),   Cartesian3D.MINUS_J, tolerance);
+        final Plane pyMax = new Plane(new Cartesian3D(0,    yMax, 0),   Cartesian3D.PLUS_J,  tolerance);
+        final Plane pzMin = new Plane(new Cartesian3D(0,    0,   zMin), Cartesian3D.MINUS_K, tolerance);
+        final Plane pzMax = new Plane(new Cartesian3D(0,    0,   zMax), Cartesian3D.PLUS_K,  tolerance);
+        final Region<Euclidean3D> boundary =
+        new RegionFactory<Euclidean3D>().buildConvex(pxMin, pxMax, pyMin, pyMax, pzMin, pzMax);
+        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 IllegalArgumentException if some basic sanity checks fail
+     */
+    private static List<SubHyperplane<Euclidean3D>> buildBoundary(final List<Cartesian3D> vertices,
+                                                                  final List<int[]> facets,
+                                                                  final double tolerance) {
+
+        // check vertices distances
+        for (int i = 0; i < vertices.size() - 1; ++i) {
+            final Cartesian3D vi = vertices.get(i);
+            for (int j = i + 1; j < vertices.size(); ++j) {
+                if (Cartesian3D.distance(vi, vertices.get(j)) <= tolerance) {
+                    throw new IllegalArgumentException("Vertices are too close near point " + vi);
+                }
+            }
+        }
+
+        // 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 Cartesian3D start = vertices.get(vA);
+                        final Cartesian3D end   = vertices.get(vB);
+                        throw new IllegalArgumentException("Edge joining points " + start + " and " + end + " is connected to one facet only");
+                    }
+                }
+            }
+        }
+
+        final List<SubHyperplane<Euclidean3D>> boundary = new ArrayList<>();
+
+        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 Cartesian2D[] two2Points = new Cartesian2D[facet.length];
+            for (int i = 0 ; i < facet.length; ++i) {
+                final Cartesian3D v = vertices.get(facet[i]);
+                if (!plane.contains(v)) {
+                    throw new IllegalArgumentException("Point " + v + " is out of plane");
+                }
+                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 IllegalArgumentException if some facets have fewer than 3 vertices
+     */
+    private static int[][] findReferences(final List<Cartesian3D> 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 IllegalArgumentException("3 points are required, got only " + facet.length);
+            }
+            for (final int index : facet) {
+                maxFacets = Math.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 IllegalArgumentException if the same vertex appears more than
+     * once in the successors list (which means one facet orientation is wrong)
+
+     */
+    private static int[][] successors(final List<Cartesian3D> 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 Cartesian3D start = vertices.get(v);
+                        final Cartesian3D end   = vertices.get(successors[v][k]);
+                        throw new IllegalArgumentException("Facet orientation mismatch around edge joining points " + start + " and " + end);
+                    }
+                }
+
+            }
+        }
+
+        return successors;
+
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public PolyhedronsSet buildNew(final BSPTree<Euclidean3D> tree) {
+        return new PolyhedronsSet(tree, getTolerance());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void computeGeometricalProperties() {
+        // check simple cases first
+        if (isEmpty()) {
+            setSize(0.0);
+            setBarycenter((Point<Euclidean3D>) Cartesian3D.NaN);
+        }
+        else if (isFull()) {
+            setSize(Double.POSITIVE_INFINITY);
+            setBarycenter((Point<Euclidean3D>) Cartesian3D.NaN);
+        }
+        else {
+            // not empty or full; compute the contribution of all boundary facets
+            final FacetsContributionVisitor contributionVisitor = new FacetsContributionVisitor();
+            getTree(true).visit(contributionVisitor);
+
+            final double size = contributionVisitor.getSize();
+            final Cartesian3D barycenter = contributionVisitor.getBarycenter();
+
+            if (size < 0) {
+                // the polyhedrons set is a finite outside surrounded by an infinite inside
+                setSize(Double.POSITIVE_INFINITY);
+                setBarycenter((Point<Euclidean3D>) Cartesian3D.NaN);
+            } else {
+                // the polyhedrons set is finite
+                setSize(size);
+                setBarycenter((Point<Euclidean3D>) barycenter);
+            }
+        }
+    }
+
+    /** Visitor computing polyhedron geometrical properties.
+     *  The volume of the polyhedron is computed using the equation
+     *  <code>V = (1/3)*&Sigma;<sub>F</sub>[(C<sub>F</sub>&sdot;N<sub>F</sub>)*area(F)]</code>,
+     *  where <code>F</code> represents each face in the polyhedron, <code>C<sub>F</sub></code>
+     *  represents the barycenter of the face, and <code>N<sub>F</sub></code> represents the
+     *  normal of the face. (More details can be found in the article
+     *  <a href="https://en.wikipedia.org/wiki/Polyhedron#Volume";>here</a>.)
+     *  This essentially splits up the polyhedron into pyramids with a polyhedron
+     *  face forming the base of each pyramid.
+     *  The barycenter is computed in a similar way. The barycenter of each pyramid
+     *  is calculated using the fact that it is located 3/4 of the way along the
+     *  line from the apex to the base. The polyhedron barycenter then becomes
+     *  the volume-weighted average of these pyramid centers.
+     */
+    private static class FacetsContributionVisitor implements BSPTreeVisitor<Euclidean3D> {
+
+        /** Accumulator for facet volume contributions. */
+        private double volumeSum;
+
+        /** Accumulator for barycenter contributions. */
+        private Cartesian3D barycenterSum = Cartesian3D.ZERO;
+
+        /** Returns the total computed size (ie, volume) of the polyhedron.
+         * This value will be negative if the polyhedron is "inside-out", meaning
+         * that it has a finite outside surrounded by an infinite inside.
+         * @return the volume.
+         */
+        public double getSize() {
+            // apply the 1/3 pyramid volume scaling factor
+            return volumeSum / 3.0;
+        }
+
+        /** Returns the computed barycenter. This is the volume-weighted average
+         * of contributions from all facets. All coordinates will be NaN if the
+         * region is infinite.
+         * @return the barycenter.
+         */
+        public Cartesian3D getBarycenter() {
+            // Since the volume we used when adding together the facet contributions
+            // was 3x the actual pyramid size, we'll multiply by 1/4 here instead
+            // of 3/4 to adjust for the actual barycenter position in each pyramid.
+            return new Cartesian3D(1.0 / (4 * getSize()), barycenterSum);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Order visitOrder(final BSPTree<Euclidean3D> node) {
+            return Order.MINUS_SUB_PLUS;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void visitInternalNode(final BSPTree<Euclidean3D> node) {
+            @SuppressWarnings("unchecked")
+            final BoundaryAttribute<Euclidean3D> attribute =
+                (BoundaryAttribute<Euclidean3D>) node.getAttribute();
+            if (attribute.getPlusOutside() != null) {
+                addContribution(attribute.getPlusOutside(), false);
+            }
+            if (attribute.getPlusInside() != null) {
+                addContribution(attribute.getPlusInside(), true);
+            }
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void visitLeafNode(final BSPTree<Euclidean3D> node) {
+        }
+
+        /** Add the contribution of a boundary facet.
+         * @param facet boundary facet
+         * @param reversed if true, the facet has the inside on its plus side
+         */
+        private void addContribution(final SubHyperplane<Euclidean3D> facet, final boolean reversed) {
+
+            final Region<Euclidean2D> polygon = ((SubPlane) facet).getRemainingRegion();
+            final double area = polygon.getSize();
+
+            if (Double.isInfinite(area)) {
+                volumeSum = Double.POSITIVE_INFINITY;
+                barycenterSum = Cartesian3D.NaN;
+            } else {
+                final Plane plane = (Plane) facet.getHyperplane();
+                final Cartesian3D facetBarycenter = plane.toSpace(polygon.getBarycenter());
+
+                // the volume here is actually 3x the actual pyramid volume; we'll apply
+                // the final scaling all at once at the end
+                double scaledVolume = area * facetBarycenter.dotProduct(plane.getNormal());
+                if (reversed) {
+                    scaledVolume = -scaledVolume;
+                }
+
+                volumeSum += scaledVolume;
+                barycenterSum = new Cartesian3D(1.0, barycenterSum, scaledVolume, facetBarycenter);
+            }
+        }
+    }
+
+    /** Get the first sub-hyperplane crossed by a semi-infinite line.
+     * @param point start point of the part of the line considered
+     * @param line line to consider (contains point)
+     * @return the first sub-hyperplane crossed by the line after the
+     * given point, or null if the line does not intersect any
+     * sub-hyperplane
+     */
+    public SubHyperplane<Euclidean3D> firstIntersection(final Cartesian3D point, final Line line) {
+        return recurseFirstIntersection(getTree(true), point, line);
+    }
+
+    /** Get the first sub-hyperplane crossed by a semi-infinite line.
+     * @param node current node
+     * @param point start point of the part of the line considered
+     * @param line line to consider (contains point)
+     * @return the first sub-hyperplane crossed by the line after the
+     * given point, or null if the line does not intersect any
+     * sub-hyperplane
+     */
+    private SubHyperplane<Euclidean3D> recurseFirstIntersection(final BSPTree<Euclidean3D> node,
+                                                                final Cartesian3D point,
+                                                                final Line line) {
+
+        final SubHyperplane<Euclidean3D> cut = node.getCut();
+        if (cut == null) {
+            return null;
+        }
+        final BSPTree<Euclidean3D> minus = node.getMinus();
+        final BSPTree<Euclidean3D> plus  = node.getPlus();
+        final Plane                plane = (Plane) cut.getHyperplane();
+
+        // establish search order
+        final double offset = plane.getOffset(point);
+        final boolean in    = Math.abs(offset) < getTolerance();
+        final BSPTree<Euclidean3D> near;
+        final BSPTree<Euclidean3D> far;
+        if (offset < 0) {
+            near = minus;
+            far  = plus;
+        } else {
+            near = plus;
+            far  = minus;
+        }
+
+        if (in) {
+            // search in the cut hyperplane
+            final SubHyperplane<Euclidean3D> facet = boundaryFacet(point, node);
+            if (facet != null) {
+                return facet;
+            }
+        }
+
+        // search in the near branch
+        final SubHyperplane<Euclidean3D> crossed = recurseFirstIntersection(near, point, line);
+        if (crossed != null) {
+            return crossed;
+        }
+
+        if (!in) {
+            // search in the cut hyperplane
+            final Cartesian3D hit3D = plane.intersection(line);
+            if (hit3D != null && line.getAbscissa(hit3D) > line.getAbscissa(point)) {
+                final SubHyperplane<Euclidean3D> facet = boundaryFacet(hit3D, node);
+                if (facet != null) {
+                    return facet;
+                }
+            }
+        }
+
+        // search in the far branch
+        return recurseFirstIntersection(far, point, line);
+
+    }
+
+    /** Check if a point belongs to the boundary part of a node.
+     * @param point point to check
+     * @param node node containing the boundary facet to check
+     * @return the boundary facet this points belongs to (or null if it
+     * does not belong to any boundary facet)
+     */
+    private SubHyperplane<Euclidean3D> boundaryFacet(final Cartesian3D point,
+                                                     final BSPTree<Euclidean3D> node) {
+        final Cartesian2D point2D = ((Plane) node.getCut().getHyperplane()).toSubSpace(point);
+        @SuppressWarnings("unchecked")
+        final BoundaryAttribute<Euclidean3D> attribute =
+            (BoundaryAttribute<Euclidean3D>) node.getAttribute();
+        if ((attribute.getPlusOutside() != null) &&
+            (((SubPlane) attribute.getPlusOutside()).getRemainingRegion().checkPoint(point2D) == Location.INSIDE)) {
+            return attribute.getPlusOutside();
+        }
+        if ((attribute.getPlusInside() != null) &&
+            (((SubPlane) attribute.getPlusInside()).getRemainingRegion().checkPoint(point2D) == Location.INSIDE)) {
+            return attribute.getPlusInside();
+        }
+        return null;
+    }
+
+    /** Rotate the region around the specified point.
+     * <p>The instance is not modified, a new instance is created.</p>
+     * @param center rotation center
+     * @param rotation vectorial rotation operator
+     * @return a new instance representing the rotated region
+     */
+    public PolyhedronsSet rotate(final Cartesian3D center, final Rotation rotation) {
+        return (PolyhedronsSet) applyTransform(new RotationTransform(center, rotation));
+    }
+
+    /** 3D rotation as a Transform. */
+    private static class RotationTransform implements Transform<Euclidean3D, Euclidean2D> {
+
+        /** Center point of the rotation. */
+        private final Cartesian3D   center;
+
+        /** Vectorial rotation. */
+        private final Rotation   rotation;
+
+        /** Cached original hyperplane. */
+        private Plane cachedOriginal;
+
+        /** Cached 2D transform valid inside the cached original hyperplane. */
+        private Transform<Euclidean2D, Euclidean1D>  cachedTransform;
+
+        /** Build a rotation transform.
+         * @param center center point of the rotation
+         * @param rotation vectorial rotation
+         */
+        RotationTransform(final Cartesian3D center, final Rotation rotation) {
+            this.center   = center;
+            this.rotation = rotation;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Cartesian3D apply(final Point<Euclidean3D> point) {
+            final Cartesian3D delta = ((Cartesian3D) point).subtract(center);
+            return new Cartesian3D(1.0, center, 1.0, rotation.applyTo(delta));
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Plane apply(final Hyperplane<Euclidean3D> hyperplane) {
+            return ((Plane) hyperplane).rotate(center, rotation);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public SubHyperplane<Euclidean2D> apply(final SubHyperplane<Euclidean2D> sub,
+                                                final Hyperplane<Euclidean3D> original,
+                                                final Hyperplane<Euclidean3D> transformed) {
+            if (original != cachedOriginal) {
+                // we have changed hyperplane, reset the in-hyperplane transform
+
+                final Plane    oPlane = (Plane) original;
+                final Plane    tPlane = (Plane) transformed;
+                final Cartesian3D p00    = oPlane.getOrigin();
+                final Cartesian3D p10    = oPlane.toSpace(new Cartesian2D(1.0, 0.0));
+                final Cartesian3D p01    = oPlane.toSpace(new Cartesian2D(0.0, 1.0));
+                final Cartesian2D tP00   = tPlane.toSubSpace(apply(p00));
+                final Cartesian2D tP10   = tPlane.toSubSpace(apply(p10));
+                final Cartesian2D tP01   = tPlane.toSubSpace(apply(p01));
+
+                cachedOriginal  = (Plane) original;
+                cachedTransform =
+                        org.apache.commons.geometry.euclidean.twod.Line.getTransform(tP10.getX() - tP00.getX(),
+                                                                                           tP10.getY() - tP00.getY(),
+                                                                                           tP01.getX() - tP00.getX(),
+                                                                                           tP01.getY() - tP00.getY(),
+                                                                                           tP00.getX(),
+                                                                                           tP00.getY());
+
+            }
+            return ((SubLine) sub).applyTransform(cachedTransform);
+        }
+
+    }
+
+    /** Translate the region by the specified amount.
+     * <p>The instance is not modified, a new instance is created.</p>
+     * @param translation translation to apply
+     * @return a new instance representing the translated region
+     */
+    public PolyhedronsSet translate(final Cartesian3D translation) {
+        return (PolyhedronsSet) applyTransform(new TranslationTransform(translation));
+    }
+
+    /** 3D translation as a transform. */
+    private static class TranslationTransform implements Transform<Euclidean3D, Euclidean2D> {
+
+        /** Translation vector. */
+        private final Cartesian3D   translation;
+
+        /** Cached original hyperplane. */
+        private Plane cachedOriginal;
+
+        /** Cached 2D transform valid inside the cached original hyperplane. */
+        private Transform<Euclidean2D, Euclidean1D>  cachedTransform;
+
+        /** Build a translation transform.
+         * @param translation translation vector
+         */
+        TranslationTransform(final Cartesian3D translation) {
+            this.translation = translation;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Cartesian3D apply(final Point<Euclidean3D> point) {
+            return new Cartesian3D(1.0, (Cartesian3D) point, 1.0, translation);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Plane apply(final Hyperplane<Euclidean3D> hyperplane) {
+            return ((Plane) hyperplane).translate(translation);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public SubHyperplane<Euclidean2D> apply(final SubHyperplane<Euclidean2D> sub,
+                                                final Hyperplane<Euclidean3D> original,
+                                                final Hyperplane<Euclidean3D> transformed) {
+            if (original != cachedOriginal) {
+                // we have changed hyperplane, reset the in-hyperplane transform
+
+                final Plane   oPlane = (Plane) original;
+                final Plane   tPlane = (Plane) transformed;
+                final Cartesian2D shift  = tPlane.toSubSpace(apply(oPlane.getOrigin()));
+
+                cachedOriginal  = (Plane) original;
+                cachedTransform =
+                        org.apache.commons.geometry.euclidean.twod.Line.getTransform(1, 0, 0, 1,
+                                                                                           shift.getX(),
+                                                                                           shift.getY());
+
+            }
+
+            return ((SubLine) sub).applyTransform(cachedTransform);
+
+        }
+
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Rotation.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Rotation.java
new file mode 100644
index 0000000..eac61f1
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Rotation.java
@@ -0,0 +1,1419 @@
+/*
+ * 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.geometry.euclidean.threed;
+
+import java.io.Serializable;
+
+import org.apache.commons.numbers.arrays.LinearCombination;
+
+/**
+ * This class implements rotations in a three-dimensional space.
+ *
+ * <p>Rotations can be represented by several different mathematical
+ * entities (matrices, axe and angle, Cardan or Euler angles,
+ * quaternions). This class presents an higher level abstraction, more
+ * user-oriented and hiding this implementation details. Well, for the
+ * curious, we use quaternions for the internal representation. The
+ * user can build a rotation from any of these representations, and
+ * any of these representations can be retrieved from a
+ * <code>Rotation</code> instance (see the various constructors and
+ * getters). In addition, a rotation can also be built implicitly
+ * from a set of vectors and their image.</p>
+ * <p>This implies that this class can be used to convert from one
+ * representation to another one. For example, converting a rotation
+ * matrix into a set of Cardan angles from can be done using the
+ * following single line of code:</p>
+ * <pre>
+ * double[] angles = new Rotation(matrix, 1.0e-10).getAngles(RotationOrder.XYZ);
+ * </pre>
+ * <p>Focus is oriented on what a rotation <em>do</em> rather than on its
+ * underlying representation. Once it has been built, and regardless of its
+ * internal representation, a rotation is an <em>operator</em> which basically
+ * transforms three dimensional {@link Cartesian3D vectors} into other three
+ * dimensional {@link Cartesian3D vectors}. Depending on the application, the
+ * meaning of these vectors may vary and the semantics of the rotation also.</p>
+ * <p>For example in an spacecraft attitude simulation tool, users will often
+ * consider the vectors are fixed (say the Earth direction for example) and the
+ * frames change. The rotation transforms the coordinates of the vector in inertial
+ * frame into the coordinates of the same vector in satellite frame. In this
+ * case, the rotation implicitly defines the relation between the two frames.</p>
+ * <p>Another example could be a telescope control application, where the rotation
+ * would transform the sighting direction at rest into the desired observing
+ * direction when the telescope is pointed towards an object of interest. In this
+ * case the rotation transforms the direction at rest in a topocentric frame
+ * into the sighting direction in the same topocentric frame. This implies in this
+ * case the frame is fixed and the vector moves.</p>
+ * <p>In many case, both approaches will be combined. In our telescope example,
+ * we will probably also need to transform the observing direction in the topocentric
+ * frame into the observing direction in inertial frame taking into account the observatory
+ * location and the Earth rotation, which would essentially be an application of the
+ * first approach.</p>
+ *
+ * <p>These examples show that a rotation is what the user wants it to be. This
+ * class does not push the user towards one specific definition and hence does not
+ * provide methods like <code>projectVectorIntoDestinationFrame</code> or
+ * <code>computeTransformedDirection</code>. It provides simpler and more generic
+ * methods: {@link #applyTo(Cartesian3D) applyTo(Cartesian3D)} and {@link
+ * #applyInverseTo(Cartesian3D) applyInverseTo(Cartesian3D)}.</p>
+ *
+ * <p>Since a rotation is basically a vectorial operator, several rotations can be
+ * composed together and the composite operation <code>r = r<sub>1</sub> o
+ * r<sub>2</sub></code> (which means that for each vector <code>u</code>,
+ * <code>r(u) = r<sub>1</sub>(r<sub>2</sub>(u))</code>) is also a rotation. Hence
+ * we can consider that in addition to vectors, a rotation can be applied to other
+ * rotations as well (or to itself). With our previous notations, we would say we
+ * can apply <code>r<sub>1</sub></code> to <code>r<sub>2</sub></code> and the result
+ * we get is <code>r = r<sub>1</sub> o r<sub>2</sub></code>. For this purpose, the
+ * class provides the methods: {@link #applyTo(Rotation) applyTo(Rotation)} and
+ * {@link #applyInverseTo(Rotation) applyInverseTo(Rotation)}.</p>
+ *
+ * <p>Rotations are guaranteed to be immutable objects.</p>
+ *
+ * @see Cartesian3D
+ * @see RotationOrder
+ */
+
+public class Rotation implements Serializable {
+
+  /** Identity rotation. */
+  public static final Rotation IDENTITY = new Rotation(1.0, 0.0, 0.0, 0.0, false);
+
+  /** Serializable version identifier */
+  private static final long serialVersionUID = -2153622329907944313L;
+
+  /** Error message for Cardan angle singularities */
+  private static final String CARDAN_SINGULARITY_MSG = "Cardan angles singularity";
+
+  /** Error message for Euler angle singularities */
+  private static final String EULER_SINGULARITY_MSG = "Euler angles singularity";
+
+  /** Scalar coordinate of the quaternion. */
+  private final double q0;
+
+  /** First coordinate of the vectorial part of the quaternion. */
+  private final double q1;
+
+  /** Second coordinate of the vectorial part of the quaternion. */
+  private final double q2;
+
+  /** Third coordinate of the vectorial part of the quaternion. */
+  private final double q3;
+
+  /** Build a rotation from the quaternion coordinates.
+   * <p>A rotation can be built from a <em>normalized</em> quaternion,
+   * i.e. a quaternion for which q<sub>0</sub><sup>2</sup> +
+   * q<sub>1</sub><sup>2</sup> + q<sub>2</sub><sup>2</sup> +
+   * q<sub>3</sub><sup>2</sup> = 1. If the quaternion is not normalized,
+   * the constructor can normalize it in a preprocessing step.</p>
+   * <p>Note that some conventions put the scalar part of the quaternion
+   * as the 4<sup>th</sup> component and the vector part as the first three
+   * components. This is <em>not</em> our convention. We put the scalar part
+   * as the first component.</p>
+   * @param q0 scalar part of the quaternion
+   * @param q1 first coordinate of the vectorial part of the quaternion
+   * @param q2 second coordinate of the vectorial part of the quaternion
+   * @param q3 third coordinate of the vectorial part of the quaternion
+   * @param needsNormalization if true, the coordinates are considered
+   * not to be normalized, a normalization preprocessing step is performed
+   * before using them
+   */
+  public Rotation(double q0, double q1, double q2, double q3,
+                  boolean needsNormalization) {
+
+    if (needsNormalization) {
+      // normalization preprocessing
+      double inv = 1.0 / Math.sqrt(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3);
+      q0 *= inv;
+      q1 *= inv;
+      q2 *= inv;
+      q3 *= inv;
+    }
+
+    this.q0 = q0;
+    this.q1 = q1;
+    this.q2 = q2;
+    this.q3 = q3;
+
+  }
+
+  /** Build a rotation from an axis and an angle.
+   * <p>
+   * Calling this constructor is equivalent to call
+   * {@link #Rotation(Cartesian3D, double, RotationConvention)
+   * new Rotation(axis, angle, RotationConvention.VECTOR_OPERATOR)}
+   * </p>
+   * @param axis axis around which to rotate
+   * @param angle rotation angle.
+   * @exception IllegalArgumentException if the axis norm is zero
+   * @deprecated as of 3.6, replaced with {@link #Rotation(Cartesian3D, double, RotationConvention)}
+   */
+  @Deprecated
+  public Rotation(Cartesian3D axis, double angle) throws IllegalArgumentException {
+      this(axis, angle, RotationConvention.VECTOR_OPERATOR);
+  }
+
+  /** Build a rotation from an axis and an angle.
+   * @param axis axis around which to rotate
+   * @param angle rotation angle
+   * @param convention convention to use for the semantics of the angle
+   * @exception IllegalArgumentException if the axis norm is zero
+   */
+  public Rotation(final Cartesian3D axis, final double angle, final RotationConvention convention)
+      throws IllegalArgumentException {
+
+    double norm = axis.getNorm();
+    if (norm == 0) {
+      throw new IllegalArgumentException("Zero norm for rotation axis");
+    }
+
+    double halfAngle = convention == RotationConvention.VECTOR_OPERATOR ? -0.5 * angle : +0.5 * angle;
+    double coeff = Math.sin(halfAngle) / norm;
+
+    q0 = Math.cos (halfAngle);
+    q1 = coeff * axis.getX();
+    q2 = coeff * axis.getY();
+    q3 = coeff * axis.getZ();
+
+  }
+
+  /** Build a rotation from a 3X3 matrix.
+
+   * <p>Rotation matrices are orthogonal matrices, i.e. unit matrices
+   * (which are matrices for which m.m<sup>T</sup> = I) with real
+   * coefficients. The module of the determinant of unit matrices is
+   * 1, among the orthogonal 3X3 matrices, only the ones having a
+   * positive determinant (+1) are rotation matrices.</p>
+
+   * <p>When a rotation is defined by a matrix with truncated values
+   * (typically when it is extracted from a technical sheet where only
+   * four to five significant digits are available), the matrix is not
+   * orthogonal anymore. This constructor handles this case
+   * transparently by using a copy of the given matrix and applying a
+   * correction to the copy in order to perfect its orthogonality. If
+   * the Frobenius norm of the correction needed is above the given
+   * threshold, then the matrix is considered to be too far from a
+   * true rotation matrix and an exception is thrown.<p>
+
+   * @param m rotation matrix
+   * @param threshold convergence threshold for the iterative
+   * orthogonality correction (convergence is reached when the
+   * difference between two steps of the Frobenius norm of the
+   * correction is below this threshold)
+
+   * @exception IllegalArgumentException if the matrix is not a 3X3
+   * matrix, or if it cannot be transformed into an orthogonal matrix
+   * with the given threshold, or if the determinant of the resulting
+   * orthogonal matrix is negative
+   */
+  public Rotation(double[][] m, double threshold)
+    throws IllegalArgumentException {
+
+    // dimension check
+    if ((m.length != 3) || (m[0].length != 3) ||
+        (m[1].length != 3) || (m[2].length != 3)) {
+      throw new IllegalArgumentException("A " + m.length + "x" + m[0].length + " matrix cannot be a rotation matrix");
+    }
+
+    // compute a "close" orthogonal matrix
+    double[][] ort = orthogonalizeMatrix(m, threshold);
+
+    // check the sign of the determinant
+    double det = ort[0][0] * (ort[1][1] * ort[2][2] - ort[2][1] * ort[1][2]) -
+                 ort[1][0] * (ort[0][1] * ort[2][2] - ort[2][1] * ort[0][2]) +
+                 ort[2][0] * (ort[0][1] * ort[1][2] - ort[1][1] * ort[0][2]);
+    if (det < 0.0) {
+      throw new IllegalArgumentException("The closest orthogonal matrix has a negative determinant " + det);
+    }
+
+    double[] quat = mat2quat(ort);
+    q0 = quat[0];
+    q1 = quat[1];
+    q2 = quat[2];
+    q3 = quat[3];
+
+  }
+
+  /** Build the rotation that transforms a pair of vectors into another pair.
+
+   * <p>Except for possible scale factors, if the instance were applied to
+   * the pair (u<sub>1</sub>, u<sub>2</sub>) it will produce the pair
+   * (v<sub>1</sub>, v<sub>2</sub>).</p>
+
+   * <p>If the angular separation between u<sub>1</sub> and u<sub>2</sub> is
+   * not the same as the angular separation between v<sub>1</sub> and
+   * v<sub>2</sub>, then a corrected v'<sub>2</sub> will be used rather than
+   * v<sub>2</sub>, the corrected vector will be in the (&plusmn;v<sub>1</sub>,
+   * +v<sub>2</sub>) half-plane.</p>
+
+   * @param u1 first vector of the origin pair
+   * @param u2 second vector of the origin pair
+   * @param v1 desired image of u1 by the rotation
+   * @param v2 desired image of u2 by the rotation
+   * @exception IllegalArgumentException if the norm of one of the vectors is zero,
+   * or if one of the pair is degenerated (i.e. the vectors of the pair are collinear)
+   */
+  public Rotation(Cartesian3D u1, Cartesian3D u2, Cartesian3D v1, Cartesian3D v2)
+      throws IllegalArgumentException {
+
+      try {
+          // build orthonormalized base from u1, u2
+          // this fails when vectors are null or collinear, which is forbidden to define a rotation
+          final Cartesian3D u3 = u1.crossProduct(u2).normalize();
+          u2 = u3.crossProduct(u1).normalize();
+          u1 = u1.normalize();
+
+          // build an orthonormalized base from v1, v2
+          // this fails when vectors are null or collinear, which is forbidden to define a rotation
+          final Cartesian3D v3 = v1.crossProduct(v2).normalize();
+          v2 = v3.crossProduct(v1).normalize();
+          v1 = v1.normalize();
+
+          // buid a matrix transforming the first base into the second one
+          final double[][] m = new double[][] {
+              {
+                  LinearCombination.value(u1.getX(), v1.getX(), u2.getX(), v2.getX(), u3.getX(), v3.getX()),
+                  LinearCombination.value(u1.getY(), v1.getX(), u2.getY(), v2.getX(), u3.getY(), v3.getX()),
+                  LinearCombination.value(u1.getZ(), v1.getX(), u2.getZ(), v2.getX(), u3.getZ(), v3.getX())
+              },
+              {
+                  LinearCombination.value(u1.getX(), v1.getY(), u2.getX(), v2.getY(), u3.getX(), v3.getY()),
+                  LinearCombination.value(u1.getY(), v1.getY(), u2.getY(), v2.getY(), u3.getY(), v3.getY()),
+                  LinearCombination.value(u1.getZ(), v1.getY(), u2.getZ(), v2.getY(), u3.getZ(), v3.getY())
+              },
+              {
+                  LinearCombination.value(u1.getX(), v1.getZ(), u2.getX(), v2.getZ(), u3.getX(), v3.getZ()),
+                  LinearCombination.value(u1.getY(), v1.getZ(), u2.getY(), v2.getZ(), u3.getY(), v3.getZ()),
+                  LinearCombination.value(u1.getZ(), v1.getZ(), u2.getZ(), v2.getZ(), u3.getZ(), v3.getZ())
+              }
+          };
+
+          double[] quat = mat2quat(m);
+          q0 = quat[0];
+          q1 = quat[1];
+          q2 = quat[2];
+          q3 = quat[3];
+
+      } catch (IllegalStateException exc) {
+          throw new IllegalArgumentException("Invalid rotation vector pairs", exc);
+      }
+
+  }
+
+  /** Build one of the rotations that transform one vector into another one.
+
+   * <p>Except for a possible scale factor, if the instance were
+   * applied to the vector u it will produce the vector v. There is an
+   * infinite number of such rotations, this constructor choose the
+   * one with the smallest associated angle (i.e. the one whose axis
+   * is orthogonal to the (u, v) plane). If u and v are collinear, an
+   * arbitrary rotation axis is chosen.</p>
+
+   * @param u origin vector
+   * @param v desired image of u by the rotation
+   * @exception IllegalArgumentException if the norm of one of the vectors is zero
+   */
+  public Rotation(Cartesian3D u, Cartesian3D v) throws IllegalArgumentException {
+
+    double normProduct = u.getNorm() * v.getNorm();
+    if (normProduct == 0) {
+        throw new IllegalArgumentException("Zero norm for rotation defining vector");
+    }
+
+    double dot = u.dotProduct(v);
+
+    if (dot < ((2.0e-15 - 1.0) * normProduct)) {
+      // special case u = -v: we select a PI angle rotation around
+      // an arbitrary vector orthogonal to u
+      Cartesian3D w = u.orthogonal();
+      q0 = 0.0;
+      q1 = -w.getX();
+      q2 = -w.getY();
+      q3 = -w.getZ();
+    } else {
+      // general case: (u, v) defines a plane, we select
+      // the shortest possible rotation: axis orthogonal to this plane
+      q0 = Math.sqrt(0.5 * (1.0 + dot / normProduct));
+      double coeff = 1.0 / (2.0 * q0 * normProduct);
+      Cartesian3D q = v.crossProduct(u);
+      q1 = coeff * q.getX();
+      q2 = coeff * q.getY();
+      q3 = coeff * q.getZ();
+    }
+
+  }
+
+  /** Build a rotation from three Cardan or Euler elementary rotations.
+
+   * <p>
+   * Calling this constructor is equivalent to call
+   * {@link #Rotation(RotationOrder, RotationConvention, double, double, double)
+   * new Rotation(order, RotationConvention.VECTOR_OPERATOR, alpha1, alpha2, alpha3)}
+   * </p>
+
+   * @param order order of rotations to use
+   * @param alpha1 angle of the first elementary rotation
+   * @param alpha2 angle of the second elementary rotation
+   * @param alpha3 angle of the third elementary rotation
+   * @deprecated as of 3.6, replaced with {@link
+   * #Rotation(RotationOrder, RotationConvention, double, double, double)}
+   */
+  @Deprecated
+  public Rotation(RotationOrder order,
+                  double alpha1, double alpha2, double alpha3) {
+      this(order, RotationConvention.VECTOR_OPERATOR, alpha1, alpha2, alpha3);
+  }
+
+  /** Build a rotation from three Cardan or Euler elementary rotations.
+
+   * <p>Cardan rotations are three successive rotations around the
+   * canonical axes X, Y and Z, each axis being used once. There are
+   * 6 such sets of rotations (XYZ, XZY, YXZ, YZX, ZXY and ZYX). Euler
+   * rotations are three successive rotations around the canonical
+   * axes X, Y and Z, the first and last rotations being around the
+   * same axis. There are 6 such sets of rotations (XYX, XZX, YXY,
+   * YZY, ZXZ and ZYZ), the most popular one being ZXZ.</p>
+   * <p>Beware that many people routinely use the term Euler angles even
+   * for what really are Cardan angles (this confusion is especially
+   * widespread in the aerospace business where Roll, Pitch and Yaw angles
+   * are often wrongly tagged as Euler angles).</p>
+
+   * @param order order of rotations to compose, from left to right
+   * (i.e. we will use {@code r1.compose(r2.compose(r3, convention), convention)})
+   * @param convention convention to use for the semantics of the angle
+   * @param alpha1 angle of the first elementary rotation
+   * @param alpha2 angle of the second elementary rotation
+   * @param alpha3 angle of the third elementary rotation
+   */
+  public Rotation(RotationOrder order, RotationConvention convention,
+                  double alpha1, double alpha2, double alpha3) {
+      Rotation r1 = new Rotation(order.getA1(), alpha1, convention);
+      Rotation r2 = new Rotation(order.getA2(), alpha2, convention);
+      Rotation r3 = new Rotation(order.getA3(), alpha3, convention);
+      Rotation composed = r1.compose(r2.compose(r3, convention), convention);
+      q0 = composed.q0;
+      q1 = composed.q1;
+      q2 = composed.q2;
+      q3 = composed.q3;
+  }
+
+  /** Convert an orthogonal rotation matrix to a quaternion.
+   * @param ort orthogonal rotation matrix
+   * @return quaternion corresponding to the matrix
+   */
+  private static double[] mat2quat(final double[][] ort) {
+
+      final double[] quat = new double[4];
+
+      // There are different ways to compute the quaternions elements
+      // from the matrix. They all involve computing one element from
+      // the diagonal of the matrix, and computing the three other ones
+      // using a formula involving a division by the first element,
+      // which unfortunately can be zero. Since the norm of the
+      // quaternion is 1, we know at least one element has an absolute
+      // value greater or equal to 0.5, so it is always possible to
+      // select the right formula and avoid division by zero and even
+      // numerical inaccuracy. Checking the elements in turn and using
+      // the first one greater than 0.45 is safe (this leads to a simple
+      // test since qi = 0.45 implies 4 qi^2 - 1 = -0.19)
+      double s = ort[0][0] + ort[1][1] + ort[2][2];
+      if (s > -0.19) {
+          // compute q0 and deduce q1, q2 and q3
+          quat[0] = 0.5 * Math.sqrt(s + 1.0);
+          double inv = 0.25 / quat[0];
+          quat[1] = inv * (ort[1][2] - ort[2][1]);
+          quat[2] = inv * (ort[2][0] - ort[0][2]);
+          quat[3] = inv * (ort[0][1] - ort[1][0]);
+      } else {
+          s = ort[0][0] - ort[1][1] - ort[2][2];
+          if (s > -0.19) {
+              // compute q1 and deduce q0, q2 and q3
+              quat[1] = 0.5 * Math.sqrt(s + 1.0);
+              double inv = 0.25 / quat[1];
+              quat[0] = inv * (ort[1][2] - ort[2][1]);
+              quat[2] = inv * (ort[0][1] + ort[1][0]);
+              quat[3] = inv * (ort[0][2] + ort[2][0]);
+          } else {
+              s = ort[1][1] - ort[0][0] - ort[2][2];
+              if (s > -0.19) {
+                  // compute q2 and deduce q0, q1 and q3
+                  quat[2] = 0.5 * Math.sqrt(s + 1.0);
+                  double inv = 0.25 / quat[2];
+                  quat[0] = inv * (ort[2][0] - ort[0][2]);
+                  quat[1] = inv * (ort[0][1] + ort[1][0]);
+                  quat[3] = inv * (ort[2][1] + ort[1][2]);
+              } else {
+                  // compute q3 and deduce q0, q1 and q2
+                  s = ort[2][2] - ort[0][0] - ort[1][1];
+                  quat[3] = 0.5 * Math.sqrt(s + 1.0);
+                  double inv = 0.25 / quat[3];
+                  quat[0] = inv * (ort[0][1] - ort[1][0]);
+                  quat[1] = inv * (ort[0][2] + ort[2][0]);
+                  quat[2] = inv * (ort[2][1] + ort[1][2]);
+              }
+          }
+      }
+
+      return quat;
+
+  }
+
+  /** Revert a rotation.
+   * Build a rotation which reverse the effect of another
+   * rotation. This means that if r(u) = v, then r.revert(v) = u. The
+   * instance is not changed.
+   * @return a new rotation whose effect is the reverse of the effect
+   * of the instance
+   */
+  public Rotation revert() {
+    return new Rotation(-q0, q1, q2, q3, false);
+  }
+
+  /** Get the scalar coordinate of the quaternion.
+   * @return scalar coordinate of the quaternion
+   */
+  public double getQ0() {
+    return q0;
+  }
+
+  /** Get the first coordinate of the vectorial part of the quaternion.
+   * @return first coordinate of the vectorial part of the quaternion
+   */
+  public double getQ1() {
+    return q1;
+  }
+
+  /** Get the second coordinate of the vectorial part of the quaternion.
+   * @return second coordinate of the vectorial part of the quaternion
+   */
+  public double getQ2() {
+    return q2;
+  }
+
+  /** Get the third coordinate of the vectorial part of the quaternion.
+   * @return third coordinate of the vectorial part of the quaternion
+   */
+  public double getQ3() {
+    return q3;
+  }
+
+  /** Get the normalized axis of the rotation.
+   * <p>
+   * Calling this method is equivalent to call
+   * {@link #getAxis(RotationConvention) getAxis(RotationConvention.VECTOR_OPERATOR)}
+   * </p>
+   * @return normalized axis of the rotation
+   * @see #Rotation(Cartesian3D, double, RotationConvention)
+   * @deprecated as of 3.6, replaced with {@link #getAxis(RotationConvention)}
+   */
+  @Deprecated
+  public Cartesian3D getAxis() {
+    return getAxis(RotationConvention.VECTOR_OPERATOR);
+  }
+
+  /** Get the normalized axis of the rotation.
+   * <p>
+   * Note that as {@link #getAngle()} always returns an angle
+   * between 0 and &pi;, changing the convention changes the
+   * direction of the axis, not the sign of the angle.
+   * </p>
+   * @param convention convention to use for the semantics of the angle
+   * @return normalized axis of the rotation
+   * @see #Rotation(Cartesian3D, double, RotationConvention)
+   */
+  public Cartesian3D getAxis(final RotationConvention convention) {
+    final double squaredSine = q1 * q1 + q2 * q2 + q3 * q3;
+    if (squaredSine == 0) {
+      return convention == RotationConvention.VECTOR_OPERATOR ? Cartesian3D.PLUS_I : Cartesian3D.MINUS_I;
+    } else {
+        final double sgn = convention == RotationConvention.VECTOR_OPERATOR ? +1 : -1;
+        if (q0 < 0) {
+            final double inverse = sgn / Math.sqrt(squaredSine);
+            return new Cartesian3D(q1 * inverse, q2 * inverse, q3 * inverse);
+        }
+        final double inverse = -sgn / Math.sqrt(squaredSine);
+        return new Cartesian3D(q1 * inverse, q2 * inverse, q3 * inverse);
+    }
+  }
+
+  /** Get the angle of the rotation.
+   * @return angle of the rotation (between 0 and &pi;)
+   * @see #Rotation(Cartesian3D, double)
+   */
+  public double getAngle() {
+    if ((q0 < -0.1) || (q0 > 0.1)) {
+      return 2 * Math.asin(Math.sqrt(q1 * q1 + q2 * q2 + q3 * q3));
+    } else if (q0 < 0) {
+      return 2 * Math.acos(-q0);
+    }
+    return 2 * Math.acos(q0);
+  }
+
+  /** Get the Cardan or Euler angles corresponding to the instance.
+
+   * <p>
+   * Calling this method is equivalent to call
+   * {@link #getAngles(RotationOrder, RotationConvention)
+   * getAngles(order, RotationConvention.VECTOR_OPERATOR)}
+   * </p>
+
+   * @param order rotation order to use
+   * @return an array of three angles, in the order specified by the set
+   * @exception IllegalStateException if the rotation is
+   * singular with respect to the angles set specified
+   * @deprecated as of 3.6, replaced with {@link #getAngles(RotationOrder, RotationConvention)}
+   */
+  @Deprecated
+  public double[] getAngles(RotationOrder order)
+      throws IllegalStateException {
+      return getAngles(order, RotationConvention.VECTOR_OPERATOR);
+  }
+
+  /** Get the Cardan or Euler angles corresponding to the instance.
+
+   * <p>The equations show that each rotation can be defined by two
+   * different values of the Cardan or Euler angles set. For example
+   * if Cardan angles are used, the rotation defined by the angles
+   * a<sub>1</sub>, a<sub>2</sub> and a<sub>3</sub> is the same as
+   * the rotation defined by the angles &pi; + a<sub>1</sub>, &pi;
+   * - a<sub>2</sub> and &pi; + a<sub>3</sub>. This method implements
+   * the following arbitrary choices:</p>
+   * <ul>
+   *   <li>for Cardan angles, the chosen set is the one for which the
+   *   second angle is between -&pi;/2 and &pi;/2 (i.e its cosine is
+   *   positive),</li>
+   *   <li>for Euler angles, the chosen set is the one for which the
+   *   second angle is between 0 and &pi; (i.e its sine is positive).</li>
+   * </ul>
+
+   * <p>Cardan and Euler angle have a very disappointing drawback: all
+   * of them have singularities. This means that if the instance is
+   * too close to the singularities corresponding to the given
+   * rotation order, it will be impossible to retrieve the angles. For
+   * Cardan angles, this is often called gimbal lock. There is
+   * <em>nothing</em> to do to prevent this, it is an intrinsic problem
+   * with Cardan and Euler representation (but not a problem with the
+   * rotation itself, which is perfectly well defined). For Cardan
+   * angles, singularities occur when the second angle is close to
+   * -&pi;/2 or +&pi;/2, for Euler angle singularities occur when the
+   * second angle is close to 0 or &pi;, this implies that the identity
+   * rotation is always singular for Euler angles!</p>
+
+   * @param order rotation order to use
+   * @param convention convention to use for the semantics of the angle
+   * @return an array of three angles, in the order specified by the set
+   * @exception IllegalStateException if the rotation is
+   * singular with respect to the angles set specified
+   */
+  public double[] getAngles(RotationOrder order, RotationConvention convention)
+      throws IllegalStateException {
+
+      if (convention == RotationConvention.VECTOR_OPERATOR) {
+          if (order == RotationOrder.XYZ) {
+
+              // r (Cartesian3D.plusK) coordinates are :
+              //  sin (theta), -cos (theta) sin (phi), cos (theta) cos (phi)
+              // (-r) (Cartesian3D.plusI) coordinates are :
+              // cos (psi) cos (theta), -sin (psi) cos (theta), sin (theta)
+              // and we can choose to have theta in the interval [-PI/2 ; +PI/2]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_K);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_I);
+              if  ((v2.getZ() < -0.9999999999) || (v2.getZ() > 0.9999999999)) {
+                  throw new IllegalStateException(CARDAN_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(-(v1.getY()), v1.getZ()),
+                  Math.asin(v2.getZ()),
+                  Math.atan2(-(v2.getY()), v2.getX())
+              };
+
+          } else if (order == RotationOrder.XZY) {
+
+              // r (Cartesian3D.plusJ) coordinates are :
+              // -sin (psi), cos (psi) cos (phi), cos (psi) sin (phi)
+              // (-r) (Cartesian3D.plusI) coordinates are :
+              // cos (theta) cos (psi), -sin (psi), sin (theta) cos (psi)
+              // and we can choose to have psi in the interval [-PI/2 ; +PI/2]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_J);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_I);
+              if ((v2.getY() < -0.9999999999) || (v2.getY() > 0.9999999999)) {
+                  throw new IllegalStateException(CARDAN_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(v1.getZ(), v1.getY()),
+                 -Math.asin(v2.getY()),
+                  Math.atan2(v2.getZ(), v2.getX())
+              };
+
+          } else if (order == RotationOrder.YXZ) {
+
+              // r (Cartesian3D.plusK) coordinates are :
+              //  cos (phi) sin (theta), -sin (phi), cos (phi) cos (theta)
+              // (-r) (Cartesian3D.plusJ) coordinates are :
+              // sin (psi) cos (phi), cos (psi) cos (phi), -sin (phi)
+              // and we can choose to have phi in the interval [-PI/2 ; +PI/2]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_K);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_J);
+              if ((v2.getZ() < -0.9999999999) || (v2.getZ() > 0.9999999999)) {
+                  throw new IllegalStateException(CARDAN_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(v1.getX(), v1.getZ()),
+                 -Math.asin(v2.getZ()),
+                  Math.atan2(v2.getX(), v2.getY())
+              };
+
+          } else if (order == RotationOrder.YZX) {
+
+              // r (Cartesian3D.plusI) coordinates are :
+              // cos (psi) cos (theta), sin (psi), -cos (psi) sin (theta)
+              // (-r) (Cartesian3D.plusJ) coordinates are :
+              // sin (psi), cos (phi) cos (psi), -sin (phi) cos (psi)
+              // and we can choose to have psi in the interval [-PI/2 ; +PI/2]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_I);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_J);
+              if ((v2.getX() < -0.9999999999) || (v2.getX() > 0.9999999999)) {
+                  throw new IllegalStateException(CARDAN_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(-(v1.getZ()), v1.getX()),
+                  Math.asin(v2.getX()),
+                  Math.atan2(-(v2.getZ()), v2.getY())
+              };
+
+          } else if (order == RotationOrder.ZXY) {
+
+              // r (Cartesian3D.plusJ) coordinates are :
+              // -cos (phi) sin (psi), cos (phi) cos (psi), sin (phi)
+              // (-r) (Cartesian3D.plusK) coordinates are :
+              // -sin (theta) cos (phi), sin (phi), cos (theta) cos (phi)
+              // and we can choose to have phi in the interval [-PI/2 ; +PI/2]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_J);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_K);
+              if ((v2.getY() < -0.9999999999) || (v2.getY() > 0.9999999999)) {
+                  throw new IllegalStateException(CARDAN_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(-(v1.getX()), v1.getY()),
+                  Math.asin(v2.getY()),
+                  Math.atan2(-(v2.getX()), v2.getZ())
+              };
+
+          } else if (order == RotationOrder.ZYX) {
+
+              // r (Cartesian3D.plusI) coordinates are :
+              //  cos (theta) cos (psi), cos (theta) sin (psi), -sin (theta)
+              // (-r) (Cartesian3D.plusK) coordinates are :
+              // -sin (theta), sin (phi) cos (theta), cos (phi) cos (theta)
+              // and we can choose to have theta in the interval [-PI/2 ; +PI/2]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_I);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_K);
+              if ((v2.getX() < -0.9999999999) || (v2.getX() > 0.9999999999)) {
+                  throw new IllegalStateException(CARDAN_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(v1.getY(), v1.getX()),
+                 -Math.asin(v2.getX()),
+                  Math.atan2(v2.getY(), v2.getZ())
+              };
+
+          } else if (order == RotationOrder.XYX) {
+
+              // r (Cartesian3D.plusI) coordinates are :
+              //  cos (theta), sin (phi1) sin (theta), -cos (phi1) sin (theta)
+              // (-r) (Cartesian3D.plusI) coordinates are :
+              // cos (theta), sin (theta) sin (phi2), sin (theta) cos (phi2)
+              // and we can choose to have theta in the interval [0 ; PI]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_I);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_I);
+              if ((v2.getX() < -0.9999999999) || (v2.getX() > 0.9999999999)) {
+                  throw new IllegalStateException(EULER_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(v1.getY(), -v1.getZ()),
+                  Math.acos(v2.getX()),
+                  Math.atan2(v2.getY(), v2.getZ())
+              };
+
+          } else if (order == RotationOrder.XZX) {
+
+              // r (Cartesian3D.plusI) coordinates are :
+              //  cos (psi), cos (phi1) sin (psi), sin (phi1) sin (psi)
+              // (-r) (Cartesian3D.plusI) coordinates are :
+              // cos (psi), -sin (psi) cos (phi2), sin (psi) sin (phi2)
+              // and we can choose to have psi in the interval [0 ; PI]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_I);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_I);
+              if ((v2.getX() < -0.9999999999) || (v2.getX() > 0.9999999999)) {
+                  throw new IllegalStateException(EULER_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(v1.getZ(), v1.getY()),
+                  Math.acos(v2.getX()),
+                  Math.atan2(v2.getZ(), -v2.getY())
+              };
+
+          } else if (order == RotationOrder.YXY) {
+
+              // r (Cartesian3D.plusJ) coordinates are :
+              //  sin (theta1) sin (phi), cos (phi), cos (theta1) sin (phi)
+              // (-r) (Cartesian3D.plusJ) coordinates are :
+              // sin (phi) sin (theta2), cos (phi), -sin (phi) cos (theta2)
+              // and we can choose to have phi in the interval [0 ; PI]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_J);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_J);
+              if ((v2.getY() < -0.9999999999) || (v2.getY() > 0.9999999999)) {
+                  throw new IllegalStateException(EULER_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(v1.getX(), v1.getZ()),
+                  Math.acos(v2.getY()),
+                  Math.atan2(v2.getX(), -v2.getZ())
+              };
+
+          } else if (order == RotationOrder.YZY) {
+
+              // r (Cartesian3D.plusJ) coordinates are :
+              //  -cos (theta1) sin (psi), cos (psi), sin (theta1) sin (psi)
+              // (-r) (Cartesian3D.plusJ) coordinates are :
+              // sin (psi) cos (theta2), cos (psi), sin (psi) sin (theta2)
+              // and we can choose to have psi in the interval [0 ; PI]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_J);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_J);
+              if ((v2.getY() < -0.9999999999) || (v2.getY() > 0.9999999999)) {
+                  throw new IllegalStateException(EULER_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(v1.getZ(), -v1.getX()),
+                  Math.acos(v2.getY()),
+                  Math.atan2(v2.getZ(), v2.getX())
+              };
+
+          } else if (order == RotationOrder.ZXZ) {
+
+              // r (Cartesian3D.plusK) coordinates are :
+              //  sin (psi1) sin (phi), -cos (psi1) sin (phi), cos (phi)
+              // (-r) (Cartesian3D.plusK) coordinates are :
+              // sin (phi) sin (psi2), sin (phi) cos (psi2), cos (phi)
+              // and we can choose to have phi in the interval [0 ; PI]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_K);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_K);
+              if ((v2.getZ() < -0.9999999999) || (v2.getZ() > 0.9999999999)) {
+                  throw new IllegalStateException(EULER_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(v1.getX(), -v1.getY()),
+                  Math.acos(v2.getZ()),
+                  Math.atan2(v2.getX(), v2.getY())
+              };
+
+          } else { // last possibility is ZYZ
+
+              // r (Cartesian3D.plusK) coordinates are :
+              //  cos (psi1) sin (theta), sin (psi1) sin (theta), cos (theta)
+              // (-r) (Cartesian3D.plusK) coordinates are :
+              // -sin (theta) cos (psi2), sin (theta) sin (psi2), cos (theta)
+              // and we can choose to have theta in the interval [0 ; PI]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_K);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_K);
+              if ((v2.getZ() < -0.9999999999) || (v2.getZ() > 0.9999999999)) {
+                  throw new IllegalStateException(EULER_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(v1.getY(), v1.getX()),
+                  Math.acos(v2.getZ()),
+                  Math.atan2(v2.getY(), -v2.getX())
+              };
+
+          }
+      } else {
+          if (order == RotationOrder.XYZ) {
+
+              // r (Cartesian3D.plusI) coordinates are :
+              //  cos (theta) cos (psi), -cos (theta) sin (psi), sin (theta)
+              // (-r) (Cartesian3D.plusK) coordinates are :
+              // sin (theta), -sin (phi) cos (theta), cos (phi) cos (theta)
+              // and we can choose to have theta in the interval [-PI/2 ; +PI/2]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_I);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_K);
+              if ((v2.getX() < -0.9999999999) || (v2.getX() > 0.9999999999)) {
+                  throw new IllegalStateException(CARDAN_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(-v2.getY(), v2.getZ()),
+                  Math.asin(v2.getX()),
+                  Math.atan2(-v1.getY(), v1.getX())
+              };
+
+          } else if (order == RotationOrder.XZY) {
+
+              // r (Cartesian3D.plusI) coordinates are :
+              // cos (psi) cos (theta), -sin (psi), cos (psi) sin (theta)
+              // (-r) (Cartesian3D.plusJ) coordinates are :
+              // -sin (psi), cos (phi) cos (psi), sin (phi) cos (psi)
+              // and we can choose to have psi in the interval [-PI/2 ; +PI/2]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_I);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_J);
+              if ((v2.getX() < -0.9999999999) || (v2.getX() > 0.9999999999)) {
+                  throw new IllegalStateException(CARDAN_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(v2.getZ(), v2.getY()),
+                 -Math.asin(v2.getX()),
+                  Math.atan2(v1.getZ(), v1.getX())
+              };
+
+          } else if (order == RotationOrder.YXZ) {
+
+              // r (Cartesian3D.plusJ) coordinates are :
+              // cos (phi) sin (psi), cos (phi) cos (psi), -sin (phi)
+              // (-r) (Cartesian3D.plusK) coordinates are :
+              // sin (theta) cos (phi), -sin (phi), cos (theta) cos (phi)
+              // and we can choose to have phi in the interval [-PI/2 ; +PI/2]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_J);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_K);
+              if ((v2.getY() < -0.9999999999) || (v2.getY() > 0.9999999999)) {
+                  throw new IllegalStateException(CARDAN_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(v2.getX(), v2.getZ()),
+                 -Math.asin(v2.getY()),
+                  Math.atan2(v1.getX(), v1.getY())
+              };
+
+          } else if (order == RotationOrder.YZX) {
+
+              // r (Cartesian3D.plusJ) coordinates are :
+              // sin (psi), cos (psi) cos (phi), -cos (psi) sin (phi)
+              // (-r) (Cartesian3D.plusI) coordinates are :
+              // cos (theta) cos (psi), sin (psi), -sin (theta) cos (psi)
+              // and we can choose to have psi in the interval [-PI/2 ; +PI/2]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_J);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_I);
+              if ((v2.getY() < -0.9999999999) || (v2.getY() > 0.9999999999)) {
+                  throw new IllegalStateException(CARDAN_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(-v2.getZ(), v2.getX()),
+                  Math.asin(v2.getY()),
+                  Math.atan2(-v1.getZ(), v1.getY())
+              };
+
+          } else if (order == RotationOrder.ZXY) {
+
+              // r (Cartesian3D.plusK) coordinates are :
+              //  -cos (phi) sin (theta), sin (phi), cos (phi) cos (theta)
+              // (-r) (Cartesian3D.plusJ) coordinates are :
+              // -sin (psi) cos (phi), cos (psi) cos (phi), sin (phi)
+              // and we can choose to have phi in the interval [-PI/2 ; +PI/2]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_K);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_J);
+              if ((v2.getZ() < -0.9999999999) || (v2.getZ() > 0.9999999999)) {
+                  throw new IllegalStateException(CARDAN_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(-v2.getX(), v2.getY()),
+                  Math.asin(v2.getZ()),
+                  Math.atan2(-v1.getX(), v1.getZ())
+              };
+
+          } else if (order == RotationOrder.ZYX) {
+
+              // r (Cartesian3D.plusK) coordinates are :
+              //  -sin (theta), cos (theta) sin (phi), cos (theta) cos (phi)
+              // (-r) (Cartesian3D.plusI) coordinates are :
+              // cos (psi) cos (theta), sin (psi) cos (theta), -sin (theta)
+              // and we can choose to have theta in the interval [-PI/2 ; +PI/2]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_K);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_I);
+              if  ((v2.getZ() < -0.9999999999) || (v2.getZ() > 0.9999999999)) {
+                  throw new IllegalStateException(CARDAN_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(v2.getY(), v2.getX()),
+                 -Math.asin(v2.getZ()),
+                  Math.atan2(v1.getY(), v1.getZ())
+              };
+
+          } else if (order == RotationOrder.XYX) {
+
+              // r (Cartesian3D.plusI) coordinates are :
+              //  cos (theta), sin (phi2) sin (theta), cos (phi2) sin (theta)
+              // (-r) (Cartesian3D.plusI) coordinates are :
+              // cos (theta), sin (theta) sin (phi1), -sin (theta) cos (phi1)
+              // and we can choose to have theta in the interval [0 ; PI]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_I);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_I);
+              if ((v2.getX() < -0.9999999999) || (v2.getX() > 0.9999999999)) {
+                  throw new IllegalStateException(EULER_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(v2.getY(), -v2.getZ()),
+                  Math.acos(v2.getX()),
+                  Math.atan2(v1.getY(), v1.getZ())
+              };
+
+          } else if (order == RotationOrder.XZX) {
+
+              // r (Cartesian3D.plusI) coordinates are :
+              //  cos (psi), -cos (phi2) sin (psi), sin (phi2) sin (psi)
+              // (-r) (Cartesian3D.plusI) coordinates are :
+              // cos (psi), sin (psi) cos (phi1), sin (psi) sin (phi1)
+              // and we can choose to have psi in the interval [0 ; PI]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_I);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_I);
+              if ((v2.getX() < -0.9999999999) || (v2.getX() > 0.9999999999)) {
+                  throw new IllegalStateException(EULER_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(v2.getZ(), v2.getY()),
+                  Math.acos(v2.getX()),
+                  Math.atan2(v1.getZ(), -v1.getY())
+              };
+
+          } else if (order == RotationOrder.YXY) {
+
+              // r (Cartesian3D.plusJ) coordinates are :
+              // sin (phi) sin (theta2), cos (phi), -sin (phi) cos (theta2)
+              // (-r) (Cartesian3D.plusJ) coordinates are :
+              //  sin (theta1) sin (phi), cos (phi), cos (theta1) sin (phi)
+              // and we can choose to have phi in the interval [0 ; PI]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_J);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_J);
+              if ((v2.getY() < -0.9999999999) || (v2.getY() > 0.9999999999)) {
+                  throw new IllegalStateException(EULER_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(v2.getX(), v2.getZ()),
+                  Math.acos(v2.getY()),
+                  Math.atan2(v1.getX(), -v1.getZ())
+              };
+
+          } else if (order == RotationOrder.YZY) {
+
+              // r (Cartesian3D.plusJ) coordinates are :
+              // sin (psi) cos (theta2), cos (psi), sin (psi) sin (theta2)
+              // (-r) (Cartesian3D.plusJ) coordinates are :
+              //  -cos (theta1) sin (psi), cos (psi), sin (theta1) sin (psi)
+              // and we can choose to have psi in the interval [0 ; PI]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_J);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_J);
+              if ((v2.getY() < -0.9999999999) || (v2.getY() > 0.9999999999)) {
+                  throw new IllegalStateException(EULER_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(v2.getZ(), -v2.getX()),
+                  Math.acos(v2.getY()),
+                  Math.atan2(v1.getZ(), v1.getX())
+              };
+
+          } else if (order == RotationOrder.ZXZ) {
+
+              // r (Cartesian3D.plusK) coordinates are :
+              // sin (phi) sin (psi2), sin (phi) cos (psi2), cos (phi)
+              // (-r) (Cartesian3D.plusK) coordinates are :
+              //  sin (psi1) sin (phi), -cos (psi1) sin (phi), cos (phi)
+              // and we can choose to have phi in the interval [0 ; PI]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_K);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_K);
+              if ((v2.getZ() < -0.9999999999) || (v2.getZ() > 0.9999999999)) {
+                  throw new IllegalStateException(EULER_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(v2.getX(), -v2.getY()),
+                  Math.acos(v2.getZ()),
+                  Math.atan2(v1.getX(), v1.getY())
+              };
+
+          } else { // last possibility is ZYZ
+
+              // r (Cartesian3D.plusK) coordinates are :
+              // -sin (theta) cos (psi2), sin (theta) sin (psi2), cos (theta)
+              // (-r) (Cartesian3D.plusK) coordinates are :
+              //  cos (psi1) sin (theta), sin (psi1) sin (theta), cos (theta)
+              // and we can choose to have theta in the interval [0 ; PI]
+              Cartesian3D v1 = applyTo(Cartesian3D.PLUS_K);
+              Cartesian3D v2 = applyInverseTo(Cartesian3D.PLUS_K);
+              if ((v2.getZ() < -0.9999999999) || (v2.getZ() > 0.9999999999)) {
+                  throw new IllegalStateException(EULER_SINGULARITY_MSG);
+              }
+              return new double[] {
+                  Math.atan2(v2.getY(), v2.getX()),
+                  Math.acos(v2.getZ()),
+                  Math.atan2(v1.getY(), -v1.getX())
+              };
+
+          }
+      }
+
+  }
+
+  /** Get the 3X3 matrix corresponding to the instance
+   * @return the matrix corresponding to the instance
+   */
+  public double[][] getMatrix() {
+
+    // products
+    double q0q0  = q0 * q0;
+    double q0q1  = q0 * q1;
+    double q0q2  = q0 * q2;
+    double q0q3  = q0 * q3;
+    double q1q1  = q1 * q1;
+    double q1q2  = q1 * q2;
+    double q1q3  = q1 * q3;
+    double q2q2  = q2 * q2;
+    double q2q3  = q2 * q3;
+    double q3q3  = q3 * q3;
+
+    // create the matrix
+    double[][] m = new double[3][];
+    m[0] = new double[3];
+    m[1] = new double[3];
+    m[2] = new double[3];
+
+    m [0][0] = 2.0 * (q0q0 + q1q1) - 1.0;
+    m [1][0] = 2.0 * (q1q2 - q0q3);
+    m [2][0] = 2.0 * (q1q3 + q0q2);
+
+    m [0][1] = 2.0 * (q1q2 + q0q3);
+    m [1][1] = 2.0 * (q0q0 + q2q2) - 1.0;
+    m [2][1] = 2.0 * (q2q3 - q0q1);
+
+    m [0][2] = 2.0 * (q1q3 - q0q2);
+    m [1][2] = 2.0 * (q2q3 + q0q1);
+    m [2][2] = 2.0 * (q0q0 + q3q3) - 1.0;
+
+    return m;
+
+  }
+
+  /** Apply the rotation to a vector.
+   * @param u vector to apply the rotation to
+   * @return a new vector which is the image of u by the rotation
+   */
+  public Cartesian3D applyTo(Cartesian3D u) {
+
+    double x = u.getX();
+    double y = u.getY();
+    double z = u.getZ();
+
+    double s = q1 * x + q2 * y + q3 * z;
+
+    return new Cartesian3D(2 * (q0 * (x * q0 - (q2 * z - q3 * y)) + s * q1) - x,
+                        2 * (q0 * (y * q0 - (q3 * x - q1 * z)) + s * q2) - y,
+                        2 * (q0 * (z * q0 - (q1 * y - q2 * x)) + s * q3) - z);
+
+  }
+
+  /** Apply the rotation to a vector stored in an array.
+   * @param in an array with three items which stores vector to rotate
+   * @param out an array with three items to put result to (it can be the same
+   * array as in)
+   */
+  public void applyTo(final double[] in, final double[] out) {
+
+      final double x = in[0];
+      final double y = in[1];
+      final double z = in[2];
+
+      final double s = q1 * x + q2 * y + q3 * z;
+
+      out[0] = 2 * (q0 * (x * q0 - (q2 * z - q3 * y)) + s * q1) - x;
+      out[1] = 2 * (q0 * (y * q0 - (q3 * x - q1 * z)) + s * q2) - y;
+      out[2] = 2 * (q0 * (z * q0 - (q1 * y - q2 * x)) + s * q3) - z;
+
+  }
+
+  /** Apply the inverse of the rotation to a vector.
+   * @param u vector to apply the inverse of the rotation to
+   * @return a new vector which such that u is its image by the rotation
+   */
+  public Cartesian3D applyInverseTo(Cartesian3D u) {
+
+    double x = u.getX();
+    double y = u.getY();
+    double z = u.getZ();
+
+    double s = q1 * x + q2 * y + q3 * z;
+    double m0 = -q0;
+
+    return new Cartesian3D(2 * (m0 * (x * m0 - (q2 * z - q3 * y)) + s * q1) - x,
+                        2 * (m0 * (y * m0 - (q3 * x - q1 * z)) + s * q2) - y,
+                        2 * (m0 * (z * m0 - (q1 * y - q2 * x)) + s * q3) - z);
+
+  }
+
+  /** Apply the inverse of the rotation to a vector stored in an array.
+   * @param in an array with three items which stores vector to rotate
+   * @param out an array with three items to put result to (it can be the same
+   * array as in)
+   */
+  public void applyInverseTo(final double[] in, final double[] out) {
+
+      final double x = in[0];
+      final double y = in[1];
+      final double z = in[2];
+
+      final double s = q1 * x + q2 * y + q3 * z;
+      final double m0 = -q0;
+
+      out[0] = 2 * (m0 * (x * m0 - (q2 * z - q3 * y)) + s * q1) - x;
+      out[1] = 2 * (m0 * (y * m0 - (q3 * x - q1 * z)) + s * q2) - y;
+      out[2] = 2 * (m0 * (z * m0 - (q1 * y - q2 * x)) + s * q3) - z;
+
+  }
+
+  /** Apply the instance to another rotation.
+   * <p>
+   * Calling this method is equivalent to call
+   * {@link #compose(Rotation, RotationConvention)
+   * compose(r, RotationConvention.VECTOR_OPERATOR)}.
+   * </p>
+   * @param r rotation to apply the rotation to
+   * @return a new rotation which is the composition of r by the instance
+   */
+  public Rotation applyTo(Rotation r) {
+    return compose(r, RotationConvention.VECTOR_OPERATOR);
+  }
+
+  /** Compose the instance with another rotation.
+   * <p>
+   * If the semantics of the rotations composition corresponds to a
+   * {@link RotationConvention#VECTOR_OPERATOR vector operator} convention,
+   * applying the instance to a rotation is computing the composition
+   * in an order compliant with the following rule : let {@code u} be any
+   * vector and {@code v} its image by {@code r1} (i.e.
+   * {@code r1.applyTo(u) = v}). Let {@code w} be the image of {@code v} by
+   * rotation {@code r2} (i.e. {@code r2.applyTo(v) = w}). Then
+   * {@code w = comp.applyTo(u)}, where
+   * {@code comp = r2.compose(r1, RotationConvention.VECTOR_OPERATOR)}.
+   * </p>
+   * <p>
+   * If the semantics of the rotations composition corresponds to a
+   * {@link RotationConvention#FRAME_TRANSFORM frame transform} convention,
+   * the application order will be reversed. So keeping the exact same
+   * meaning of all {@code r1}, {@code r2}, {@code u}, {@code v}, {@code w}
+   * and  {@code comp} as above, {@code comp} could also be computed as
+   * {@code comp = r1.compose(r2, RotationConvention.FRAME_TRANSFORM)}.
+   * </p>
+   * @param r rotation to apply the rotation to
+   * @param convention convention to use for the semantics of the angle
+   * @return a new rotation which is the composition of r by the instance
+   */
+  public Rotation compose(final Rotation r, final RotationConvention convention) {
+    return convention == RotationConvention.VECTOR_OPERATOR ?
+           composeInternal(r) : r.composeInternal(this);
+  }
+
+  /** Compose the instance with another rotation using vector operator convention.
+   * @param r rotation to apply the rotation to
+   * @return a new rotation which is the composition of r by the instance
+   * using vector operator convention
+   */
+  private Rotation composeInternal(final Rotation r) {
+    return new Rotation(r.q0 * q0 - (r.q1 * q1 + r.q2 * q2 + r.q3 * q3),
+                        r.q1 * q0 + r.q0 * q1 + (r.q2 * q3 - r.q3 * q2),
+                        r.q2 * q0 + r.q0 * q2 + (r.q3 * q1 - r.q1 * q3),
+                        r.q3 * q0 + r.q0 * q3 + (r.q1 * q2 - r.q2 * q1),
+                        false);
+  }
+
+  /** Apply the inverse of the instance to another rotation.
+   * <p>
+   * Calling this method is equivalent to call
+   * {@link #composeInverse(Rotation, RotationConvention)
+   * composeInverse(r, RotationConvention.VECTOR_OPERATOR)}.
+   * </p>
+   * @param r rotation to apply the rotation to
+   * @return a new rotation which is the composition of r by the inverse
+   * of the instance
+   */
+  public Rotation applyInverseTo(Rotation r) {
+    return composeInverse(r, RotationConvention.VECTOR_OPERATOR);
+  }
+
+  /** Compose the inverse of the instance with another rotation.
+   * <p>
+   * If the semantics of the rotations composition corresponds to a
+   * {@link RotationConvention#VECTOR_OPERATOR vector operator} convention,
+   * applying the inverse of the instance to a rotation is computing
+   * the composition in an order compliant with the following rule :
+   * let {@code u} be any vector and {@code v} its image by {@code r1}
+   * (i.e. {@code r1.applyTo(u) = v}). Let {@code w} be the inverse image
+   * of {@code v} by {@code r2} (i.e. {@code r2.applyInverseTo(v) = w}).
+   * Then {@code w = comp.applyTo(u)}, where
+   * {@code comp = r2.composeInverse(r1)}.
+   * </p>
+   * <p>
+   * If the semantics of the rotations composition corresponds to a
+   * {@link RotationConvention#FRAME_TRANSFORM frame transform} convention,
+   * the application order will be reversed, which means it is the
+   * <em>innermost</em> rotation that will be reversed. So keeping the exact same
+   * meaning of all {@code r1}, {@code r2}, {@code u}, {@code v}, {@code w}
+   * and  {@code comp} as above, {@code comp} could also be computed as
+   * {@code comp = r1.revert().composeInverse(r2.revert(), RotationConvention.FRAME_TRANSFORM)}.
+   * </p>
+   * @param r rotation to apply the rotation to
+   * @param convention convention to use for the semantics of the angle
+   * @return a new rotation which is the composition of r by the inverse
+   * of the instance
+   */
+  public Rotation composeInverse(final Rotation r, final RotationConvention convention) {
+    return convention == RotationConvention.VECTOR_OPERATOR ?
+           composeInverseInternal(r) : r.composeInternal(revert());
+  }
+
+  /** Compose the inverse of the instance with another rotation
+   * using vector operator convention.
+   * @param r rotation to apply the rotation to
+   * @return a new rotation which is the composition of r by the inverse
+   * of the instance using vector operator convention
+   */
+  private Rotation composeInverseInternal(Rotation r) {
+    return new Rotation(-r.q0 * q0 - (r.q1 * q1 + r.q2 * q2 + r.q3 * q3),
+                        -r.q1 * q0 + r.q0 * q1 + (r.q2 * q3 - r.q3 * q2),
+                        -r.q2 * q0 + r.q0 * q2 + (r.q3 * q1 - r.q1 * q3),
+                        -r.q3 * q0 + r.q0 * q3 + (r.q1 * q2 - r.q2 * q1),
+                        false);
+  }
+
+  /** Perfect orthogonality on a 3X3 matrix.
+   * @param m initial matrix (not exactly orthogonal)
+   * @param threshold convergence threshold for the iterative
+   * orthogonality correction (convergence is reached when the
+   * difference between two steps of the Frobenius norm of the
+   * correction is below this threshold)
+   * @return an orthogonal matrix close to m
+   * @exception IllegalArgumentException if the matrix cannot be
+   * orthogonalized with the given threshold after 10 iterations
+   */
+  private double[][] orthogonalizeMatrix(double[][] m, double threshold)
+    throws IllegalArgumentException {
+    double[] m0 = m[0];
+    double[] m1 = m[1];
+    double[] m2 = m[2];
+    double x00 = m0[0];
+    double x01 = m0[1];
+    double x02 = m0[2];
+    double x10 = m1[0];
+    double x11 = m1[1];
+    double x12 = m1[2];
+    double x20 = m2[0];
+    double x21 = m2[1];
+    double x22 = m2[2];
+    double fn = 0;
+    double fn1;
+
+    double[][] o = new double[3][3];
+    double[] o0 = o[0];
+    double[] o1 = o[1];
+    double[] o2 = o[2];
+
+    // iterative correction: Xn+1 = Xn - 0.5 * (Xn.Mt.Xn - M)
+    int i = 0;
+    while (++i < 11) {
+
+      // Mt.Xn
+      double mx00 = m0[0] * x00 + m1[0] * x10 + m2[0] * x20;
+      double mx10 = m0[1] * x00 + m1[1] * x10 + m2[1] * x20;
+      double mx20 = m0[2] * x00 + m1[2] * x10 + m2[2] * x20;
+      double mx01 = m0[0] * x01 + m1[0] * x11 + m2[0] * x21;
+      double mx11 = m0[1] * x01 + m1[1] * x11 + m2[1] * x21;
+      double mx21 = m0[2] * x01 + m1[2] * x11 + m2[2] * x21;
+      double mx02 = m0[0] * x02 + m1[0] * x12 + m2[0] * x22;
+      double mx12 = m0[1] * x02 + m1[1] * x12 + m2[1] * x22;
+      double mx22 = m0[2] * x02 + m1[2] * x12 + m2[2] * x22;
+
+      // Xn+1
+      o0[0] = x00 - 0.5 * (x00 * mx00 + x01 * mx10 + x02 * mx20 - m0[0]);
+      o0[1] = x01 - 0.5 * (x00 * mx01 + x01 * mx11 + x02 * mx21 - m0[1]);
+      o0[2] = x02 - 0.5 * (x00 * mx02 + x01 * mx12 + x02 * mx22 - m0[2]);
+      o1[0] = x10 - 0.5 * (x10 * mx00 + x11 * mx10 + x12 * mx20 - m1[0]);
+      o1[1] = x11 - 0.5 * (x10 * mx01 + x11 * mx11 + x12 * mx21 - m1[1]);
+      o1[2] = x12 - 0.5 * (x10 * mx02 + x11 * mx12 + x12 * mx22 - m1[2]);
+      o2[0] = x20 - 0.5 * (x20 * mx00 + x21 * mx10 + x22 * mx20 - m2[0]);
+      o2[1] = x21 - 0.5 * (x20 * mx01 + x21 * mx11 + x22 * mx21 - m2[1]);
+      o2[2] = x22 - 0.5 * (x20 * mx02 + x21 * mx12 + x22 * mx22 - m2[2]);
+
+      // correction on each elements
+      double corr00 = o0[0] - m0[0];
+      double corr01 = o0[1] - m0[1];
+      double corr02 = o0[2] - m0[2];
+      double corr10 = o1[0] - m1[0];
+      double corr11 = o1[1] - m1[1];
+      double corr12 = o1[2] - m1[2];
+      double corr20 = o2[0] - m2[0];
+      double corr21 = o2[1] - m2[1];
+      double corr22 = o2[2] - m2[2];
+
+      // Frobenius norm of the correction
+      fn1 = corr00 * corr00 + corr01 * corr01 + corr02 * corr02 +
+            corr10 * corr10 + corr11 * corr11 + corr12 * corr12 +
+            corr20 * corr20 + corr21 * corr21 + corr22 * corr22;
+
+      // convergence test
+      if (Math.abs(fn1 - fn) <= threshold) {
+          return o;
+      }
+
+      // prepare next iteration
+      x00 = o0[0];
+      x01 = o0[1];
+      x02 = o0[2];
+      x10 = o1[0];
+      x11 = o1[1];
+      x12 = o1[2];
+      x20 = o2[0];
+      x21 = o2[1];
+      x22 = o2[2];
+      fn  = fn1;
+
+    }
+
+    // the algorithm did not converge after 10 iterations
+    throw new IllegalArgumentException("Unable to orthogonalize matrix in " + (i - 1) + " iterations");
+  }
+
+  /** Compute the <i>distance</i> between two rotations.
+   * <p>The <i>distance</i> is intended here as a way to check if two
+   * rotations are almost similar (i.e. they transform vectors the same way)
+   * or very different. It is mathematically defined as the angle of
+   * the rotation r that prepended to one of the rotations gives the other
+   * one:</p>
+   * <div style="white-space: pre"><code>
+   *        r<sub>1</sub>(r) = r<sub>2</sub>
+   * </code></div>
+   * <p>This distance is an angle between 0 and &pi;. Its value is the smallest
+   * possible upper bound of the angle in radians between r<sub>1</sub>(v)
+   * and r<sub>2</sub>(v) for all possible vectors v. This upper bound is
+   * reached for some v. The distance is equal to 0 if and only if the two
+   * rotations are identical.</p>
+   * <p>Comparing two rotations should always be done using this value rather
+   * than for example comparing the components of the quaternions. It is much
+   * more stable, and has a geometric meaning. Also comparing quaternions
+   * components is error prone since for example quaternions (0.36, 0.48, -0.48, -0.64)
+   * and (-0.36, -0.48, 0.48, 0.64) represent exactly the same rotation despite
+   * their components are different (they are exact opposites).</p>
+   * @param r1 first rotation
+   * @param r2 second rotation
+   * @return <i>distance</i> between r1 and r2
+   */
+  public static double distance(Rotation r1, Rotation r2) {
+      return r1.composeInverseInternal(r2).getAngle();
+  }
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RotationConvention.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RotationConvention.java
new file mode 100644
index 0000000..53766ce
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RotationConvention.java
@@ -0,0 +1,78 @@
+/*
+ * 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.geometry.euclidean.threed;
+
+/**
+ * This enumerates is used to differentiate the semantics of a rotation.
+ * @see Rotation
+ */
+public enum RotationConvention {
+
+    /** Constant for rotation that have the semantics of a vector operator.
+     * <p>
+     * According to this convention, the rotation moves vectors with respect
+     * to a fixed reference frame.
+     * </p>
+     * <p>
+     * This means that if we define rotation r is a 90 degrees rotation around
+     * the Z axis, the image of vector {@link Cartesian3D#PLUS_I} would be
+     * {@link Cartesian3D#PLUS_J}, the image of vector {@link Cartesian3D#PLUS_J}
+     * would be {@link Cartesian3D#MINUS_I}, the image of vector {@link Cartesian3D#PLUS_K}
+     * would be {@link Cartesian3D#PLUS_K}, and the image of vector with coordinates (1, 2, 3)
+     * would be vector (-2, 1, 3). This means that the vector rotates counterclockwise.
+     * </p>
+     * <p>
+     * This convention was the only one supported by Apache Commons Math up to version 3.5.
+     * </p>
+     * <p>
+     * The difference with {@link #FRAME_TRANSFORM} is only the semantics of the sign
+     * of the angle. It is always possible to create or use a rotation using either
+     * convention to really represent a rotation that would have been best created or
+     * used with the other convention, by changing accordingly the sign of the
+     * rotation angle. This is how things were done up to version 3.5.
+     * </p>
+     */
+    VECTOR_OPERATOR,
+
+    /** Constant for rotation that have the semantics of a frame conversion.
+     * <p>
+     * According to this convention, the rotation considered vectors to be fixed,
+     * but their coordinates change as they are converted from an initial frame to
+     * a destination frame rotated with respect to the initial frame.
+     * </p>
+     * <p>
+     * This means that if we define rotation r is a 90 degrees rotation around
+     * the Z axis, the image of vector {@link Cartesian3D#PLUS_I} would be
+     * {@link Cartesian3D#MINUS_J}, the image of vector {@link Cartesian3D#PLUS_J}
+     * would be {@link Cartesian3D#PLUS_I}, the image of vector {@link Cartesian3D#PLUS_K}
+     * would be {@link Cartesian3D#PLUS_K}, and the image of vector with coordinates (1, 2, 3)
+     * would be vector (2, -1, 3). This means that the coordinates of the vector rotates
+     * clockwise, because they are expressed with respect to a destination frame that is rotated
+     * counterclockwise.
+     * </p>
+     * <p>
+     * The difference with {@link #VECTOR_OPERATOR} is only the semantics of the sign
+     * of the angle. It is always possible to create or use a rotation using either
+     * convention to really represent a rotation that would have been best created or
+     * used with the other convention, by changing accordingly the sign of the
+     * rotation angle. This is how things were done up to version 3.5.
+     * </p>
+     */
+    FRAME_TRANSFORM;
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RotationOrder.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RotationOrder.java
new file mode 100644
index 0000000..7e0a042
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/RotationOrder.java
@@ -0,0 +1,172 @@
+/*
+ * 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.geometry.euclidean.threed;
+
+/**
+ * This class is a utility representing a rotation order specification
+ * for Cardan or Euler angles specification.
+ *
+ * This class cannot be instanciated by the user. He can only use one
+ * of the twelve predefined supported orders as an argument to either
+ * the {@link Rotation#Rotation(RotationOrder,double,double,double)}
+ * constructor or the {@link Rotation#getAngles} method..2
+ */
+public final class RotationOrder {
+
+    /** Set of Cardan angles.
+     * this ordered set of rotations is around X, then around Y, then
+     * around Z
+     */
+    public static final RotationOrder XYZ =
+      new RotationOrder("XYZ", Cartesian3D.PLUS_I, Cartesian3D.PLUS_J, Cartesian3D.PLUS_K);
+
+    /** Set of Cardan angles.
+     * this ordered set of rotations is around X, then around Z, then
+     * around Y
+     */
+    public static final RotationOrder XZY =
+      new RotationOrder("XZY", Cartesian3D.PLUS_I, Cartesian3D.PLUS_K, Cartesian3D.PLUS_J);
+
+    /** Set of Cardan angles.
+     * this ordered set of rotations is around Y, then around X, then
+     * around Z
+     */
+    public static final RotationOrder YXZ =
+      new RotationOrder("YXZ", Cartesian3D.PLUS_J, Cartesian3D.PLUS_I, Cartesian3D.PLUS_K);
+
+    /** Set of Cardan angles.
+     * this ordered set of rotations is around Y, then around Z, then
+     * around X
+     */
+    public static final RotationOrder YZX =
+      new RotationOrder("YZX", Cartesian3D.PLUS_J, Cartesian3D.PLUS_K, Cartesian3D.PLUS_I);
+
+    /** Set of Cardan angles.
+     * this ordered set of rotations is around Z, then around X, then
+     * around Y
+     */
+    public static final RotationOrder ZXY =
+      new RotationOrder("ZXY", Cartesian3D.PLUS_K, Cartesian3D.PLUS_I, Cartesian3D.PLUS_J);
+
+    /** Set of Cardan angles.
+     * this ordered set of rotations is around Z, then around Y, then
+     * around X
+     */
+    public static final RotationOrder ZYX =
+      new RotationOrder("ZYX", Cartesian3D.PLUS_K, Cartesian3D.PLUS_J, Cartesian3D.PLUS_I);
+
+    /** Set of Euler angles.
+     * this ordered set of rotations is around X, then around Y, then
+     * around X
+     */
+    public static final RotationOrder XYX =
+      new RotationOrder("XYX", Cartesian3D.PLUS_I, Cartesian3D.PLUS_J, Cartesian3D.PLUS_I);
+
+    /** Set of Euler angles.
+     * this ordered set of rotations is around X, then around Z, then
+     * around X
+     */
+    public static final RotationOrder XZX =
+      new RotationOrder("XZX", Cartesian3D.PLUS_I, Cartesian3D.PLUS_K, Cartesian3D.PLUS_I);
+
+    /** Set of Euler angles.
+     * this ordered set of rotations is around Y, then around X, then
+     * around Y
+     */
+    public static final RotationOrder YXY =
+      new RotationOrder("YXY", Cartesian3D.PLUS_J, Cartesian3D.PLUS_I, Cartesian3D.PLUS_J);
+
+    /** Set of Euler angles.
+     * this ordered set of rotations is around Y, then around Z, then
+     * around Y
+     */
+    public static final RotationOrder YZY =
+      new RotationOrder("YZY", Cartesian3D.PLUS_J, Cartesian3D.PLUS_K, Cartesian3D.PLUS_J);
+
+    /** Set of Euler angles.
+     * this ordered set of rotations is around Z, then around X, then
+     * around Z
+     */
+    public static final RotationOrder ZXZ =
+      new RotationOrder("ZXZ", Cartesian3D.PLUS_K, Cartesian3D.PLUS_I, Cartesian3D.PLUS_K);
+
+    /** Set of Euler angles.
+     * this ordered set of rotations is around Z, then around Y, then
+     * around Z
+     */
+    public static final RotationOrder ZYZ =
+      new RotationOrder("ZYZ", Cartesian3D.PLUS_K, Cartesian3D.PLUS_J, Cartesian3D.PLUS_K);
+
+    /** Name of the rotations order. */
+    private final String name;
+
+    /** Axis of the first rotation. */
+    private final Cartesian3D a1;
+
+    /** Axis of the second rotation. */
+    private final Cartesian3D a2;
+
+    /** Axis of the third rotation. */
+    private final Cartesian3D a3;
+
+    /** Private constructor.
+     * This is a utility class that cannot be instantiated by the user,
+     * so its only constructor is private.
+     * @param name name of the rotation order
+     * @param a1 axis of the first rotation
+     * @param a2 axis of the second rotation
+     * @param a3 axis of the third rotation
+     */
+    private RotationOrder(final String name,
+                          final Cartesian3D a1, final Cartesian3D a2, final Cartesian3D a3) {
+        this.name = name;
+        this.a1   = a1;
+        this.a2   = a2;
+        this.a3   = a3;
+    }
+
+    /** Get a string representation of the instance.
+     * @return a string representation of the instance (in fact, its name)
+     */
+    @Override
+    public String toString() {
+        return name;
+    }
+
+    /** Get the axis of the first rotation.
+     * @return axis of the first rotation
+     */
+    public Cartesian3D getA1() {
+        return a1;
+    }
+
+    /** Get the axis of the second rotation.
+     * @return axis of the second rotation
+     */
+    public Cartesian3D getA2() {
+        return a2;
+    }
+
+    /** Get the axis of the second rotation.
+     * @return axis of the second rotation
+     */
+    public Cartesian3D getA3() {
+        return a3;
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Segment.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Segment.java
new file mode 100644
index 0000000..e9681a1
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Segment.java
@@ -0,0 +1,65 @@
+/*
+ * 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.geometry.euclidean.threed;
+
+
+/** Simple container for a two-points segment.
+ */
+public class Segment {
+
+    /** Start point of the segment. */
+    private final Cartesian3D start;
+
+    /** End point of the segments. */
+    private final Cartesian3D end;
+
+    /** Line containing the segment. */
+    private final Line     line;
+
+    /** Build a segment.
+     * @param start start point of the segment
+     * @param end end point of the segment
+     * @param line line containing the segment
+     */
+    public Segment(final Cartesian3D start, final Cartesian3D end, final Line line) {
+        this.start  = start;
+        this.end    = end;
+        this.line   = line;
+    }
+
+    /** Get the start point of the segment.
+     * @return start point of the segment
+     */
+    public Cartesian3D getStart() {
+        return start;
+    }
+
+    /** Get the end point of the segment.
+     * @return end point of the segment
+     */
+    public Cartesian3D getEnd() {
+        return end;
+    }
+
+    /** Get the line containing the segment.
+     * @return line containing the segment
+     */
+    public Line getLine() {
+        return line;
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubLine.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubLine.java
new file mode 100644
index 0000000..2be477f
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubLine.java
@@ -0,0 +1,147 @@
+/*
+ * 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.geometry.euclidean.threed;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.partitioning.Region.Location;
+import org.apache.commons.geometry.euclidean.oned.Cartesian1D;
+import org.apache.commons.geometry.euclidean.oned.Interval;
+import org.apache.commons.geometry.euclidean.oned.IntervalsSet;
+
+/** This class represents a subset of a {@link Line}.
+ */
+public class SubLine {
+
+    /** Underlying line. */
+    private final Line line;
+
+    /** Remaining region of the hyperplane. */
+    private final IntervalsSet remainingRegion;
+
+    /** Simple constructor.
+     * @param line underlying line
+     * @param remainingRegion remaining region of the line
+     */
+    public SubLine(final Line line, final IntervalsSet remainingRegion) {
+        this.line            = line;
+        this.remainingRegion = remainingRegion;
+    }
+
+    /** Create a sub-line from two endpoints.
+     * @param start start point
+     * @param end end point
+     * @param tolerance tolerance below which points are considered identical
+     * @exception IllegalArgumentException if the points are equal
+     */
+    public SubLine(final Cartesian3D start, final Cartesian3D end, final double tolerance)
+        throws IllegalArgumentException {
+        this(new Line(start, end, tolerance), buildIntervalSet(start, end, tolerance));
+    }
+
+    /** Create a sub-line from a segment.
+     * @param segment single segment forming the sub-line
+     * @exception IllegalArgumentException if the segment endpoints are equal
+     */
+    public SubLine(final Segment segment) throws IllegalArgumentException {
+        this(segment.getLine(),
+             buildIntervalSet(segment.getStart(), segment.getEnd(), segment.getLine().getTolerance()));
+    }
+
+    /** Get the endpoints of the sub-line.
+     * <p>
+     * A subline may be any arbitrary number of disjoints segments, so the endpoints
+     * are provided as a list of endpoint pairs. Each element of the list represents
+     * one segment, and each segment contains a start point at index 0 and an end point
+     * at index 1. If the sub-line is unbounded in the negative infinity direction,
+     * the start point of the first segment will have infinite coordinates. If the
+     * sub-line is unbounded in the positive infinity direction, the end point of the
+     * last segment will have infinite coordinates. So a sub-line covering the whole
+     * line will contain just one row and both elements of this row will have infinite
+     * coordinates. If the sub-line is empty, the returned list will contain 0 segments.
+     * </p>
+     * @return list of segments endpoints
+     */
+    public List<Segment> getSegments() {
+
+        final List<Interval> list = remainingRegion.asList();
+        final List<Segment> segments = new ArrayList<>(list.size());
+
+        for (final Interval interval : list) {
+            final Cartesian3D start = line.toSpace(new Cartesian1D(interval.getInf()));
+            final Cartesian3D end   = line.toSpace(new Cartesian1D(interval.getSup()));
+            segments.add(new Segment(start, end, line));
+        }
+
+        return segments;
+
+    }
+
+    /** Get the intersection of the instance and another sub-line.
+     * <p>
+     * This method is related to the {@link Line#intersection(Line)
+     * intersection} method in the {@link Line Line} class, but in addition
+     * to compute the point along infinite lines, it also checks the point
+     * lies on both sub-line ranges.
+     * </p>
+     * @param subLine other sub-line which may intersect instance
+     * @param includeEndPoints if true, endpoints are considered to belong to
+     * instance (i.e. they are closed sets) and may be returned, otherwise endpoints
+     * are considered to not belong to instance (i.e. they are open sets) and intersection
+     * occurring on endpoints lead to null being returned
+     * @return the intersection point if there is one, null if the sub-lines don't intersect
+     */
+    public Cartesian3D intersection(final SubLine subLine, final boolean includeEndPoints) {
+
+        // compute the intersection on infinite line
+        Cartesian3D v1D = line.intersection(subLine.line);
+        if (v1D == null) {
+            return null;
+        }
+
+        // check location of point with respect to first sub-line
+        Location loc1 = remainingRegion.checkPoint(line.toSubSpace((Point<Euclidean3D>) v1D));
+
+        // check location of point with respect to second sub-line
+        Location loc2 = subLine.remainingRegion.checkPoint(subLine.line.toSubSpace((Point<Euclidean3D>) v1D));
+
+        if (includeEndPoints) {
+            return ((loc1 != Location.OUTSIDE) && (loc2 != Location.OUTSIDE)) ? v1D : null;
+        } else {
+            return ((loc1 == Location.INSIDE) && (loc2 == Location.INSIDE)) ? v1D : null;
+        }
+
+    }
+
+    /** Build an interval set from two points.
+     * @param start start point
+     * @param end end point
+     * @return an interval set
+     * @param tolerance tolerance below which points are considered identical
+     * @exception IllegalArgumentException if the points are equal
+     */
+    private static IntervalsSet buildIntervalSet(final Cartesian3D start, final Cartesian3D end, final double tolerance)
+        throws IllegalArgumentException {
+        final Line line = new Line(start, end, tolerance);
+        return new IntervalsSet(line.toSubSpace((Point<Euclidean3D>) start).getX(),
+                                line.toSubSpace((Point<Euclidean3D>) end).getX(),
+                                tolerance);
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubPlane.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubPlane.java
new file mode 100644
index 0000000..7b38e64
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/SubPlane.java
@@ -0,0 +1,105 @@
+/*
+ * 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.geometry.euclidean.threed;
+
+import org.apache.commons.geometry.core.partitioning.AbstractSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.BSPTree;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Region;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.euclidean.oned.Cartesian1D;
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+import org.apache.commons.geometry.euclidean.twod.PolygonsSet;
+import org.apache.commons.geometry.euclidean.twod.Cartesian2D;
+
+/** This class represents a sub-hyperplane for {@link Plane}.
+ */
+public class SubPlane extends AbstractSubHyperplane<Euclidean3D, Euclidean2D> {
+
+    /** Simple constructor.
+     * @param hyperplane underlying hyperplane
+     * @param remainingRegion remaining region of the hyperplane
+     */
+    public SubPlane(final Hyperplane<Euclidean3D> hyperplane,
+                    final Region<Euclidean2D> remainingRegion) {
+        super(hyperplane, remainingRegion);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected AbstractSubHyperplane<Euclidean3D, Euclidean2D> buildNew(final Hyperplane<Euclidean3D> hyperplane,
+                                                                       final Region<Euclidean2D> remainingRegion) {
+        return new SubPlane(hyperplane, remainingRegion);
+    }
+
+    /** Split the instance in two parts by an hyperplane.
+     * @param hyperplane splitting hyperplane
+     * @return an object containing both the part of the instance
+     * on the plus side of the instance and the part of the
+     * instance on the minus side of the instance
+     */
+    @Override
+    public SplitSubHyperplane<Euclidean3D> split(Hyperplane<Euclidean3D> hyperplane) {
+
+        final Plane otherPlane = (Plane) hyperplane;
+        final Plane thisPlane  = (Plane) getHyperplane();
+        final Line  inter      = otherPlane.intersection(thisPlane);
+        final double tolerance = thisPlane.getTolerance();
+
+        if (inter == null) {
+            // the hyperplanes are parallel
+            final double global = otherPlane.getOffset(thisPlane);
+            if (global < -tolerance) {
+                return new SplitSubHyperplane<>(null, this);
+            } else if (global > tolerance) {
+                return new SplitSubHyperplane<>(this, null);
+            } else {
+                return new SplitSubHyperplane<>(null, null);
+            }
+        }
+
+        // the hyperplanes do intersect
+        Cartesian2D p = thisPlane.toSubSpace(inter.toSpace(Cartesian1D.ZERO));
+        Cartesian2D q = thisPlane.toSubSpace(inter.toSpace(Cartesian1D.ONE));
+        Cartesian3D crossP = Cartesian3D.crossProduct(inter.getDirection(), thisPlane.getNormal());
+        if (crossP.dotProduct(otherPlane.getNormal()) < 0) {
+            final Cartesian2D tmp = p;
+            p           = q;
+            q           = tmp;
+        }
+        final SubHyperplane<Euclidean2D> l2DMinus =
+            new org.apache.commons.geometry.euclidean.twod.Line(p, q, tolerance).wholeHyperplane();
+        final SubHyperplane<Euclidean2D> l2DPlus =
+            new org.apache.commons.geometry.euclidean.twod.Line(q, p, tolerance).wholeHyperplane();
+
+        final BSPTree<Euclidean2D> splitTree = getRemainingRegion().getTree(false).split(l2DMinus);
+        final BSPTree<Euclidean2D> plusTree  = getRemainingRegion().isEmpty(splitTree.getPlus()) ?
+                                               new BSPTree<Euclidean2D>(Boolean.FALSE) :
+                                               new BSPTree<>(l2DPlus, new BSPTree<Euclidean2D>(Boolean.FALSE),
+                                                                        splitTree.getPlus(), null);
+
+        final BSPTree<Euclidean2D> minusTree = getRemainingRegion().isEmpty(splitTree.getMinus()) ?
+                                               new BSPTree<Euclidean2D>(Boolean.FALSE) :
+                                                   new BSPTree<>(l2DMinus, new BSPTree<Euclidean2D>(Boolean.FALSE),
+                                                                            splitTree.getMinus(), null);
+
+        return new SplitSubHyperplane<>(new SubPlane(thisPlane.copySelf(), new PolygonsSet(plusTree, tolerance)),
+                                                   new SubPlane(thisPlane.copySelf(), new PolygonsSet(minusTree, tolerance)));
+
+    }
+
+}
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
new file mode 100644
index 0000000..03efa55
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Vector3D.java
@@ -0,0 +1,45 @@
+/*
+ * 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.geometry.euclidean.threed;
+
+import org.apache.commons.geometry.core.Vector;
+
+/**
+ * This class implements vectors in a three-dimensional space.
+ */
+public abstract class Vector3D implements Vector<Euclidean3D> {
+
+    /** Get the abscissa of the vector.
+     * @return abscissa of the vector
+     * @see Cartesian3D#Cartesian3D(double, double, double)
+     */
+    public abstract double getX();
+
+    /** Get the ordinate of the vector.
+     * @return ordinate of the vector
+     * @see Cartesian3D#Cartesian3D(double, double, double)
+     */
+    public abstract double getY();
+
+    /** Get the height of the vector.
+     * @return height of the vector
+     * @see Cartesian3D#Cartesian3D(double, double, double)
+     */
+    public abstract double getZ();
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/package-info.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/package-info.java
new file mode 100644
index 0000000..ef62edc
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+/**
+ *
+ * <p>
+ * This package provides basic 3D geometry components.
+ * </p>
+ *
+ */
+package org.apache.commons.geometry.euclidean.threed;
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Cartesian2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Cartesian2D.java
new file mode 100644
index 0000000..483d341
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Cartesian2D.java
@@ -0,0 +1,491 @@
+/*
+ * 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.geometry.euclidean.twod;
+
+import java.text.NumberFormat;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+import org.apache.commons.geometry.core.Vector;
+import org.apache.commons.numbers.arrays.LinearCombination;
+
+/** This class represents a 2D point or a 2D vector.
+ * <p>An instance of Cartesian2D represents the point with the corresponding
+ * coordinates.</p>
+ * <p>An instance of Cartesian2D also represents the vector which begins at
+ * the origin and ends at the point corresponding to the coordinates.</p>
+ * <p>Instances of this class are guaranteed to be immutable.</p>
+ */
+public class Cartesian2D extends Vector2D implements Point<Euclidean2D> {
+
+    /** Origin (coordinates: 0, 0). */
+    public static final Cartesian2D ZERO   = new Cartesian2D(0, 0);
+
+    // CHECKSTYLE: stop ConstantName
+    /** A vector with all coordinates set to NaN. */
+    public static final Cartesian2D NaN = new Cartesian2D(Double.NaN, Double.NaN);
+    // CHECKSTYLE: resume ConstantName
+
+    /** A vector with all coordinates set to positive infinity. */
+    public static final Cartesian2D POSITIVE_INFINITY =
+        new Cartesian2D(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
+
+    /** A vector with all coordinates set to negative infinity. */
+    public static final Cartesian2D NEGATIVE_INFINITY =
+        new Cartesian2D(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY);
+
+    /** Serializable UID. */
+    private static final long serialVersionUID = 266938651998679754L;
+
+    /** Error message when norms are zero. */
+    private static final String ZERO_NORM_MSG = "Norm is zero";
+
+    /** Abscissa. */
+    private final double x;
+
+    /** Ordinate. */
+    private final double y;
+
+    /** Simple constructor.
+     * Build a vector from its coordinates
+     * @param x abscissa
+     * @param y ordinate
+     * @see #getX()
+     * @see #getY()
+     */
+    public Cartesian2D(double x, double y) {
+        this.x = x;
+        this.y = y;
+    }
+
+    /** Simple constructor.
+     * Build a vector from its coordinates
+     * @param v coordinates array
+     * @exception DimensionMismatchException if array does not have 2 elements
+     * @see #toArray()
+     */
+    public Cartesian2D(double[] v) throws IllegalArgumentException {
+        if (v.length != 2) {
+            throw new IllegalArgumentException("Dimension mismatch: " + v.length + " != 2");
+        }
+        this.x = v[0];
+        this.y = v[1];
+    }
+
+    /** Multiplicative constructor
+     * Build a vector from another one and a scale factor.
+     * The vector built will be a * u
+     * @param a scale factor
+     * @param u base (unscaled) vector
+     */
+    public Cartesian2D(double a, Cartesian2D u) {
+        this.x = a * u.x;
+        this.y = a * u.y;
+    }
+
+    /** Linear constructor
+     * Build a vector from two other ones and corresponding scale factors.
+     * The vector built will be a1 * u1 + a2 * u2
+     * @param a1 first scale factor
+     * @param u1 first base (unscaled) vector
+     * @param a2 second scale factor
+     * @param u2 second base (unscaled) vector
+     */
+    public Cartesian2D(double a1, Cartesian2D u1, double a2, Cartesian2D u2) {
+        this.x = a1 * u1.x + a2 * u2.x;
+        this.y = a1 * u1.y + a2 * u2.y;
+    }
+
+    /** Linear constructor
+     * Build a vector from three other ones and corresponding scale factors.
+     * The vector built will be a1 * u1 + a2 * u2 + a3 * u3
+     * @param a1 first scale factor
+     * @param u1 first base (unscaled) vector
+     * @param a2 second scale factor
+     * @param u2 second base (unscaled) vector
+     * @param a3 third scale factor
+     * @param u3 third base (unscaled) vector
+     */
+    public Cartesian2D(double a1, Cartesian2D u1, double a2, Cartesian2D u2,
+                   double a3, Cartesian2D u3) {
+        this.x = a1 * u1.x + a2 * u2.x + a3 * u3.x;
+        this.y = a1 * u1.y + a2 * u2.y + a3 * u3.y;
+    }
+
+    /** Linear constructor
+     * Build a vector from four other ones and corresponding scale factors.
+     * The vector built will be a1 * u1 + a2 * u2 + a3 * u3 + a4 * u4
+     * @param a1 first scale factor
+     * @param u1 first base (unscaled) vector
+     * @param a2 second scale factor
+     * @param u2 second base (unscaled) vector
+     * @param a3 third scale factor
+     * @param u3 third base (unscaled) vector
+     * @param a4 fourth scale factor
+     * @param u4 fourth base (unscaled) vector
+     */
+    public Cartesian2D(double a1, Cartesian2D u1, double a2, Cartesian2D u2,
+                   double a3, Cartesian2D u3, double a4, Cartesian2D u4) {
+        this.x = a1 * u1.x + a2 * u2.x + a3 * u3.x + a4 * u4.x;
+        this.y = a1 * u1.y + a2 * u2.y + a3 * u3.y + a4 * u4.y;
+    }
+
+    /** Get the abscissa of the vector.
+     * @return abscissa of the vector
+     * @see #Cartesian2D(double, double)
+     */
+    @Override
+    public double getX() {
+        return x;
+    }
+
+    /** Get the ordinate of the vector.
+     * @return ordinate of the vector
+     * @see #Cartesian2D(double, double)
+     */
+    @Override
+    public double getY() {
+        return y;
+    }
+
+    /** Get the vector coordinates as a dimension 2 array.
+     * @return vector coordinates
+     * @see #Cartesian2D(double[])
+     */
+    public double[] toArray() {
+        return new double[] { x, y };
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Space getSpace() {
+        return Euclidean2D.getInstance();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian2D getZero() {
+        return ZERO;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getNorm1() {
+        return Math.abs(x) + Math.abs(y);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getNorm() {
+        return Math.sqrt (x * x + y * y);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getNormSq() {
+        return x * x + y * y;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getNormInf() {
+        return Math.max(Math.abs(x), Math.abs(y));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian2D add(Vector<Euclidean2D> v) {
+        Cartesian2D v2 = (Cartesian2D) v;
+        return new Cartesian2D(x + v2.getX(), y + v2.getY());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian2D add(double factor, Vector<Euclidean2D> v) {
+        Cartesian2D v2 = (Cartesian2D) v;
+        return new Cartesian2D(x + factor * v2.getX(), y + factor * v2.getY());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian2D subtract(Vector<Euclidean2D> p) {
+        Cartesian2D p3 = (Cartesian2D) p;
+        return new Cartesian2D(x - p3.x, y - p3.y);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian2D subtract(double factor, Vector<Euclidean2D> v) {
+        Cartesian2D v2 = (Cartesian2D) v;
+        return new Cartesian2D(x - factor * v2.getX(), y - factor * v2.getY());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian2D normalize() throws IllegalStateException {
+        double s = getNorm();
+        if (s == 0) {
+            throw new IllegalStateException(ZERO_NORM_MSG);
+        }
+        return scalarMultiply(1 / s);
+    }
+
+    /** Compute the angular separation between two vectors.
+     * <p>This method computes the angular separation between two
+     * vectors using the dot product for well separated vectors and the
+     * cross product for almost aligned vectors. This allows to have a
+     * good accuracy in all cases, even for vectors very close to each
+     * other.</p>
+     * @param v1 first vector
+     * @param v2 second vector
+     * @return angular separation between v1 and v2
+     * @exception IllegalArgumentException if either vector has a zero norm
+     */
+    public static double angle(Cartesian2D v1, Cartesian2D v2) throws IllegalArgumentException {
+
+        double normProduct = v1.getNorm() * v2.getNorm();
+        if (normProduct == 0) {
+            throw new IllegalArgumentException(ZERO_NORM_MSG);
+        }
+
+        double dot = v1.dotProduct(v2);
+        double threshold = normProduct * 0.9999;
+        if ((dot < -threshold) || (dot > threshold)) {
+            // the vectors are almost aligned, compute using the sine
+            final double n = Math.abs(LinearCombination.value(v1.x, v2.y, -v1.y, v2.x));
+            if (dot >= 0) {
+                return Math.asin(n / normProduct);
+            }
+            return Math.PI - Math.asin(n / normProduct);
+        }
+
+        // the vectors are sufficiently separated to use the cosine
+        return Math.acos(dot / normProduct);
+
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian2D negate() {
+        return new Cartesian2D(-x, -y);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian2D scalarMultiply(double a) {
+        return new Cartesian2D(a * x, a * y);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isNaN() {
+        return Double.isNaN(x) || Double.isNaN(y);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isInfinite() {
+        return !isNaN() && (Double.isInfinite(x) || Double.isInfinite(y));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double distance1(Vector<Euclidean2D> p) {
+        Cartesian2D p3 = (Cartesian2D) p;
+        final double dx = Math.abs(p3.x - x);
+        final double dy = Math.abs(p3.y - y);
+        return dx + dy;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double distance(Point<Euclidean2D> p) {
+        return distance((Cartesian2D) p);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double distance(Vector<Euclidean2D> v) {
+        return distance((Cartesian2D) v);
+    }
+
+    /** Compute the distance between the instance and other coordinates.
+     * @param c other coordinates
+     * @return the distance between the instance and c
+     */
+    public double distance(Cartesian2D c) {
+        final double dx = c.x - x;
+        final double dy = c.y - y;
+        return Math.sqrt(dx * dx + dy * dy);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double distanceInf(Vector<Euclidean2D> p) {
+        Cartesian2D p3 = (Cartesian2D) p;
+        final double dx = Math.abs(p3.x - x);
+        final double dy = Math.abs(p3.y - y);
+        return Math.max(dx, dy);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double distanceSq(Vector<Euclidean2D> p) {
+        Cartesian2D p3 = (Cartesian2D) p;
+        final double dx = p3.x - x;
+        final double dy = p3.y - y;
+        return dx * dx + dy * dy;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double dotProduct(final Vector<Euclidean2D> v) {
+        final Cartesian2D v2 = (Cartesian2D) v;
+        return LinearCombination.value(x, v2.x, y, v2.y);
+    }
+
+    /**
+     * Compute the cross-product of the instance and the given vector.
+     * <p>
+     * The cross product can be used to determine the location of a point
+     * with regard to the line formed by (p1, p2) and is calculated as:
+     * \[
+     *    P = (x_2 - x_1)(y_3 - y_1) - (y_2 - y_1)(x_3 - x_1)
+     * \]
+     * with \(p3 = (x_3, y_3)\) being this instance.
+     * <p>
+     * If the result is 0, the points are collinear, i.e. lie on a single straight line L;
+     * if it is positive, this point lies to the left, otherwise to the right of the line
+     * formed by (p1, p2).
+     *
+     * @param p1 first point of the line
+     * @param p2 second point of the line
+     * @return the cross-product
+     *
+     * @see <a href="http://en.wikipedia.org/wiki/Cross_product";>Cross product (Wikipedia)</a>
+     */
+    public double crossProduct(final Cartesian2D p1, final Cartesian2D p2) {
+        final double x1 = p2.getX() - p1.getX();
+        final double y1 = getY() - p1.getY();
+        final double x2 = getX() - p1.getX();
+        final double y2 = p2.getY() - p1.getY();
+        return LinearCombination.value(x1, y1, -x2, y2);
+    }
+
+    /** Compute the distance between two points according to the L<sub>2</sub> norm.
+     * <p>Calling this method is equivalent to calling:
+     * <code>p1.subtract(p2).getNorm()</code> except that no intermediate
+     * vector is built</p>
+     * @param p1 first point
+     * @param p2 second point
+     * @return the distance between p1 and p2 according to the L<sub>2</sub> norm
+     */
+    public static double distance(Cartesian2D p1, Cartesian2D p2) {
+        return p1.distance(p2);
+    }
+
+    /** Compute the distance between two points according to the L<sub>&infin;</sub> norm.
+     * <p>Calling this method is equivalent to calling:
+     * <code>p1.subtract(p2).getNormInf()</code> except that no intermediate
+     * vector is built</p>
+     * @param p1 first point
+     * @param p2 second point
+     * @return the distance between p1 and p2 according to the L<sub>&infin;</sub> norm
+     */
+    public static double distanceInf(Cartesian2D p1, Cartesian2D p2) {
+        return p1.distanceInf(p2);
+    }
+
+    /** Compute the square of the distance between two points.
+     * <p>Calling this method is equivalent to calling:
+     * <code>p1.subtract(p2).getNormSq()</code> except that no intermediate
+     * vector is built</p>
+     * @param p1 first point
+     * @param p2 second point
+     * @return the square of the distance between p1 and p2
+     */
+    public static double distanceSq(Cartesian2D p1, Cartesian2D p2) {
+        return p1.distanceSq(p2);
+    }
+
+    /**
+     * Test for the equality of two 2D instances.
+     * <p>
+     * If all coordinates of two 2D vectors are exactly the same, and none are
+     * <code>Double.NaN</code>, the two 2D instances are considered to be equal.
+     * </p>
+     * <p>
+     * <code>NaN</code> coordinates are considered to affect globally the vector
+     * and be equals to each other - i.e, if either (or all) coordinates of the
+     * 2D vector are equal to <code>Double.NaN</code>, the 2D vector is equal to
+     * {@link #NaN}.
+     * </p>
+     *
+     * @param other Object to test for equality to this
+     * @return true if two 2D Cartesian objects are equal, false if
+     *         object is null, not an instance of Cartesian2D, or
+     *         not equal to this Cartesian2D instance
+     *
+     */
+    @Override
+    public boolean equals(Object other) {
+
+        if (this == other) {
+            return true;
+        }
+
+        if (other instanceof Cartesian2D) {
+            final Cartesian2D rhs = (Cartesian2D)other;
+            if (rhs.isNaN()) {
+                return this.isNaN();
+            }
+
+            return (x == rhs.x) && (y == rhs.y);
+        }
+        return false;
+    }
+
+    /**
+     * Get a hashCode for the 2D coordinates.
+     * <p>
+     * All NaN values have the same hash code.</p>
+     *
+     * @return a hash code value for this object
+     */
+    @Override
+    public int hashCode() {
+        if (isNaN()) {
+            return 542;
+        }
+        return 122 * (76 * Double.hashCode(x) +  Double.hashCode(y));
+    }
+
+    /** Get a string representation of this vector.
+     * @return a string representation of this vector
+     */
+    @Override
+    public String toString() {
+        return toString(NumberFormat.getInstance());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString(final NumberFormat format) {
+        return "{" + format.format(x) + "; " + format.format(y) + "}";
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Euclidean2D.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Euclidean2D.java
new file mode 100644
index 0000000..83ec10c
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Euclidean2D.java
@@ -0,0 +1,75 @@
+/*
+ * 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.geometry.euclidean.twod;
+
+import java.io.Serializable;
+
+import org.apache.commons.geometry.core.Space;
+import org.apache.commons.geometry.euclidean.oned.Euclidean1D;
+
+/**
+ * This class implements a two-dimensional space.
+ */
+public class Euclidean2D implements Serializable, Space {
+
+    /** Serializable version identifier. */
+    private static final long serialVersionUID = 4793432849757649566L;
+
+    /** Private constructor for the singleton.
+     */
+    private Euclidean2D() {
+    }
+
+    /** Get the unique instance.
+     * @return the unique instance
+     */
+    public static Euclidean2D getInstance() {
+        return LazyHolder.INSTANCE;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getDimension() {
+        return 2;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Euclidean1D getSubSpace() {
+        return Euclidean1D.getInstance();
+    }
+
+    // CHECKSTYLE: stop HideUtilityClassConstructor
+    /** Holder for the instance.
+     * <p>We use here the Initialization On Demand Holder Idiom.</p>
+     */
+    private static class LazyHolder {
+        /** Cached field instance. */
+        private static final Euclidean2D INSTANCE = new Euclidean2D();
+    }
+    // CHECKSTYLE: resume HideUtilityClassConstructor
+
+    /** Handle deserialization of the singleton.
+     * @return the singleton instance
+     */
+    private Object readResolve() {
+        // return the singleton instance
+        return LazyHolder.INSTANCE;
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java
new file mode 100644
index 0000000..aadb7ca
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Line.java
@@ -0,0 +1,559 @@
+/*
+ * 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.geometry.euclidean.twod;
+
+import org.apache.commons.numbers.arrays.LinearCombination;
+import org.apache.commons.numbers.angle.PlaneAngleRadians;
+import org.apache.commons.geometry.euclidean.oned.Euclidean1D;
+import org.apache.commons.geometry.euclidean.oned.IntervalsSet;
+import org.apache.commons.geometry.euclidean.oned.OrientedPoint;
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Vector;
+import org.apache.commons.geometry.core.partitioning.Embedding;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Transform;
+import org.apache.commons.geometry.euclidean.oned.Cartesian1D;
+
+/** This class represents an oriented line in the 2D plane.
+
+ * <p>An oriented line can be defined either by prolongating a line
+ * segment between two points past these points, or by one point and
+ * an angular direction (in trigonometric orientation).</p>
+
+ * <p>Since it is oriented the two half planes at its two sides are
+ * unambiguously identified as a left half plane and a right half
+ * plane. This can be used to identify the interior and the exterior
+ * in a simple way by local properties only when part of a line is
+ * used to define part of a polygon boundary.</p>
+
+ * <p>A line can also be used to completely define a reference frame
+ * in the plane. It is sufficient to select one specific point in the
+ * line (the orthogonal projection of the original reference frame on
+ * the line) and to use the unit vector in the line direction and the
+ * orthogonal vector oriented from left half plane to right half
+ * plane. We define two coordinates by the process, the
+ * <em>abscissa</em> along the line, and the <em>offset</em> across
+ * the line. All points of the plane are uniquely identified by these
+ * two coordinates. The line is the set of points at zero offset, the
+ * left half plane is the set of points with negative offsets and the
+ * right half plane is the set of points with positive offsets.</p>
+ */
+public class Line implements Hyperplane<Euclidean2D>, Embedding<Euclidean2D, Euclidean1D> {
+    /** Angle with respect to the abscissa axis. */
+    private double angle;
+
+    /** Cosine of the line angle. */
+    private double cos;
+
+    /** Sine of the line angle. */
+    private double sin;
+
+    /** Offset of the frame origin. */
+    private double originOffset;
+
+    /** Tolerance below which points are considered identical. */
+    private final double tolerance;
+
+    /** Reverse line. */
+    private Line reverse;
+
+    /** Build a line from two points.
+     * <p>The line is oriented from p1 to p2</p>
+     * @param p1 first point
+     * @param p2 second point
+     * @param tolerance tolerance below which points are considered identical
+     */
+    public Line(final Cartesian2D p1, final Cartesian2D p2, final double tolerance) {
+        reset(p1, p2);
+        this.tolerance = tolerance;
+    }
+
+    /** Build a line from a point and an angle.
+     * @param p point belonging to the line
+     * @param angle angle of the line with respect to abscissa axis
+     * @param tolerance tolerance below which points are considered identical
+     */
+    public Line(final Cartesian2D p, final double angle, final double tolerance) {
+        reset(p, angle);
+        this.tolerance = tolerance;
+    }
+
+    /** Build a line from its internal characteristics.
+     * @param angle angle of the line with respect to abscissa axis
+     * @param cos cosine of the angle
+     * @param sin sine of the angle
+     * @param originOffset offset of the origin
+     * @param tolerance tolerance below which points are considered identical
+     */
+    private Line(final double angle, final double cos, final double sin,
+                 final double originOffset, final double tolerance) {
+        this.angle        = angle;
+        this.cos          = cos;
+        this.sin          = sin;
+        this.originOffset = originOffset;
+        this.tolerance    = tolerance;
+        this.reverse      = null;
+    }
+
+    /** Copy constructor.
+     * <p>The created instance is completely independent from the
+     * original instance, it is a deep copy.</p>
+     * @param line line to copy
+     */
+    public Line(final Line line) {
+        angle        = PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(line.angle);
+        cos          = line.cos;
+        sin          = line.sin;
+        originOffset = line.originOffset;
+        tolerance    = line.tolerance;
+        reverse      = null;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Line copySelf() {
+        return new Line(this);
+    }
+
+    /** Reset the instance as if built from two points.
+     * <p>The line is oriented from p1 to p2</p>
+     * @param p1 first point
+     * @param p2 second point
+     */
+    public void reset(final Cartesian2D p1, final Cartesian2D p2) {
+        unlinkReverse();
+        final double dx = p2.getX() - p1.getX();
+        final double dy = p2.getY() - p1.getY();
+        final double d = Math.hypot(dx, dy);
+        if (d == 0.0) {
+            angle        = 0.0;
+            cos          = 1.0;
+            sin          = 0.0;
+            originOffset = p1.getY();
+        } else {
+            angle        = Math.PI + Math.atan2(-dy, -dx);
+            cos          = dx / d;
+            sin          = dy / d;
+            originOffset = LinearCombination.value(p2.getX(), p1.getY(), -p1.getX(), p2.getY()) / d;
+        }
+    }
+
+    /** Reset the instance as if built from a line and an angle.
+     * @param p point belonging to the line
+     * @param alpha angle of the line with respect to abscissa axis
+     */
+    public void reset(final Cartesian2D p, final double alpha) {
+        unlinkReverse();
+        this.angle   = PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(alpha);
+        cos          = Math.cos(this.angle);
+        sin          = Math.sin(this.angle);
+        originOffset = LinearCombination.value(cos, p.getY(), -sin, p.getX());
+    }
+
+    /** Revert the instance.
+     */
+    public void revertSelf() {
+        unlinkReverse();
+        if (angle < Math.PI) {
+            angle += Math.PI;
+        } else {
+            angle -= Math.PI;
+        }
+        cos          = -cos;
+        sin          = -sin;
+        originOffset = -originOffset;
+    }
+
+    /** Unset the link between an instance and its reverse.
+     */
+    private void unlinkReverse() {
+        if (reverse != null) {
+            reverse.reverse = null;
+        }
+        reverse = null;
+    }
+
+    /** Get the reverse of the instance.
+     * <p>Get a line with reversed orientation with respect to the
+     * instance.</p>
+     * <p>
+     * As long as neither the instance nor its reverse are modified
+     * (i.e. as long as none of the {@link #reset(Cartesian2D, Cartesian2D)},
+     * {@link #reset(Cartesian2D, double)}, {@link #revertSelf()},
+     * {@link #setAngle(double)} or {@link #setOriginOffset(double)}
+     * methods are called), then the line and its reverse remain linked
+     * together so that {@code line.getReverse().getReverse() == line}.
+     * When one of the line is modified, the link is deleted as both
+     * instance becomes independent.
+     * </p>
+     * @return a new line, with orientation opposite to the instance orientation
+     */
+    public Line getReverse() {
+        if (reverse == null) {
+            reverse = new Line((angle < Math.PI) ? (angle + Math.PI) : (angle - Math.PI),
+                               -cos, -sin, -originOffset, tolerance);
+            reverse.reverse = this;
+        }
+        return reverse;
+    }
+
+    /** Transform a space point into a sub-space point.
+     * @param vector n-dimension point of the space
+     * @return (n-1)-dimension point of the sub-space corresponding to
+     * the specified space point
+     */
+    public Cartesian1D toSubSpace(Vector<Euclidean2D> vector) {
+        return toSubSpace((Cartesian2D) vector);
+    }
+
+    /** Transform a sub-space point into a space point.
+     * @param vector (n-1)-dimension point of the sub-space
+     * @return n-dimension point of the space corresponding to the
+     * specified sub-space point
+     */
+    public Cartesian2D toSpace(Vector<Euclidean1D> vector) {
+        return toSpace((Cartesian1D) vector);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian1D toSubSpace(final Point<Euclidean2D> point) {
+        return toSubSpace((Cartesian2D) point);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cartesian2D toSpace(final Point<Euclidean1D> point) {
+        return toSpace((Cartesian1D) point);
+    }
+
+    /** Transform a space point into a sub-space point.
+     * @param cartesian n-dimension point of the space
+     * @return (n-1)-dimension point of the sub-space corresponding to
+     * the specified space point
+     */
+    public Cartesian1D toSubSpace(final Cartesian2D cartesian) {
+        return new Cartesian1D(LinearCombination.value(cos, cartesian.getX(), sin, cartesian.getY()));
+    }
+
+    /** Transform a sub-space point into a space point.
+     * @param cartesian (n-1)-dimension point of the sub-space
+     * @return n-dimension point of the space corresponding to the
+     * specified sub-space point
+     */
+    public Cartesian2D toSpace(Cartesian1D cartesian) {
+        final double abscissa = cartesian.getX();
+        return new Cartesian2D(LinearCombination.value(abscissa, cos, -originOffset, sin),
+                            LinearCombination.value(abscissa, sin,  originOffset, cos));
+    }
+
+    /** Get the intersection point of the instance and another line.
+     * @param other other line
+     * @return intersection point of the instance and the other line
+     * or null if there are no intersection points
+     */
+    public Cartesian2D intersection(final Line other) {
+        final double d = LinearCombination.value(sin, other.cos, -other.sin, cos);
+        if (Math.abs(d) < tolerance) {
+            return null;
+        }
+        return new Cartesian2D(LinearCombination.value(cos, other.originOffset, -other.cos, originOffset) / d,
+                            LinearCombination.value(sin, other.originOffset, -other.sin, originOffset) / d);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Point<Euclidean2D> project(Point<Euclidean2D> point) {
+        return toSpace(toSubSpace(point));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getTolerance() {
+        return tolerance;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public SubLine wholeHyperplane() {
+        return new SubLine(this, new IntervalsSet(tolerance));
+    }
+
+    /** Build a region covering the whole space.
+     * @return a region containing the instance (really a {@link
+     * PolygonsSet PolygonsSet} instance)
+     */
+    @Override
+    public PolygonsSet wholeSpace() {
+        return new PolygonsSet(tolerance);
+    }
+
+    /** Get the offset (oriented distance) of a parallel line.
+     * <p>This method should be called only for parallel lines otherwise
+     * the result is not meaningful.</p>
+     * <p>The offset is 0 if both lines are the same, it is
+     * positive if the line is on the right side of the instance and
+     * negative if it is on the left side, according to its natural
+     * orientation.</p>
+     * @param line line to check
+     * @return offset of the line
+     */
+    public double getOffset(final Line line) {
+        return originOffset +
+               (LinearCombination.value(cos, line.cos, sin, line.sin) > 0 ? -line.originOffset : line.originOffset);
+    }
+
+    /** Get the offset (oriented distance) of a vector.
+     * @param vector vector to check
+     * @return offset of the vector
+     */
+    public double getOffset(Vector<Euclidean2D> vector) {
+        return getOffset((Cartesian2D) vector);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getOffset(final Point<Euclidean2D> point) {
+        return getOffset((Cartesian2D) point);
+    }
+
+    /** Get the offset (oriented distance) of a point.
+     * @param cartesian point to check
+     * @return offset of the point
+     */
+    public double getOffset(Cartesian2D cartesian) {
+        return LinearCombination.value(sin, cartesian.getX(), -cos, cartesian.getY(), 1.0, originOffset);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean sameOrientationAs(final Hyperplane<Euclidean2D> other) {
+        final Line otherL = (Line) other;
+        return LinearCombination.value(sin, otherL.sin, cos, otherL.cos) >= 0.0;
+    }
+
+    /** Get one point from the plane.
+     * @param abscissa desired abscissa for the point
+     * @param offset desired offset for the point
+     * @return one point in the plane, with given abscissa and offset
+     * relative to the line
+     */
+    public Cartesian2D getPointAt(final Cartesian1D abscissa, final double offset) {
+        final double x       = abscissa.getX();
+        final double dOffset = offset - originOffset;
+        return new Cartesian2D(LinearCombination.value(x, cos,  dOffset, sin),
+                            LinearCombination.value(x, sin, -dOffset, cos));
+    }
+
+    /** Check if the line contains a point.
+     * @param p point to check
+     * @return true if p belongs to the line
+     */
+    public boolean contains(final Cartesian2D p) {
+        return Math.abs(getOffset(p)) < tolerance;
+    }
+
+    /** Compute the distance between the instance and a point.
+     * <p>This is a shortcut for invoking Math.abs(getOffset(p)),
+     * and provides consistency with what is in the
+     * org.apache.commons.geometry.euclidean.threed.Line class.</p>
+     *
+     * @param p to check
+     * @return distance between the instance and the point
+     */
+    public double distance(final Cartesian2D p) {
+        return Math.abs(getOffset(p));
+    }
+
+    /** Check the instance is parallel to another line.
+     * @param line other line to check
+     * @return true if the instance is parallel to the other line
+     * (they can have either the same or opposite orientations)
+     */
+    public boolean isParallelTo(final Line line) {
+        return Math.abs(LinearCombination.value(sin, line.cos, -cos, line.sin)) < tolerance;
+    }
+
+    /** Translate the line to force it passing by a point.
+     * @param p point by which the line should pass
+     */
+    public void translateToPoint(final Cartesian2D p) {
+        originOffset = LinearCombination.value(cos, p.getY(), -sin, p.getX());
+    }
+
+    /** Get the angle of the line.
+     * @return the angle of the line with respect to the abscissa axis
+     */
+    public double getAngle() {
+        return PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(angle);
+    }
+
+    /** Set the angle of the line.
+     * @param angle new angle of the line with respect to the abscissa axis
+     */
+    public void setAngle(final double angle) {
+        unlinkReverse();
+        this.angle = PlaneAngleRadians.normalizeBetweenZeroAndTwoPi(angle);
+        cos        = Math.cos(this.angle);
+        sin        = Math.sin(this.angle);
+    }
+
+    /** Get the offset of the origin.
+     * @return the offset of the origin
+     */
+    public double getOriginOffset() {
+        return originOffset;
+    }
+
+    /** Set the offset of the origin.
+     * @param offset offset of the origin
+     */
+    public void setOriginOffset(final double offset) {
+        unlinkReverse();
+        originOffset = offset;
+    }
+
+    /** Get a {@link org.apache.commons.geometry.core.partitioning.Transform
+     * Transform} embedding an affine transform.
+     * @param cXX transform factor between input abscissa and output abscissa
+     * @param cYX transform factor between input abscissa and output ordinate
+     * @param cXY transform factor between input ordinate and output abscissa
+     * @param cYY transform factor between input ordinate and output ordinate
+     * @param cX1 transform addendum for output abscissa
+     * @param cY1 transform addendum for output ordinate
+     * @return a new transform that can be applied to either {@link
+     * Cartesian2D}, {@link Line Line} or {@link
+     * org.apache.commons.geometry.core.partitioning.SubHyperplane
+     * SubHyperplane} instances
+     * @exception IllegalArgumentException if the transform is non invertible
+     */
+    public static Transform<Euclidean2D, Euclidean1D> getTransform(final double cXX,
+                                                                   final double cYX,
+                                                                   final double cXY,
+                                                                   final double cYY,
+                                                                   final double cX1,
+                                                                   final double cY1)
+        throws IllegalArgumentException {
+        return new LineTransform(cXX, cYX, cXY, cYY, cX1, cY1);
+    }
+
+    /** Class embedding an affine transform.
+     * <p>This class is used in order to apply an affine transform to a
+     * line. Using a specific object allow to perform some computations
+     * on the transform only once even if the same transform is to be
+     * applied to a large number of lines (for example to a large
+     * polygon)./<p>
+     */
+    private static class LineTransform implements Transform<Euclidean2D, Euclidean1D> {
+
+        /** Transform factor between input abscissa and output abscissa. */
+        private final double cXX;
+
+        /** Transform factor between input abscissa and output ordinate. */
+        private final double cYX;
+
+        /** Transform factor between input ordinate and output abscissa. */
+        private final double cXY;
+
+        /** Transform factor between input ordinate and output ordinate. */
+        private final double cYY;
+
+        /** Transform addendum for output abscissa. */
+        private final double cX1;
+
+        /** Transform addendum for output ordinate. */
+        private final double cY1;
+
+        /** cXY * cY1 - cYY * cX1. */
+        private final double c1Y;
+
+        /** cXX * cY1 - cYX * cX1. */
+        private final double c1X;
+
+        /** cXX * cYY - cYX * cXY. */
+        private final double c11;
+
+        /** Build an affine line transform from a n {@code AffineTransform}.
+         * @param cXX transform factor between input abscissa and output abscissa
+         * @param cYX transform factor between input abscissa and output ordinate
+         * @param cXY transform factor between input ordinate and output abscissa
+         * @param cYY transform factor between input ordinate and output ordinate
+         * @param cX1 transform addendum for output abscissa
+         * @param cY1 transform addendum for output ordinate
+         * @exception IllegalArgumentException if the transform is non invertible
+         */
+        LineTransform(final double cXX, final double cYX, final double cXY,
+                      final double cYY, final double cX1, final double cY1)
+            throws IllegalArgumentException {
+
+            this.cXX = cXX;
+            this.cYX = cYX;
+            this.cXY = cXY;
+            this.cYY = cYY;
+            this.cX1 = cX1;
+            this.cY1 = cY1;
+
+            c1Y = LinearCombination.value(cXY, cY1, -cYY, cX1);
+            c1X = LinearCombination.value(cXX, cY1, -cYX, cX1);
+            c11 = LinearCombination.value(cXX, cYY, -cYX, cXY);
+
+            if (Math.abs(c11) < 1.0e-20) {
+                throw new IllegalArgumentException("Non-invertible affine transform collapses some lines into single points");
+            }
+
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Cartesian2D apply(final Point<Euclidean2D> point) {
+            final Cartesian2D p2D = (Cartesian2D) point;
+            final double  x   = p2D.getX();
+            final double  y   = p2D.getY();
+            return new Cartesian2D(LinearCombination.value(cXX, x, cXY, y, cX1, 1),
+                                LinearCombination.value(cYX, x, cYY, y, cY1, 1));
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Line apply(final Hyperplane<Euclidean2D> hyperplane) {
+            final Line   line    = (Line) hyperplane;
+            final double rOffset = LinearCombination.value(c1X, line.cos, c1Y, line.sin, c11, line.originOffset);
+            final double rCos    = LinearCombination.value(cXX, line.cos, cXY, line.sin);
+            final double rSin    = LinearCombination.value(cYX, line.cos, cYY, line.sin);
+            final double inv     = 1.0 / Math.sqrt(rSin * rSin + rCos * rCos);
+            return new Line(Math.PI + Math.atan2(-rSin, -rCos),
+                            inv * rCos, inv * rSin,
+                            inv * rOffset, line.tolerance);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public SubHyperplane<Euclidean1D> apply(final SubHyperplane<Euclidean1D> sub,
+                                                final Hyperplane<Euclidean2D> original,
+                                                final Hyperplane<Euclidean2D> transformed) {
+            final OrientedPoint op     = (OrientedPoint) sub.getHyperplane();
+            final Line originalLine    = (Line) original;
+            final Line transformedLine = (Line) transformed;
+            final Cartesian1D newLoc =
+                transformedLine.toSubSpace(apply(originalLine.toSpace(op.getLocation())));
+            return new OrientedPoint(newLoc, op.isDirect(), originalLine.tolerance).wholeHyperplane();
+        }
+
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/NestedLoops.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/NestedLoops.java
new file mode 100644
index 0000000..75554f2
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/NestedLoops.java
@@ -0,0 +1,195 @@
+/*
+ * 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.geometry.euclidean.twod;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.partitioning.Region;
+import org.apache.commons.geometry.core.partitioning.RegionFactory;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.euclidean.oned.IntervalsSet;
+
+/** This class represent a tree of nested 2D boundary loops.
+
+ * <p>This class is used for piecewise polygons construction.
+ * Polygons are built using the outline edges as
+ * representative of boundaries, the orientation of these lines are
+ * meaningful. However, we want to allow the user to specify its
+ * outline loops without having to take care of this orientation. This
+ * class is devoted to correct mis-oriented loops.<p>
+
+ * <p>Orientation is computed assuming the piecewise polygon is finite,
+ * i.e. the outermost loops have their exterior side facing points at
+ * infinity, and hence are oriented counter-clockwise. The orientation of
+ * internal loops is computed as the reverse of the orientation of
+ * their immediate surrounding loop.</p>
+ */
+class NestedLoops {
+
+    /** Boundary loop. */
+    private Cartesian2D[] loop;
+
+    /** Surrounded loops. */
+    private List<NestedLoops> surrounded;
+
+    /** Polygon enclosing a finite region. */
+    private Region<Euclidean2D> polygon;
+
+    /** Indicator for original loop orientation. */
+    private boolean originalIsClockwise;
+
+    /** Tolerance below which points are considered identical. */
+    private final double tolerance;
+
+    /** Simple Constructor.
+     * <p>Build an empty tree of nested loops. This instance will become
+     * the root node of a complete tree, it is not associated with any
+     * loop by itself, the outermost loops are in the root tree child
+     * nodes.</p>
+     * @param tolerance tolerance below which points are considered identical
+     */
+    NestedLoops(final double tolerance) {
+        this.surrounded = new ArrayList<>();
+        this.tolerance  = tolerance;
+    }
+
+    /** Constructor.
+     * <p>Build a tree node with neither parent nor children</p>
+     * @param loop boundary loop (will be reversed in place if needed)
+     * @param tolerance tolerance below which points are considered identical
+     * @exception IllegalArgumentException if an outline has an open boundary loop
+     */
+    private NestedLoops(final Cartesian2D[] loop, final double tolerance)
+        throws IllegalArgumentException {
+
+        if (loop[0] == null) {
+            throw new IllegalArgumentException("An outline boundary loop is open");
+        }
+
+        this.loop       = loop;
+        this.surrounded = new ArrayList<>();
+        this.tolerance  = tolerance;
+
+        // build the polygon defined by the loop
+        final ArrayList<SubHyperplane<Euclidean2D>> edges = new ArrayList<>();
+        Cartesian2D current = loop[loop.length - 1];
+        for (int i = 0; i < loop.length; ++i) {
+            final Cartesian2D previous = current;
+            current = loop[i];
+            final Line   line   = new Line(previous, current, tolerance);
+            final IntervalsSet region =
+                new IntervalsSet(line.toSubSpace((Point<Euclidean2D>) previous).getX(),
+                                 line.toSubSpace((Point<Euclidean2D>) current).getX(),
+                                 tolerance);
+            edges.add(new SubLine(line, region));
+        }
+        polygon = new PolygonsSet(edges, tolerance);
+
+        // ensure the polygon encloses a finite region of the plane
+        if (Double.isInfinite(polygon.getSize())) {
+            polygon = new RegionFactory<Euclidean2D>().getComplement(polygon);
+            originalIsClockwise = false;
+        } else {
+            originalIsClockwise = true;
+        }
+
+    }
+
+    /** Add a loop in a tree.
+     * @param bLoop boundary loop (will be reversed in place if needed)
+     * @exception IllegalArgumentException if an outline has crossing
+     * boundary loops or open boundary loops
+     */
+    public void add(final Cartesian2D[] bLoop) throws IllegalArgumentException {
+        add(new NestedLoops(bLoop, tolerance));
+    }
+
+    /** Add a loop in a tree.
+     * @param node boundary loop (will be reversed in place if needed)
+     * @exception IllegalArgumentException if an outline has boundary
+     * loops that cross each other
+     */
+    private void add(final NestedLoops node) throws IllegalArgumentException {
+
+        // check if we can go deeper in the tree
+        for (final NestedLoops child : surrounded) {
+            if (child.polygon.contains(node.polygon)) {
+                child.add(node);
+                return;
+            }
+        }
+
+        // check if we can absorb some of the instance children
+        for (final Iterator<NestedLoops> iterator = surrounded.iterator(); iterator.hasNext();) {
+            final NestedLoops child = iterator.next();
+            if (node.polygon.contains(child.polygon)) {
+                node.surrounded.add(child);
+                iterator.remove();
+            }
+        }
+
+        // we should be separate from the remaining children
+        RegionFactory<Euclidean2D> factory = new RegionFactory<>();
+        for (final NestedLoops child : surrounded) {
+            if (!factory.intersection(node.polygon, child.polygon).isEmpty()) {
+                throw new IllegalArgumentException("Some outline boundary loops cross each other");
+            }
+        }
+
+        surrounded.add(node);
+
+    }
+
+    /** Correct the orientation of the loops contained in the tree.
+     * <p>This is this method that really inverts the loops that where
+     * provided through the {@link #add(Cartesian2D[]) add} method if
+     * they are mis-oriented</p>
+     */
+    public void correctOrientation() {
+        for (NestedLoops child : surrounded) {
+            child.setClockWise(true);
+        }
+    }
+
+    /** Set the loop orientation.
+     * @param clockwise if true, the loop should be set to clockwise
+     * orientation
+     */
+    private void setClockWise(final boolean clockwise) {
+
+        if (originalIsClockwise ^ clockwise) {
+            // we need to inverse the original loop
+            int min = -1;
+            int max = loop.length;
+            while (++min < --max) {
+                final Cartesian2D tmp = loop[min];
+                loop[min] = loop[max];
+                loop[max] = tmp;
+            }
+        }
+
+        // go deeper in the tree
+        for (final NestedLoops child : surrounded) {
+            child.setClockWise(!clockwise);
+        }
+
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolygonsSet.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolygonsSet.java
new file mode 100644
index 0000000..290daca
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/PolygonsSet.java
@@ -0,0 +1,1105 @@
+/*
+ * 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.geometry.euclidean.twod;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.commons.geometry.euclidean.oned.Euclidean1D;
+import org.apache.commons.geometry.euclidean.oned.Interval;
+import org.apache.commons.geometry.euclidean.oned.IntervalsSet;
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.partitioning.AbstractRegion;
+import org.apache.commons.geometry.core.partitioning.AbstractSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.BSPTree;
+import org.apache.commons.geometry.core.partitioning.BSPTreeVisitor;
+import org.apache.commons.geometry.core.partitioning.BoundaryAttribute;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Side;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.euclidean.oned.Cartesian1D;
+import org.apache.commons.numbers.core.Precision;
+
+/** This class represents a 2D region: a set of polygons.
+ */
+public class PolygonsSet extends AbstractRegion<Euclidean2D, Euclidean1D> {
+
+    /** Vertices organized as boundary loops. */
+    private Cartesian2D[][] vertices;
+
+    /** Build a polygons set representing the whole plane.
+     * @param tolerance tolerance below which points are considered identical
+     */
+    public PolygonsSet(final double tolerance) {
+        super(tolerance);
+    }
+
+    /** Build a polygons set from a BSP tree.
+     * <p>The leaf nodes of the BSP tree <em>must</em> have a
+     * {@code Boolean} attribute representing the inside status of
+     * the corresponding cell (true for inside cells, false for outside
+     * cells). In order to avoid building too many small objects, it is
+     * recommended to use the predefined constants
+     * {@code Boolean.TRUE} and {@code Boolean.FALSE}</p>
+     * <p>
+     * This constructor is aimed at expert use, as building the tree may
+     * be a difficult task. It is not intended for general use and for
+     * performances reasons does not check thoroughly its input, as this would
+     * require walking the full tree each time. Failing to provide a tree with
+     * the proper attributes, <em>will</em> therefore generate problems like
+     * {@link NullPointerException} or {@link ClassCastException} only later on.
+     * This limitation is known and explains why this constructor is for expert
+     * use only. The caller does have the responsibility to provided correct arguments.
+     * </p>
+     * @param tree inside/outside BSP tree representing the region
+     * @param tolerance tolerance below which points are considered identical
+     */
+    public PolygonsSet(final BSPTree<Euclidean2D> tree, final double tolerance) {
+        super(tree, tolerance);
+    }
+
+    /** Build a polygons set from a Boundary REPresentation (B-rep).
+     * <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
+     * its plus side.</p>
+     * <p>The boundary elements can be in any order, and can form
+     * several non-connected sets (like for example polygons with holes
+     * or a set of disjoint polygons considered as a whole). In
+     * fact, the elements do not even need to be connected together
+     * (their topological connections are not used here). However, if the
+     * boundary does not really separate an inside open from an outside
+     * open (open having here its topological meaning), then subsequent
+     * calls to the {@link
+     * org.apache.commons.geometry.core.partitioning.Region#checkPoint(org.apache.commons.geometry.core.Point)
+     * checkPoint} method will not be meaningful anymore.</p>
+     * <p>If the boundary is empty, the region will represent the whole
+     * space.</p>
+     * @param boundary collection of boundary elements, as a
+     * collection of {@link SubHyperplane SubHyperplane} objects
+     * @param tolerance tolerance below which points are considered identical
+     */
+    public PolygonsSet(final Collection<SubHyperplane<Euclidean2D>> boundary, final double tolerance) {
+        super(boundary, tolerance);
+    }
+
+    /** Build a parallellepipedic box.
+     * @param xMin low bound along the x direction
+     * @param xMax high bound along the x direction
+     * @param yMin low bound along the y direction
+     * @param yMax high bound along the y direction
+     * @param tolerance tolerance below which points are considered identical
+     */
+    public PolygonsSet(final double xMin, final double xMax,
+                       final double yMin, final double yMax,
+                       final double tolerance) {
+        super(boxBoundary(xMin, xMax, yMin, yMax, tolerance), tolerance);
+    }
+
+    /** Build a polygon from a simple list of vertices.
+     * <p>The boundary is provided as a list of points considering to
+     * represent the vertices of a simple loop. The interior part of the
+     * region is on the left side of this path and the exterior is on its
+     * right side.</p>
+     * <p>This constructor does not handle polygons with a boundary
+     * forming several disconnected paths (such as polygons with holes).</p>
+     * <p>For cases where this simple constructor applies, it is expected to
+     * be numerically more robust than the {@link #PolygonsSet(Collection,double) general
+     * constructor} using {@link SubHyperplane subhyperplanes}.</p>
+     * <p>If the list is empty, the region will represent the whole
+     * space.</p>
+     * <p>
+     * Polygons with thin pikes or dents are inherently difficult to handle because
+     * they involve lines with almost opposite directions at some vertices. Polygons
+     * whose vertices come from some physical measurement with noise are also
+     * difficult because an edge that should be straight may be broken in lots of
+     * different pieces with almost equal directions. In both cases, computing the
+     * lines intersections is not numerically robust due to the almost 0 or almost
+     * &pi; angle. Such cases need to carefully adjust the {@code hyperplaneThickness}
+     * parameter. A too small value would often lead to completely wrong polygons
+     * with large area wrongly identified as inside or outside. Large values are
+     * often much safer. As a rule of thumb, a value slightly below the size of the
+     * most accurate detail needed is a good value for the {@code hyperplaneThickness}
+     * parameter.
+     * </p>
+     * @param hyperplaneThickness tolerance below which points are considered to
+     * belong to the hyperplane (which is therefore more a slab)
+     * @param vertices vertices of the simple loop boundary
+     */
+    public PolygonsSet(final double hyperplaneThickness, final Cartesian2D ... vertices) {
+        super(verticesToTree(hyperplaneThickness, vertices), hyperplaneThickness);
+    }
+
+    /** Create a list of hyperplanes representing the boundary of a box.
+     * @param xMin low bound along the x direction
+     * @param xMax high bound along the x direction
+     * @param yMin low bound along the y direction
+     * @param yMax high bound along the y direction
+     * @param tolerance tolerance below which points are considered identical
+     * @return boundary of the box
+     */
+    private static Line[] boxBoundary(final double xMin, final double xMax,
+                                      final double yMin, final double yMax,
+                                      final double tolerance) {
+        if ((xMin >= xMax - tolerance) || (yMin >= yMax - tolerance)) {
+            // too thin box, build an empty polygons set
+            return null;
+        }
+        final Cartesian2D minMin = new Cartesian2D(xMin, yMin);
+        final Cartesian2D minMax = new Cartesian2D(xMin, yMax);
+        final Cartesian2D maxMin = new Cartesian2D(xMax, yMin);
+        final Cartesian2D maxMax = new Cartesian2D(xMax, yMax);
+        return new Line[] {
+            new Line(minMin, maxMin, tolerance),
+            new Line(maxMin, maxMax, tolerance),
+            new Line(maxMax, minMax, tolerance),
+            new Line(minMax, minMin, tolerance)
+        };
+    }
+
+    /** Build the BSP tree of a polygons set from a simple list of vertices.
+     * <p>The boundary is provided as a list of points considering to
+     * represent the vertices of a simple loop. The interior part of the
+     * region is on the left side of this path and the exterior is on its
+     * right side.</p>
+     * <p>This constructor does not handle polygons with a boundary
+     * forming several disconnected paths (such as polygons with holes).</p>
+     * <p>For cases where this simple constructor applies, it is expected to
+     * be numerically more robust than the {@link #PolygonsSet(Collection,double) general
+     * constructor} using {@link SubHyperplane subhyperplanes}.</p>
+     * @param hyperplaneThickness tolerance below which points are consider to
+     * belong to the hyperplane (which is therefore more a slab)
+     * @param vertices vertices of the simple loop boundary
+     * @return the BSP tree of the input vertices
+     */
+    private static BSPTree<Euclidean2D> verticesToTree(final double hyperplaneThickness,
+                                                       final Cartesian2D ... vertices) {
+
+        final int n = vertices.length;
+        if (n == 0) {
+            // the tree represents the whole space
+            return new BSPTree<>(Boolean.TRUE);
+        }
+
+        // build the vertices
+        final Vertex[] vArray = new Vertex[n];
+        for (int i = 0; i < n; ++i) {
+            vArray[i] = new Vertex(vertices[i]);
+        }
+
+        // build the edges
+        List<Edge> edges = new ArrayList<>(n);
+        for (int i = 0; i < n; ++i) {
+
+            // get the endpoints of the edge
+            final Vertex start = vArray[i];
+            final Vertex end   = vArray[(i + 1) % n];
+
+            // get the line supporting the edge, taking care not to recreate it
+            // if it was already created earlier due to another edge being aligned
+            // with the current one
+            Line line = start.sharedLineWith(end);
+            if (line == null) {
+                line = new Line(start.getLocation(), end.getLocation(), hyperplaneThickness);
+            }
+
+            // create the edge and store it
+            edges.add(new Edge(start, end, line));
+
+            // check if another vertex also happens to be on this line
+            for (final Vertex vertex : vArray) {
+                if (vertex != start && vertex != end &&
+                    Math.abs(line.getOffset((Point<Euclidean2D>) vertex.getLocation())) <= hyperplaneThickness) {
+                    vertex.bindWith(line);
+                }
+            }
+
+        }
+
+        // build the tree top-down
+        final BSPTree<Euclidean2D> tree = new BSPTree<>();
+        insertEdges(hyperplaneThickness, tree, edges);
+
+        return tree;
+
+    }
+
+    /** Recursively build a tree by inserting cut sub-hyperplanes.
+     * @param hyperplaneThickness tolerance below which points are consider to
+     * belong to the hyperplane (which is therefore more a slab)
+     * @param node current tree node (it is a leaf node at the beginning
+     * of the call)
+     * @param edges list of edges to insert in the cell defined by this node
+     * (excluding edges not belonging to the cell defined by this node)
+     */
+    private static void insertEdges(final double hyperplaneThickness,
+                                    final BSPTree<Euclidean2D> node,
+                                    final List<Edge> edges) {
+
+        // find an edge with an hyperplane that can be inserted in the node
+        int index = 0;
+        Edge inserted =null;
+        while (inserted == null && index < edges.size()) {
+            inserted = edges.get(index++);
+            if (inserted.getNode() == null) {
+                if (node.insertCut(inserted.getLine())) {
+                    inserted.setNode(node);
+                } else {
+                    inserted = null;
+                }
+            } else {
+                inserted = null;
+            }
+        }
+
+        if (inserted == null) {
+            // no suitable edge was found, the node remains a leaf node
+            // we need to set its inside/outside boolean indicator
+            final BSPTree<Euclidean2D> parent = node.getParent();
+            if (parent == null || node == parent.getMinus()) {
+                node.setAttribute(Boolean.TRUE);
+            } else {
+                node.setAttribute(Boolean.FALSE);
+            }
+            return;
+        }
+
+        // we have split the node by inserting an edge as a cut sub-hyperplane
+        // distribute the remaining edges in the two sub-trees
+        final List<Edge> plusList  = new ArrayList<>();
+        final List<Edge> minusList = new ArrayList<>();
+        for (final Edge edge : edges) {
+            if (edge != inserted) {
+                final double startOffset = inserted.getLine().getOffset((Point<Euclidean2D>) edge.getStart().getLocation());
+                final double endOffset   = inserted.getLine().getOffset((Point<Euclidean2D>) edge.getEnd().getLocation());
+                Side startSide = (Math.abs(startOffset) <= hyperplaneThickness) ?
+                                 Side.HYPER : ((startOffset < 0) ? Side.MINUS : Side.PLUS);
+                Side endSide   = (Math.abs(endOffset) <= hyperplaneThickness) ?
+                                 Side.HYPER : ((endOffset < 0) ? Side.MINUS : Side.PLUS);
+                switch (startSide) {
+                    case PLUS:
+                        if (endSide == Side.MINUS) {
+                            // we need to insert a split point on the hyperplane
+                            final Vertex splitPoint = edge.split(inserted.getLine());
+                            minusList.add(splitPoint.getOutgoing());
+                            plusList.add(splitPoint.getIncoming());
+                        } else {
+                            plusList.add(edge);
+                        }
+                        break;
+                    case MINUS:
+                        if (endSide == Side.PLUS) {
+                            // we need to insert a split point on the hyperplane
+                            final Vertex splitPoint = edge.split(inserted.getLine());
+                            minusList.add(splitPoint.getIncoming());
+                            plusList.add(splitPoint.getOutgoing());
+                        } else {
+                            minusList.add(edge);
+                        }
+                        break;
+                    default:
+                        if (endSide == Side.PLUS) {
+                            plusList.add(edge);
+                        } else if (endSide == Side.MINUS) {
+                            minusList.add(edge);
+                        }
+                        break;
+                }
+            }
+        }
+
+        // recurse through lower levels
+        if (!plusList.isEmpty()) {
+            insertEdges(hyperplaneThickness, node.getPlus(),  plusList);
+        } else {
+            node.getPlus().setAttribute(Boolean.FALSE);
+        }
+        if (!minusList.isEmpty()) {
+            insertEdges(hyperplaneThickness, node.getMinus(), minusList);
+        } else {
+            node.getMinus().setAttribute(Boolean.TRUE);
+        }
+
+    }
+
+    /** Internal class for holding vertices while they are processed to build a BSP tree. */
+    private static class Vertex {
+
+        /** Vertex location. */
+        private final Cartesian2D location;
+
+        /** Incoming edge. */
+        private Edge incoming;
+
+        /** Outgoing edge. */
+        private Edge outgoing;
+
+        /** Lines bound with this vertex. */
+        private final List<Line> lines;
+
+        /** Build a non-processed vertex not owned by any node yet.
+         * @param location vertex location
+         */
+        Vertex(final Cartesian2D location) {
+            this.location = location;
+            this.incoming = null;
+            this.outgoing = null;
+            this.lines    = new ArrayList<>();
+        }
+
+        /** Get Vertex location.
+         * @return vertex location
+         */
+        public Cartesian2D getLocation() {
+            return location;
+        }
+
+        /** Bind a line considered to contain this vertex.
+         * @param line line to bind with this vertex
+         */
+        public void bindWith(final Line line) {
+            lines.add(line);
+        }
+
+        /** Get the common line bound with both the instance and another vertex, if any.
+         * <p>
+         * When two vertices are both bound to the same line, this means they are
+         * already handled by node associated with this line, so there is no need
+         * to create a cut hyperplane for them.
+         * </p>
+         * @param vertex other vertex to check instance against
+         * @return line bound with both the instance and another vertex, or null if the
+         * two vertices do not share a line yet
+         */
+        public Line sharedLineWith(final Vertex vertex) {
+            for (final Line line1 : lines) {
+                for (final Line line2 : vertex.lines) {
+                    if (line1 == line2) {
+                        return line1;
+                    }
+                }
+            }
+            return null;
+        }
+
+        /** Set incoming edge.
+         * <p>
+         * The line supporting the incoming edge is automatically bound
+         * with the instance.
+         * </p>
+         * @param incoming incoming edge
+         */
+        public void setIncoming(final Edge incoming) {
+            this.incoming = incoming;
+            bindWith(incoming.getLine());
+        }
+
+        /** Get incoming edge.
+         * @return incoming edge
+         */
+        public Edge getIncoming() {
+            return incoming;
+        }
+
+        /** Set outgoing edge.
+         * <p>
+         * The line supporting the outgoing edge is automatically bound
+         * with the instance.
+         * </p>
+         * @param outgoing outgoing edge
+         */
+        public void setOutgoing(final Edge outgoing) {
+            this.outgoing = outgoing;
+            bindWith(outgoing.getLine());
+        }
+
+        /** Get outgoing edge.
+         * @return outgoing edge
+         */
+        public Edge getOutgoing() {
+            return outgoing;
+        }
+
+    }
+
+    /** Internal class for holding edges while they are processed to build a BSP tree. */
+    private static class Edge {
+
+        /** Start vertex. */
+        private final Vertex start;
+
+        /** End vertex. */
+        private final Vertex end;
+
+        /** Line supporting the edge. */
+        private final Line line;
+
+        /** Node whose cut hyperplane contains this edge. */
+        private BSPTree<Euclidean2D> node;
+
+        /** Build an edge not contained in any node yet.
+         * @param start start vertex
+         * @param end end vertex
+         * @param line line supporting the edge
+         */
+        Edge(final Vertex start, final Vertex end, final Line line) {
+
+            this.start = start;
+            this.end   = end;
+            this.line  = line;
+            this.node  = null;
+
+            // connect the vertices back to the edge
+            start.setOutgoing(this);
+            end.setIncoming(this);
+
+        }
+
+        /** Get start vertex.
+         * @return start vertex
+         */
+        public Vertex getStart() {
+            return start;
+        }
+
+        /** Get end vertex.
+         * @return end vertex
+         */
+        public Vertex getEnd() {
+            return end;
+        }
+
+        /** Get the line supporting this edge.
+         * @return line supporting this edge
+         */
+        public Line getLine() {
+            return line;
+        }
+
+        /** Set the node whose cut hyperplane contains this edge.
+         * @param node node whose cut hyperplane contains this edge
+         */
+        public void setNode(final BSPTree<Euclidean2D> node) {
+            this.node = node;
+        }
+
+        /** Get the node whose cut hyperplane contains this edge.
+         * @return node whose cut hyperplane contains this edge
+         * (null if edge has not yet been inserted into the BSP tree)
+         */
+        public BSPTree<Euclidean2D> getNode() {
+            return node;
+        }
+
+        /** Split the edge.
+         * <p>
+         * Once split, this edge is not referenced anymore by the vertices,
+         * it is replaced by the two half-edges and an intermediate splitting
+         * vertex is introduced to connect these two halves.
+         * </p>
+         * @param splitLine line splitting the edge in two halves
+         * @return split vertex (its incoming and outgoing edges are the two halves)
+         */
+        public Vertex split(final Line splitLine) {
+            final Vertex splitVertex = new Vertex(line.intersection(splitLine));
+            splitVertex.bindWith(splitLine);
+            final Edge startHalf = new Edge(start, splitVertex, line);
+            final Edge endHalf   = new Edge(splitVertex, end, line);
+            startHalf.node = node;
+            endHalf.node   = node;
+            return splitVertex;
+        }
+
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public PolygonsSet buildNew(final BSPTree<Euclidean2D> tree) {
+        return new PolygonsSet(tree, getTolerance());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void computeGeometricalProperties() {
+
+        final Cartesian2D[][] v = getVertices();
+
+        if (v.length == 0) {
+            final BSPTree<Euclidean2D> tree = getTree(false);
+            if (tree.getCut() == null && (Boolean) tree.getAttribute()) {
+                // the instance covers the whole space
+                setSize(Double.POSITIVE_INFINITY);
+                setBarycenter((Point<Euclidean2D>) Cartesian2D.NaN);
+            } else {
+                setSize(0);
+                setBarycenter((Point<Euclidean2D>) Cartesian2D.NaN);
+            }
+        } else if (v[0][0] == null) {
+            // there is at least one open-loop: the polygon is infinite
+            setSize(Double.POSITIVE_INFINITY);
+            setBarycenter((Point<Euclidean2D>) Cartesian2D.NaN);
+        } else {
+            // all loops are closed, we compute some integrals around the shape
+
+            double sum  = 0;
+            double sumX = 0;
+            double sumY = 0;
+
+            for (Cartesian2D[] loop : v) {
+                double x1 = loop[loop.length - 1].getX();
+                double y1 = loop[loop.length - 1].getY();
+                for (final Cartesian2D point : loop) {
+                    final double x0 = x1;
+                    final double y0 = y1;
+                    x1 = point.getX();
+                    y1 = point.getY();
+                    final double factor = x0 * y1 - y0 * x1;
+                    sum  += factor;
+                    sumX += factor * (x0 + x1);
+                    sumY += factor * (y0 + y1);
+                }
+            }
+
+            if (sum < 0) {
+                // the polygon as a finite outside surrounded by an infinite inside
+                setSize(Double.POSITIVE_INFINITY);
+                setBarycenter((Point<Euclidean2D>) Cartesian2D.NaN);
+            } else {
+                setSize(sum / 2);
+                setBarycenter((Point<Euclidean2D>) new Cartesian2D(sumX / (3 * sum), sumY / (3 * sum)));
+            }
+
+        }
+
+    }
+
+    /** Get the vertices of the polygon.
+     * <p>The polygon boundary can be represented as an array of loops,
+     * each loop being itself an array of vertices.</p>
+     * <p>In order to identify open loops which start and end by
+     * infinite edges, the open loops arrays start with a null point. In
+     * this case, the first non null point and the last point of the
+     * array do not represent real vertices, they are dummy points
+     * intended only to get the direction of the first and last edge. An
+     * open loop consisting of a single infinite line will therefore be
+     * represented by a three elements array with one null point
+     * followed by two dummy points. The open loops are always the first
+     * ones in the loops array.</p>
+     * <p>If the polygon has no boundary at all, a zero length loop
+     * array will be returned.</p>
+     * <p>All line segments in the various loops have the inside of the
+     * region on their left side and the outside on their right side
+     * when moving in the underlying line direction. This means that
+     * closed loops surrounding finite areas obey the direct
+     * trigonometric orientation.</p>
+     * @return vertices of the polygon, organized as oriented boundary
+     * loops with the open loops first (the returned value is guaranteed
+     * to be non-null)
+     */
+    public Cartesian2D[][] getVertices() {
+        if (vertices == null) {
+            if (getTree(false).getCut() == null) {
+                vertices = new Cartesian2D[0][];
+            } else {
+
+                // build the unconnected segments
+                final SegmentsBuilder visitor = new SegmentsBuilder(getTolerance());
+                getTree(true).visit(visitor);
+                final List<ConnectableSegment> segments = visitor.getSegments();
+
+                // connect all segments, using topological criteria first
+                // and using Euclidean distance only as a last resort
+                int pending = segments.size();
+                pending -= naturalFollowerConnections(segments);
+                if (pending > 0) {
+                    pending -= splitEdgeConnections(segments);
+                }
+                if (pending > 0) {
+                    pending -= closeVerticesConnections(segments);
+                }
+
+                // create the segment loops
+                final ArrayList<List<Segment>> loops = new ArrayList<>();
+                for (ConnectableSegment s = getUnprocessed(segments); s != null; s = getUnprocessed(segments)) {
+                    final List<Segment> loop = followLoop(s);
+                    if (loop != null) {
+                        // an open loop is one that has fewer than two segments or has a null
+                        // start point; the case where we have two segments in a closed loop
+                        // (ie, an infinitely thin, degenerate loop) will result in null being
+                        // returned from the followLoops method
+                        if (loop.size() < 2 || loop.get(0).getStart() == null) {
+                            // this is an open loop, we put it on the front
+                            loops.add(0, loop);
+                        } else {
+                            // this is a closed loop, we put it on the back
+                            loops.add(loop);
+                        }
+                    }
+                }
+
+                // transform the loops in an array of arrays of points
+                vertices = new Cartesian2D[loops.size()][];
+                int i = 0;
+
+                for (final List<Segment> loop : loops) {
+                    if (loop.size() < 2 ||
+                        (loop.size() == 2 && loop.get(0).getStart() == null && loop.get(1).getEnd() == null)) {
+                        // single infinite line
+                        final Line line = loop.get(0).getLine();
+                        vertices[i++] = new Cartesian2D[] {
+                            null,
+                            line.toSpace(new Cartesian1D(-Float.MAX_VALUE)),
+                            line.toSpace(new Cartesian1D(+Float.MAX_VALUE))
+                        };
+                    } else if (loop.get(0).getStart() == null) {
+                        // open loop with at least one real point
+                        final Cartesian2D[] array = new Cartesian2D[loop.size() + 2];
+                        int j = 0;
+                        for (Segment segment : loop) {
+
+                            if (j == 0) {
+                                // null point and first dummy point
+                                double x = segment.getLine().toSubSpace(segment.getEnd()).getX();
+                                x -= Math.max(1.0, Math.abs(x / 2));
+                                array[j++] = null;
+                                array[j++] = segment.getLine().toSpace(new Cartesian1D(x));
+                            }
+
+                            if (j < (array.length - 1)) {
+                                // current point
+                                array[j++] = segment.getEnd();
+                            } else if (j == (array.length - 1)) {
+                                // last dummy point
+                                double x = segment.getLine().toSubSpace(segment.getStart()).getX();
+                                x += Math.max(1.0, Math.abs(x / 2));
+                                array[j++] = segment.getLine().toSpace(new Cartesian1D(x));
+                            }
+
+                        }
+                        vertices[i++] = array;
+                    } else {
+                        final Cartesian2D[] array = new Cartesian2D[loop.size()];
+                        int j = 0;
+                        for (Segment segment : loop) {
+                            array[j++] = segment.getStart();
+                        }
+                        vertices[i++] = array;
+                    }
+                }
+
+            }
+        }
+
+        return vertices.clone();
+
+    }
+
+    /** Connect the segments using only natural follower information.
+     * @param segments segments complete segments list
+     * @return number of connections performed
+     */
+    private int naturalFollowerConnections(final List<ConnectableSegment> segments) {
+        int connected = 0;
+        for (final ConnectableSegment segment : segments) {
+            if (segment.getNext() == null) {
+                final BSPTree<Euclidean2D> node = segment.getNode();
+                final BSPTree<Euclidean2D> end  = segment.getEndNode();
+                for (final ConnectableSegment candidateNext : segments) {
+                    if (candidateNext.getPrevious()  == null &&
+                        candidateNext.getNode()      == end &&
+                        candidateNext.getStartNode() == node) {
+                        // connect the two segments
+                        segment.setNext(candidateNext);
+                        candidateNext.setPrevious(segment);
+                        ++connected;
+                        break;
+                    }
+                }
+            }
+        }
+        return connected;
+    }
+
+    /** Connect the segments resulting from a line splitting a straight edge.
+     * @param segments segments complete segments list
+     * @return number of connections performed
+     */
+    private int splitEdgeConnections(final List<ConnectableSegment> segments) {
+        int connected = 0;
+        for (final ConnectableSegment segment : segments) {
+            if (segment.getNext() == null) {
+                final Hyperplane<Euclidean2D> hyperplane = segment.getNode().getCut().getHyperplane();
+                final BSPTree<Euclidean2D> end  = segment.getEndNode();
+                for (final ConnectableSegment candidateNext : segments) {
+                    if (candidateNext.getPrevious()                      == null &&
+                        candidateNext.getNode().getCut().getHyperplane() == hyperplane &&
+                        candidateNext.getStartNode()                     == end) {
+                        // connect the two segments
+                        segment.setNext(candidateNext);
+                        candidateNext.setPrevious(segment);
+                        ++connected;
+                        break;
+                    }
+                }
+            }
+        }
+        return connected;
+    }
+
+    /** Connect the segments using Euclidean distance.
+     * <p>
+     * This connection heuristic should be used last, as it relies
+     * only on a fuzzy distance criterion.
+     * </p>
+     * @param segments segments complete segments list
+     * @return number of connections performed
+     */
+    private int closeVerticesConnections(final List<ConnectableSegment> segments) {
+        int connected = 0;
+        for (final ConnectableSegment segment : segments) {
+            if (segment.getNext() == null && segment.getEnd() != null) {
+                final Cartesian2D end = segment.getEnd();
+                ConnectableSegment selectedNext = null;
+                double min = Double.POSITIVE_INFINITY;
+                for (final ConnectableSegment candidateNext : segments) {
+                    if (candidateNext.getPrevious() == null && candidateNext.getStart() != null) {
+                        final double distance = Cartesian2D.distance(end, candidateNext.getStart());
+                        if (distance < min) {
+                            selectedNext = candidateNext;
+                            min          = distance;
+                        }
+                    }
+                }
+                if (min <= getTolerance()) {
+                    // connect the two segments
+                    segment.setNext(selectedNext);
+                    selectedNext.setPrevious(segment);
+                    ++connected;
+                }
+            }
+        }
+        return connected;
+    }
+
+    /** Get first unprocessed segment from a list.
+     * @param segments segments list
+     * @return first segment that has not been processed yet
+     * or null if all segments have been processed
+     */
+    private ConnectableSegment getUnprocessed(final List<ConnectableSegment> segments) {
+        for (final ConnectableSegment segment : segments) {
+            if (!segment.isProcessed()) {
+                return segment;
+            }
+        }
+        return null;
+    }
+
+    /** Build the loop containing a segment.
+     * <p>
+     * The segment put in the loop will be marked as processed.
+     * </p>
+     * @param defining segment used to define the loop
+     * @return loop containing the segment (may be null if the loop is a
+     * degenerated infinitely thin 2 points loop
+     */
+    private List<Segment> followLoop(final ConnectableSegment defining) {
+
+        final List<Segment> loop = new ArrayList<>();
+        loop.add(defining);
+        defining.setProcessed(true);
+
+        // add segments in connection order
+        ConnectableSegment next = defining.getNext();
+        while (next != defining && next != null) {
+            loop.add(next);
+            next.setProcessed(true);
+            next = next.getNext();
+        }
+
+        if (next == null) {
+            // the loop is open and we have found its end,
+            // we need to find its start too
+            ConnectableSegment previous = defining.getPrevious();
+            while (previous != null) {
+                loop.add(0, previous);
+                previous.setProcessed(true);
+                previous = previous.getPrevious();
+            }
+        }
+
+        // filter out spurious vertices
+        filterSpuriousVertices(loop);
+
+        if (loop.size() == 2 && loop.get(0).getStart() != null) {
+            // this is a degenerated infinitely thin closed loop, we simply ignore it
+            return null;
+        } else {
+            return loop;
+        }
+
+    }
+
+    /** Filter out spurious vertices on straight lines (at machine precision).
+     * @param loop segments loop to filter (will be modified in-place)
+     */
+    private void filterSpuriousVertices(final List<Segment> loop) {
+        // we need at least 2 segments in order for one of the contained vertices
+        // to be unnecessary
+        if (loop.size() > 1) {
+            // Go through the list and compare each segment with the next
+            // one in line. We can remove the shared vertex if the segments
+            // are not infinite and they lie on the same line.
+            for (int i = 0; i < loop.size(); ++i) {
+                final Segment previous = loop.get(i);
+                int j = (i + 1) % loop.size();
+                final Segment next = loop.get(j);
+                if (next != null &&
+                    previous.getStart() != null && next.getEnd() != null &&
+                    Precision.equals(previous.getLine().getAngle(), next.getLine().getAngle(), Precision.EPSILON)) {
+                    // the vertex between the two edges is a spurious one
+                    // replace the two segments by a single one
+                    loop.set(j, new Segment(previous.getStart(), next.getEnd(), previous.getLine()));
+                    loop.remove(i--);
+                }
+            }
+        }
+    }
+
+    /** Private extension of Segment allowing connection. */
+    private static class ConnectableSegment extends Segment {
+
+        /** Node containing segment. */
+        private final BSPTree<Euclidean2D> node;
+
+        /** Node whose intersection with current node defines start point. */
+        private final BSPTree<Euclidean2D> startNode;
+
+        /** Node whose intersection with current node defines end point. */
+        private final BSPTree<Euclidean2D> endNode;
+
+        /** Previous segment. */
+        private ConnectableSegment previous;
+
+        /** Next segment. */
+        private ConnectableSegment next;
+
+        /** Indicator for completely processed segments. */
+        private boolean processed;
+
+        /** Build a segment.
+         * @param start start point of the segment
+         * @param end end point of the segment
+         * @param line line containing the segment
+         * @param node node containing the segment
+         * @param startNode node whose intersection with current node defines start point
+         * @param endNode node whose intersection with current node defines end point
+         */
+        ConnectableSegment(final Cartesian2D start, final Cartesian2D end, final Line line,
+                           final BSPTree<Euclidean2D> node,
+                           final BSPTree<Euclidean2D> startNode,
+                           final BSPTree<Euclidean2D> endNode) {
+            super(start, end, line);
+            this.node      = node;
+            this.startNode = startNode;
+            this.endNode   = endNode;
+            this.previous  = null;
+            this.next      = null;
+            this.processed = false;
+        }
+
+        /** Get the node containing segment.
+         * @return node containing segment
+         */
+        public BSPTree<Euclidean2D> getNode() {
+            return node;
+        }
+
+        /** Get the node whose intersection with current node defines start point.
+         * @return node whose intersection with current node defines start point
+         */
+        public BSPTree<Euclidean2D> getStartNode() {
+            return startNode;
+        }
+
+        /** Get the node whose intersection with current node defines end point.
+         * @return node whose intersection with current node defines end point
+         */
+        public BSPTree<Euclidean2D> getEndNode() {
+            return endNode;
+        }
+
+        /** Get the previous segment.
+         * @return previous segment
+         */
+        public ConnectableSegment getPrevious() {
+            return previous;
+        }
+
+        /** Set the previous segment.
+         * @param previous previous segment
+         */
+        public void setPrevious(final ConnectableSegment previous) {
+            this.previous = previous;
+        }
+
+        /** Get the next segment.
+         * @return next segment
+         */
+        public ConnectableSegment getNext() {
+            return next;
+        }
+
+        /** Set the next segment.
+         * @param next previous segment
+         */
+        public void setNext(final ConnectableSegment next) {
+            this.next = next;
+        }
+
+        /** Set the processed flag.
+         * @param processed processed flag to set
+         */
+        public void setProcessed(final boolean processed) {
+            this.processed = processed;
+        }
+
+        /** Check if the segment has been processed.
+         * @return true if the segment has been processed
+         */
+        public boolean isProcessed() {
+            return processed;
+        }
+
+    }
+
+    /** Visitor building segments. */
+    private static class SegmentsBuilder implements BSPTreeVisitor<Euclidean2D> {
+
+        /** Tolerance for close nodes connection. */
+        private final double tolerance;
+
+        /** Built segments. */
+        private final List<ConnectableSegment> segments;
+
+        /** Simple constructor.
+         * @param tolerance tolerance for close nodes connection
+         */
+        SegmentsBuilder(final double tolerance) {
+            this.tolerance = tolerance;
+            this.segments  = new ArrayList<>();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Order visitOrder(final BSPTree<Euclidean2D> node) {
+            return Order.MINUS_SUB_PLUS;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void visitInternalNode(final BSPTree<Euclidean2D> node) {
+            @SuppressWarnings("unchecked")
+            final BoundaryAttribute<Euclidean2D> attribute = (BoundaryAttribute<Euclidean2D>) node.getAttribute();
+            final Iterable<BSPTree<Euclidean2D>> splitters = attribute.getSplitters();
+            if (attribute.getPlusOutside() != null) {
+                addContribution(attribute.getPlusOutside(), node, splitters, false);
+            }
+            if (attribute.getPlusInside() != null) {
+                addContribution(attribute.getPlusInside(), node, splitters, true);
+            }
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void visitLeafNode(final BSPTree<Euclidean2D> node) {
+        }
+
+        /** Add the contribution of a boundary facet.
+         * @param sub boundary facet
+         * @param node node containing segment
+         * @param splitters splitters for the boundary facet
+         * @param reversed if true, the facet has the inside on its plus side
+         */
+        private void addContribution(final SubHyperplane<Euclidean2D> sub,
+                                     final BSPTree<Euclidean2D> node,
+                                     final Iterable<BSPTree<Euclidean2D>> splitters,
+                                     final boolean reversed) {
+            @SuppressWarnings("unchecked")
+            final AbstractSubHyperplane<Euclidean2D, Euclidean1D> absSub =
+                (AbstractSubHyperplane<Euclidean2D, Euclidean1D>) sub;
+            final Line line      = (Line) sub.getHyperplane();
+            final List<Interval> intervals = ((IntervalsSet) absSub.getRemainingRegion()).asList();
+            for (final Interval i : intervals) {
+
+                // find the 2D points
+                final Cartesian2D startV = Double.isInfinite(i.getInf()) ?
+                                        null : line.toSpace(new Cartesian1D(i.getInf()));
+                final Cartesian2D endV   = Double.isInfinite(i.getSup()) ?
+                                        null : line.toSpace(new Cartesian1D(i.getSup()));
+
+                // recover the connectivity information
+                final BSPTree<Euclidean2D> startN = selectClosest(startV, splitters);
+                final BSPTree<Euclidean2D> endN   = selectClosest(endV, splitters);
+
+                if (reversed) {
+                    segments.add(new ConnectableSegment(endV, startV, line.getReverse(),
+                                                        node, endN, startN));
+                } else {
+                    segments.add(new ConnectableSegment(startV, endV, line,
+                                                        node, startN, endN));
+                }
+
+            }
+        }
+
+        /** Select the node whose cut sub-hyperplane is closest to specified point.
+         * @param point reference point
+         * @param candidates candidate nodes
+         * @return node closest to point, or null if point is null or no node is closer than tolerance
+         */
+        private BSPTree<Euclidean2D> selectClosest(final Cartesian2D point, final Iterable<BSPTree<Euclidean2D>> candidates) {
+            if (point != null) {
+                BSPTree<Euclidean2D> selected = null;
+                double min = Double.POSITIVE_INFINITY;
+
+                for (final BSPTree<Euclidean2D> node : candidates) {
+                    final double distance = Math.abs(node.getCut().getHyperplane().getOffset(point));
+                    if (distance < min) {
+                        selected = node;
+                        min      = distance;
+                    }
+                }
+
+                if (min <= tolerance) {
+                    return selected;
+                }
+            }
+            return null;
+        }
+
+        /** Get the segments.
+         * @return built segments
+         */
+        public List<ConnectableSegment> getSegments() {
+            return segments;
+        }
+
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Segment.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Segment.java
new file mode 100644
index 0000000..41646ed
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Segment.java
@@ -0,0 +1,109 @@
+/*
+ * 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.geometry.euclidean.twod;
+
+import org.apache.commons.geometry.core.Point;
+
+/** Simple container for a two-points segment.
+ */
+public class Segment {
+
+    /** Start point of the segment. */
+    private final Cartesian2D start;
+
+    /** End point of the segment. */
+    private final Cartesian2D end;
+
+    /** Line containing the segment. */
+    private final Line     line;
+
+    /** Build a segment.
+     * @param start start point of the segment
+     * @param end end point of the segment
+     * @param line line containing the segment
+     */
+    public Segment(final Cartesian2D start, final Cartesian2D end, final Line line) {
+        this.start  = start;
+        this.end    = end;
+        this.line   = line;
+    }
+
+    /** Get the start point of the segment.
+     * @return start point of the segment
+     */
+    public Cartesian2D getStart() {
+        return start;
+    }
+
+    /** Get the end point of the segment.
+     * @return end point of the segment
+     */
+    public Cartesian2D getEnd() {
+        return end;
+    }
+
+    /** Get the line containing the segment.
+     * @return line containing the segment
+     */
+    public Line getLine() {
+        return line;
+    }
+
+    /** Calculates the shortest distance from a point to this line segment.
+     * <p>
+     * If the perpendicular extension from the point to the line does not
+     * cross in the bounds of the line segment, the shortest distance to
+     * the two end points will be returned.
+     * </p>
+     *
+     * Algorithm adapted from:
+     * <a href="http://www.codeguru.com/forum/printthread.php?s=cc8cf0596231f9a7dba4da6e77c29db3&t=194400&pp=15&page=1";>
+     * Thread @ Codeguru</a>
+     *
+     * @param p to check
+     * @return distance between the instance and the point
+     */
+    public double distance(final Cartesian2D p) {
+        final double deltaX = end.getX() - start.getX();
+        final double deltaY = end.getY() - start.getY();
+
+        final double r = ((p.getX() - start.getX()) * deltaX + (p.getY() - start.getY()) * deltaY) /
+                         (deltaX * deltaX + deltaY * deltaY);
+
+        // r == 0 => P = startPt
+        // r == 1 => P = endPt
+        // r < 0 => P is on the backward extension of the segment
+        // r > 1 => P is on the forward extension of the segment
+        // 0 < r < 1 => P is on the segment
+
+        // if point isn't on the line segment, just return the shortest distance to the end points
+        if (r < 0 || r > 1) {
+            final double dist1 = getStart().distance((Point<Euclidean2D>) p);
+            final double dist2 = getEnd().distance((Point<Euclidean2D>) p);
+
+            return Math.min(dist1, dist2);
+        }
+        else {
+            // find point on line and see if it is in the line segment
+            final double px = start.getX() + r * deltaX;
+            final double py = start.getY() + r * deltaY;
+
+            final Cartesian2D interPt = new Cartesian2D(px, py);
+            return interPt.distance((Point<Euclidean2D>) p);
+        }
+    }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/SubLine.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/SubLine.java
new file mode 100644
index 0000000..2f71a28
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/SubLine.java
@@ -0,0 +1,198 @@
+/*
+ * 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.geometry.euclidean.twod;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.partitioning.AbstractSubHyperplane;
+import org.apache.commons.geometry.core.partitioning.BSPTree;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.Region;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.core.partitioning.Region.Location;
+import org.apache.commons.geometry.euclidean.oned.Cartesian1D;
+import org.apache.commons.geometry.euclidean.oned.Euclidean1D;
+import org.apache.commons.geometry.euclidean.oned.Interval;
+import org.apache.commons.geometry.euclidean.oned.IntervalsSet;
+import org.apache.commons.geometry.euclidean.oned.OrientedPoint;
+
+/** This class represents a sub-hyperplane for {@link Line}.
+ */
+public class SubLine extends AbstractSubHyperplane<Euclidean2D, Euclidean1D> {
+
+    /** Simple constructor.
+     * @param hyperplane underlying hyperplane
+     * @param remainingRegion remaining region of the hyperplane
+     */
+    public SubLine(final Hyperplane<Euclidean2D> hyperplane,
+                   final Region<Euclidean1D> remainingRegion) {
+        super(hyperplane, remainingRegion);
+    }
+
+    /** Create a sub-line from two endpoints.
+     * @param start start point
+     * @param end end point
+     * @param tolerance tolerance below which points are considered identical
+     */
+    public SubLine(final Cartesian2D start, final Cartesian2D end, final double tolerance) {
+        super(new Line(start, end, tolerance), buildIntervalSet(start, end, tolerance));
+    }
+
+    /** Create a sub-line from a segment.
+     * @param segment single segment forming the sub-line
+     */
+    public SubLine(final Segment segment) {
+        super(segment.getLine(),
+              buildIntervalSet(segment.getStart(), segment.getEnd(), segment.getLine().getTolerance()));
+    }
+
+    /** Get the endpoints of the sub-line.
+     * <p>
+     * A subline may be any arbitrary number of disjoints segments, so the endpoints
+     * are provided as a list of endpoint pairs. Each element of the list represents
+     * one segment, and each segment contains a start point at index 0 and an end point
+     * at index 1. If the sub-line is unbounded in the negative infinity direction,
+     * the start point of the first segment will have infinite coordinates. If the
+     * sub-line is unbounded in the positive infinity direction, the end point of the
+     * last segment will have infinite coordinates. So a sub-line covering the whole
+     * line will contain just one row and both elements of this row will have infinite
+     * coordinates. If the sub-line is empty, the returned list will contain 0 segments.
+     * </p>
+     * @return list of segments endpoints
+     */
+    public List<Segment> getSegments() {
+
+        final Line line = (Line) getHyperplane();
+        final List<Interval> list = ((IntervalsSet) getRemainingRegion()).asList();
+        final List<Segment> segments = new ArrayList<>(list.size());
+
+        for (final Interval interval : list) {
+            final Cartesian2D start = line.toSpace(new Cartesian1D(interval.getInf()));
+            final Cartesian2D end   = line.toSpace(new Cartesian1D(interval.getSup()));
+            segments.add(new Segment(start, end, line));
+        }
+
+        return segments;
+
+    }
+
+    /** Get the intersection of the instance and another sub-line.
+     * <p>
+     * This method is related to the {@link Line#intersection(Line)
+     * intersection} method in the {@link Line Line} class, but in addition
+     * to compute the point along infinite lines, it also checks the point
+     * lies on both sub-line ranges.
+     * </p>
+     * @param subLine other sub-line which may intersect instance
+     * @param includeEndPoints if true, endpoints are considered to belong to
+     * instance (i.e. they are closed sets) and may be returned, otherwise endpoints
+     * are considered to not belong to instance (i.e. they are open sets) and intersection
+     * occurring on endpoints lead to null being returned
+     * @return the intersection point if there is one, null if the sub-lines don't intersect
+     */
+    public Cartesian2D intersection(final SubLine subLine, final boolean includeEndPoints) {
+
+        // retrieve the underlying lines
+        Line line1 = (Line) getHyperplane();
+        Line line2 = (Line) subLine.getHyperplane();
+
+        // compute the intersection on infinite line
+        Cartesian2D v2D = line1.intersection(line2);
+        if (v2D == null) {
+            return null;
+        }
+
+        // check location of point with respect to first sub-line
+        Location loc1 = getRemainingRegion().checkPoint(line1.toSubSpace((Point<Euclidean2D>) v2D));
+
+        // check location of point with respect to second sub-line
+        Location loc2 = subLine.getRemainingRegion().checkPoint(line2.toSubSpace((Point<Euclidean2D>) v2D));
+
+        if (includeEndPoints) {
+            return ((loc1 != Location.OUTSIDE) && (loc2 != Location.OUTSIDE)) ? v2D : null;
+        } else {
+            return ((loc1 == Location.INSIDE) && (loc2 == Location.INSIDE)) ? v2D : null;
+        }
+
+    }
+
+    /** Build an interval set from two points.
+     * @param start start point
+     * @param end end point
+     * @param tolerance tolerance below which points are considered identical
+     * @return an interval set
+     */
+    private static IntervalsSet buildIntervalSet(final Cartesian2D start, final Cartesian2D end, final double tolerance) {
+        final Line line = new Line(start, end, tolerance);
+        return new IntervalsSet(line.toSubSpace(start).getX(),
+                                line.toSubSpace(end).getX(),
+                                tolerance);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected AbstractSubHyperplane<Euclidean2D, Euclidean1D> buildNew(final Hyperplane<Euclidean2D> hyperplane,
+                                                                       final Region<Euclidean1D> remainingRegion) {
+        return new SubLine(hyperplane, remainingRegion);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public SplitSubHyperplane<Euclidean2D> split(final Hyperplane<Euclidean2D> hyperplane) {
+
+        final Line    thisLine  = (Line) getHyperplane();
+        final Line    otherLine = (Line) hyperplane;
+        final Cartesian2D crossing = thisLine.intersection(otherLine);
+        final double tolerance  = thisLine.getTolerance();
+
+        if (crossing == null) {
+            // the lines are parallel
+            final double global = otherLine.getOffset(thisLine);
+            if (global < -tolerance) {
+                return new SplitSubHyperplane<>(null, this);
+            } else if (global > tolerance) {
+                return new SplitSubHyperplane<>(this, null);
+            } else {
+                return new SplitSubHyperplane<>(null, null);
+            }
+        }
+
+        // the lines do intersect
+        final boolean direct = Math.sin(thisLine.getAngle() - otherLine.getAngle()) < 0;
+        final Cartesian1D x      = thisLine.toSubSpace(crossing);
+        final SubHyperplane<Euclidean1D> subPlus  =
+                new OrientedPoint(x, !direct, tolerance).wholeHyperplane();
+        final SubHyperplane<Euclidean1D> subMinus =
+                new OrientedPoint(x,  direct, tolerance).wholeHyperplane();
+
+        final BSPTree<Euclidean1D> splitTree = getRemainingRegion().getTree(false).split(subMinus);
+        final BSPTree<Euclidean1D> plusTree  = getRemainingRegion().isEmpty(splitTree.getPlus()) ?
+                                               new BSPTree<Euclidean1D>(Boolean.FALSE) :
+                                               new BSPTree<>(subPlus, new BSPTree<Euclidean1D>(Boolean.FALSE),
+                                                                        splitTree.getPlus(), null);
+        final BSPTree<Euclidean1D> minusTree = getRemainingRegion().isEmpty(splitTree.getMinus()) ?
+                                               new BSPTree<Euclidean1D>(Boolean.FALSE) :
+                                               new BSPTree<>(subMinus, new BSPTree<Euclidean1D>(Boolean.FALSE),
+                                                                        splitTree.getMinus(), null);
+        return new SplitSubHyperplane<>(new SubLine(thisLine.copySelf(), new IntervalsSet(plusTree, tolerance)),
+                                                   new SubLine(thisLine.copySelf(), new IntervalsSet(minusTree, tolerance)));
+
+    }
+
+}
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
new file mode 100644
index 0000000..42fd4b7
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/Vector2D.java
@@ -0,0 +1,37 @@
+/*
+ * 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.geometry.euclidean.twod;
+
+import org.apache.commons.geometry.core.Vector;
+
+/** This class represents a 2D vector.
+ */
+public abstract class Vector2D implements Vector<Euclidean2D> {
+
+    /** Get the abscissa of the vector.
+     * @return abscissa of the vector
+     * @see Cartesian2D#Cartesian2D(double, double)
+     */
+    public abstract double getX();
+
+    /** Get the ordinate of the vector.
+     * @return ordinate of the vector
+     * @see Cartesian2D#Cartesian2D(double, double)
+     */
+    public abstract double getY();
+
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/package-info.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/package-info.java
new file mode 100644
index 0000000..c5ebdaf
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/twod/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+/**
+ *
+ * <p>
+ * This package provides basic 2D geometry components.
+ * </p>
+ *
+ */
+package org.apache.commons.geometry.euclidean.twod;
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/core/partitioning/CharacterizationTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/core/partitioning/CharacterizationTest.java
new file mode 100644
index 0000000..cbbaff4
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/core/partitioning/CharacterizationTest.java
@@ -0,0 +1,427 @@
+/*
+ * 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.geometry.core.partitioning;
+
+import java.util.Iterator;
+
+import org.apache.commons.geometry.core.partitioning.BSPTree;
+import org.apache.commons.geometry.core.partitioning.Characterization;
+import org.apache.commons.geometry.core.partitioning.NodesSet;
+import org.apache.commons.geometry.euclidean.oned.IntervalsSet;
+import org.apache.commons.geometry.euclidean.twod.Cartesian2D;
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+import org.apache.commons.geometry.euclidean.twod.Line;
+import org.apache.commons.geometry.euclidean.twod.PolygonsSet;
+import org.apache.commons.geometry.euclidean.twod.SubLine;
+import org.junit.Assert;
+import org.junit.Test;
+
+/** Tests for partitioning characterization. This is designed to test code
+ * in commons-geometry-core but is placed here to allow access to the euclidean
+ * spatial primitives.
+ */
+public class CharacterizationTest {
+
+    private static final double TEST_TOLERANCE = 1e-10;
+
+    @Test
+    public void testCharacterize_insideLeaf() {
+        // arrange
+        BSPTree<Euclidean2D> tree = new BSPTree<>(Boolean.TRUE);
+        SubLine sub = buildSubLine(new Cartesian2D(0, -1), new Cartesian2D(0, 1));
+
+        // act
+        Characterization<Euclidean2D> ch = new Characterization<>(tree, sub);
+
+        // assert
+        Assert.assertEquals(true, ch.touchInside());
+        Assert.assertSame(sub, ch.insideTouching());
+        Assert.assertEquals(0, size(ch.getInsideSplitters()));
+
+        Assert.assertEquals(false, ch.touchOutside());
+        Assert.assertEquals(null,  ch.outsideTouching());
+        Assert.assertEquals(0, size(ch.getOutsideSplitters()));
+    }
+
+    @Test
+    public void testCharacterize_outsideLeaf() {
+        // arrange
+        BSPTree<Euclidean2D> tree = new BSPTree<>(Boolean.FALSE);
+        SubLine sub = buildSubLine(new Cartesian2D(0, -1), new Cartesian2D(0, 1));
+
+        // act
+        Characterization<Euclidean2D> ch = new Characterization<>(tree, sub);
+
+        // assert
+        Assert.assertEquals(false, ch.touchInside());
+        Assert.assertSame(null, ch.insideTouching());
+        Assert.assertEquals(0, size(ch.getInsideSplitters()));
+
+        Assert.assertEquals(true, ch.touchOutside());
+        Assert.assertEquals(sub,  ch.outsideTouching());
+        Assert.assertEquals(0, size(ch.getOutsideSplitters()));
+    }
+
+    @Test
+    public void testCharacterize_onPlusSide() {
+        // arrange
+        BSPTree<Euclidean2D> tree = new BSPTree<>(Boolean.TRUE);
+        cut(tree, buildLine(new Cartesian2D(0, 0), new Cartesian2D(1, 0)));
+
+        SubLine sub = buildSubLine(new Cartesian2D(0, -1), new Cartesian2D(0, -2));
+
+        // act
+        Characterization<Euclidean2D> ch = new Characterization<>(tree, sub);
+
+        // assert
+        Assert.assertEquals(false, ch.touchInside());
+        Assert.assertSame(null, ch.insideTouching());
+        Assert.assertEquals(0, size(ch.getInsideSplitters()));
+
+        Assert.assertEquals(true, ch.touchOutside());
+        Assert.assertEquals(sub,  ch.outsideTouching());
+        Assert.assertEquals(0, size(ch.getOutsideSplitters()));
+    }
+
+    @Test
+    public void testCharacterize_onMinusSide() {
+        // arrange
+        BSPTree<Euclidean2D> tree = new BSPTree<>(Boolean.TRUE);
+        cut(tree, buildLine(new Cartesian2D(0, 0), new Cartesian2D(1, 0)));
+
+        SubLine sub = buildSubLine(new Cartesian2D(0, 1), new Cartesian2D(0, 2));
+
+        // act
+        Characterization<Euclidean2D> ch = new Characterization<>(tree, sub);
+
+        // assert
+        Assert.assertEquals(true, ch.touchInside());
+        Assert.assertSame(sub, ch.insideTouching());
+        Assert.assertEquals(0, size(ch.getInsideSplitters()));
+
+        Assert.assertEquals(false, ch.touchOutside());
+        Assert.assertEquals(null,  ch.outsideTouching());
+        Assert.assertEquals(0, size(ch.getOutsideSplitters()));
+    }
+
+    @Test
+    public void testCharacterize_onBothSides() {
+        // arrange
+        BSPTree<Euclidean2D> tree = new BSPTree<>(Boolean.TRUE);
+        cut(tree, buildLine(new Cartesian2D(0, 0), new Cartesian2D(1, 0)));
+
+        SubLine sub = buildSubLine(new Cartesian2D(0, -1), new Cartesian2D(0, 1));
+
+        // act
+        Characterization<Euclidean2D> ch = new Characterization<>(tree, sub);
+
+        // assert
+        Assert.assertEquals(true, ch.touchInside());
+        Assert.assertNotSame(sub, ch.insideTouching());
+
+        SubLine inside = (SubLine) ch.insideTouching();
+        Assert.assertEquals(1, inside.getSegments().size());
+        assertVectorEquals(new Cartesian2D(0, 0), inside.getSegments().get(0).getStart());
+        assertVectorEquals(new Cartesian2D(0, 1), inside.getSegments().get(0).getEnd());
+
+        Assert.assertEquals(1, size(ch.getInsideSplitters()));
+        Iterator<BSPTree<Euclidean2D>> insideSplitterIter = ch.getInsideSplitters().iterator();
+        Assert.assertSame(tree, insideSplitterIter.next());
+
+        Assert.assertEquals(true, ch.touchOutside());
+        Assert.assertNotSame(sub, ch.insideTouching());
+
+        SubLine outside = (SubLine) ch.outsideTouching();
+        Assert.assertEquals(1, outside.getSegments().size());
+        assertVectorEquals(new Cartesian2D(0, -1), outside.getSegments().get(0).getStart());
+        assertVectorEquals(new Cartesian2D(0, 0), outside.getSegments().get(0).getEnd());
+
+        Assert.assertEquals(1, size(ch.getOutsideSplitters()));
+        Iterator<BSPTree<Euclidean2D>> outsideSplitterIter = ch.getOutsideSplitters().iterator();
+        Assert.assertSame(tree, outsideSplitterIter.next());
+    }
+
+    @Test
+    public void testCharacterize_multipleSplits_reunitedOnPlusSide() {
+        // arrange
+        BSPTree<Euclidean2D> tree = new BSPTree<>(Boolean.TRUE);
+        cut(tree, buildLine(new Cartesian2D(0, 0), new Cartesian2D(1, 0)));
+        cut(tree.getMinus(), buildLine(new Cartesian2D(-1, 0), new Cartesian2D(0, 1)));
+
+        SubLine sub = buildSubLine(new Cartesian2D(0, -2), new Cartesian2D(0, 2));
+
+        // act
+        Characterization<Euclidean2D> ch = new Characterization<>(tree, sub);
+
+        // assert
+        Assert.assertEquals(true, ch.touchInside());
+        Assert.assertNotSame(sub, ch.insideTouching());
+
+        SubLine inside = (SubLine) ch.insideTouching();
+        Assert.assertEquals(1, inside.getSegments().size());
+        assertVectorEquals(new Cartesian2D(0, 1), inside.getSegments().get(0).getStart());
+        assertVectorEquals(new Cartesian2D(0, 2), inside.getSegments().get(0).getEnd());
+
+        Assert.assertEquals(2, size(ch.getInsideSplitters()));
+        Iterator<BSPTree<Euclidean2D>> insideSplitterIter = ch.getInsideSplitters().iterator();
+        Assert.assertSame(tree, insideSplitterIter.next());
+        Assert.assertSame(tree.getMinus(), insideSplitterIter.next());
+
+        Assert.assertEquals(true, ch.touchOutside());
+        Assert.assertNotSame(sub, ch.insideTouching());
+
+        SubLine outside = (SubLine) ch.outsideTouching();
+        Assert.assertEquals(1, outside.getSegments().size());
+        assertVectorEquals(new Cartesian2D(0, -2), outside.getSegments().get(0).getStart());
+        assertVectorEquals(new Cartesian2D(0, 1), outside.getSegments().get(0).getEnd());
+
+        Assert.assertEquals(2, size(ch.getOutsideSplitters()));
+        Iterator<BSPTree<Euclidean2D>> outsideSplitterIter = ch.getOutsideSplitters().iterator();
+        Assert.assertSame(tree, outsideSplitterIter.next());
+        Assert.assertSame(tree.getMinus(), outsideSplitterIter.next());
+    }
+
+    @Test
+    public void testCharacterize_multipleSplits_reunitedOnMinusSide() {
+        // arrange
+        BSPTree<Euclidean2D> tree = new BSPTree<>(Boolean.TRUE);
+        cut(tree, buildLine(new Cartesian2D(0, 0), new Cartesian2D(1, 0)));
+        cut(tree.getMinus(), buildLine(new Cartesian2D(-1, 0), new Cartesian2D(0, 1)));
+        cut(tree.getMinus().getPlus(), buildLine(new Cartesian2D(-0.5, 0.5), new Cartesian2D(0, 0)));
+
+        SubLine sub = buildSubLine(new Cartesian2D(0, -2), new Cartesian2D(0, 2));
+
+        // act
+        Characterization<Euclidean2D> ch = new Characterization<>(tree, sub);
+
+        // assert
+        Assert.assertEquals(true, ch.touchInside());
+        Assert.assertNotSame(sub, ch.insideTouching());
+
+        SubLine inside = (SubLine) ch.insideTouching();
+        Assert.assertEquals(1, inside.getSegments().size());
+        assertVectorEquals(new Cartesian2D(0, 0), inside.getSegments().get(0).getStart());
+        assertVectorEquals(new Cartesian2D(0, 2), inside.getSegments().get(0).getEnd());
+
+        Assert.assertEquals(2, size(ch.getInsideSplitters()));
+        Iterator<BSPTree<Euclidean2D>> insideSplitterIter = ch.getInsideSplitters().iterator();
+        Assert.assertSame(tree, insideSplitterIter.next());
+        Assert.assertSame(tree.getMinus(), insideSplitterIter.next());
+
+        Assert.assertEquals(true, ch.touchOutside());
+        Assert.assertNotSame(sub, ch.insideTouching());
+
+        SubLine outside = (SubLine) ch.outsideTouching();
+        Assert.assertEquals(1, outside.getSegments().size());
+        assertVectorEquals(new Cartesian2D(0, -2), outside.getSegments().get(0).getStart());
+        assertVectorEquals(new Cartesian2D(0, 0), outside.getSegments().get(0).getEnd());
+
+        Assert.assertEquals(1, size(ch.getOutsideSplitters()));
+        Iterator<BSPTree<Euclidean2D>> outsideSplitterIter = ch.getOutsideSplitters().iterator();
+        Assert.assertSame(tree, outsideSplitterIter.next());
+    }
+
+    @Test
+    public void testCharacterize_onHyperplane_sameOrientation() {
+        // arrange
+        BSPTree<Euclidean2D> tree = new BSPTree<>(Boolean.TRUE);
+        cut(tree, buildLine(new Cartesian2D(0, 0), new Cartesian2D(1, 0)));
+
+        SubLine sub = buildSubLine(new Cartesian2D(0, 0), new Cartesian2D(1, 0));
+
+        // act
+        Characterization<Euclidean2D> ch = new Characterization<>(tree, sub);
+
+        // assert
+        Assert.assertEquals(true, ch.touchInside());
+        Assert.assertSame(sub, ch.insideTouching());
+        Assert.assertEquals(0, size(ch.getInsideSplitters()));
+
+        Assert.assertEquals(false, ch.touchOutside());
+        Assert.assertEquals(null,  ch.outsideTouching());
+        Assert.assertEquals(0, size(ch.getOutsideSplitters()));
+    }
+
+    @Test
+    public void testCharacterize_onHyperplane_oppositeOrientation() {
+        // arrange
+        BSPTree<Euclidean2D> tree = new BSPTree<>(Boolean.TRUE);
+        cut(tree, buildLine(new Cartesian2D(0, 0), new Cartesian2D(1, 0)));
+
+        SubLine sub = buildSubLine(new Cartesian2D(1, 0), new Cartesian2D(0, 0));
+
+        // act
+        Characterization<Euclidean2D> ch = new Characterization<>(tree, sub);
+
+        // assert
+        Assert.assertEquals(true, ch.touchInside());
+        Assert.assertSame(sub, ch.insideTouching());
+        Assert.assertEquals(0, size(ch.getInsideSplitters()));
+
+        Assert.assertEquals(false, ch.touchOutside());
+        Assert.assertEquals(null,  ch.outsideTouching());
+        Assert.assertEquals(0, size(ch.getOutsideSplitters()));
+    }
+
+    @Test
+    public void testCharacterize_onHyperplane_multipleSplits_sameOrientation() {
+        // arrange
+        BSPTree<Euclidean2D> tree = new BSPTree<>(Boolean.TRUE);
+        cut(tree, buildLine(new Cartesian2D(0, 0), new Cartesian2D(1, 0)));
+        cut(tree.getMinus(), buildLine(new Cartesian2D(-1, 0), new Cartesian2D(0, 1)));
+
+        SubLine sub = buildSubLine(new Cartesian2D(-2, 0), new Cartesian2D(2, 0));
+
+        // act
+        Characterization<Euclidean2D> ch = new Characterization<>(tree, sub);
+
+        // assert
+        Assert.assertEquals(true, ch.touchInside());
+        Assert.assertNotSame(sub, ch.insideTouching());
+
+        SubLine inside = (SubLine) ch.insideTouching();
+        Assert.assertEquals(1, inside.getSegments().size());
+        assertVectorEquals(new Cartesian2D(-2, 0), inside.getSegments().get(0).getStart());
+        assertVectorEquals(new Cartesian2D(-1, 0), inside.getSegments().get(0).getEnd());
+
+        Assert.assertEquals(1, size(ch.getInsideSplitters()));
+        Iterator<BSPTree<Euclidean2D>> insideSplitterIter = ch.getInsideSplitters().iterator();
+        Assert.assertSame(tree.getMinus(), insideSplitterIter.next());
+
+        Assert.assertEquals(true, ch.touchOutside());
+        Assert.assertNotSame(sub, ch.insideTouching());
+
+        SubLine outside = (SubLine) ch.outsideTouching();
+        Assert.assertEquals(1, outside.getSegments().size());
+        assertVectorEquals(new Cartesian2D(-1, 0), outside.getSegments().get(0).getStart());
+        assertVectorEquals(new Cartesian2D(2, 0), outside.getSegments().get(0).getEnd());
+
+        Assert.assertEquals(1, size(ch.getOutsideSplitters()));
+        Iterator<BSPTree<Euclidean2D>> outsideSplitterIter = ch.getOutsideSplitters().iterator();
+        Assert.assertSame(tree.getMinus(), outsideSplitterIter.next());
+    }
+
+    @Test
+    public void testCharacterize_onHyperplane_multipleSplits_oppositeOrientation() {
+        // arrange
+        BSPTree<Euclidean2D> tree = new BSPTree<>(Boolean.TRUE);
+        cut(tree, buildLine(new Cartesian2D(0, 0), new Cartesian2D(1, 0)));
+        cut(tree.getMinus(), buildLine(new Cartesian2D(-1, 0), new Cartesian2D(0, 1)));
+
+        SubLine sub = buildSubLine(new Cartesian2D(2, 0), new Cartesian2D(-2, 0));
+
+        // act
+        Characterization<Euclidean2D> ch = new Characterization<>(tree, sub);
+
+        // assert
+        Assert.assertEquals(true, ch.touchInside());
+        Assert.assertNotSame(sub, ch.insideTouching());
+
+        SubLine inside = (SubLine) ch.insideTouching();
+        Assert.assertEquals(1, inside.getSegments().size());
+        assertVectorEquals(new Cartesian2D(-1, 0), inside.getSegments().get(0).getStart());
+        assertVectorEquals(new Cartesian2D(-2, 0), inside.getSegments().get(0).getEnd());
+
+        Assert.assertEquals(1, size(ch.getInsideSplitters()));
+        Iterator<BSPTree<Euclidean2D>> insideSplitterIter = ch.getInsideSplitters().iterator();
+        Assert.assertSame(tree.getMinus(), insideSplitterIter.next());
+
+        Assert.assertEquals(true, ch.touchOutside());
+        Assert.assertNotSame(sub, ch.insideTouching());
+
+        SubLine outside = (SubLine) ch.outsideTouching();
+        Assert.assertEquals(1, outside.getSegments().size());
+        assertVectorEquals(new Cartesian2D(2, 0), outside.getSegments().get(0).getStart());
+        assertVectorEquals(new Cartesian2D(-1, 0), outside.getSegments().get(0).getEnd());
+
+        Assert.assertEquals(1, size(ch.getOutsideSplitters()));
+        Iterator<BSPTree<Euclidean2D>> outsideSplitterIter = ch.getOutsideSplitters().iterator();
+        Assert.assertSame(tree.getMinus(), outsideSplitterIter.next());
+    }
+
+    @Test
+    public void testCharacterize_onHyperplane_box() {
+        // arrange
+        PolygonsSet poly = new PolygonsSet(0, 1, 0, 1, TEST_TOLERANCE);
+        BSPTree<Euclidean2D> tree = poly.getTree(false);
+
+        SubLine sub = buildSubLine(new Cartesian2D(2, 0), new Cartesian2D(-2, 0));
+
+        // act
+        Characterization<Euclidean2D> ch = new Characterization<>(tree, sub);
+
+        // assert
+        Assert.assertEquals(true, ch.touchInside());
+        Assert.assertNotSame(sub, ch.insideTouching());
+
+        SubLine inside = (SubLine) ch.insideTouching();
+        Assert.assertEquals(1, inside.getSegments().size());
+        assertVectorEquals(new Cartesian2D(1, 0), inside.getSegments().get(0).getStart());
+        assertVectorEquals(new Cartesian2D(0, 0), inside.getSegments().get(0).getEnd());
+
+        Assert.assertEquals(2, size(ch.getInsideSplitters()));
+
+        Assert.assertEquals(true, ch.touchOutside());
+        Assert.assertNotSame(sub, ch.insideTouching());
+
+        SubLine outside = (SubLine) ch.outsideTouching();
+        Assert.assertEquals(2, outside.getSegments().size());
+        assertVectorEquals(new Cartesian2D(2, 0), outside.getSegments().get(0).getStart());
+        assertVectorEquals(new Cartesian2D(1, 0), outside.getSegments().get(0).getEnd());
+        assertVectorEquals(new Cartesian2D(0, 0), outside.getSegments().get(1).getStart());
+        assertVectorEquals(new Cartesian2D(-2, 0), outside.getSegments().get(1).getEnd());
+
+        Assert.assertEquals(2, size(ch.getOutsideSplitters()));
+    }
+
+    private void cut(BSPTree<Euclidean2D> tree, Line line) {
+        if (tree.insertCut(line)) {
+            tree.setAttribute(null);
+            tree.getPlus().setAttribute(Boolean.FALSE);
+            tree.getMinus().setAttribute(Boolean.TRUE);
+        }
+    }
+
+    private int size(NodesSet<Euclidean2D> nodes) {
+        Iterator<BSPTree<Euclidean2D>> it = nodes.iterator();
+
+        int size = 0;
+        while (it.hasNext()) {
+            it.next();
+            ++size;
+        }
+
+        return size;
+    }
+
+    private Line buildLine(Cartesian2D p1, Cartesian2D p2) {
+        return new Line(p1, p2, TEST_TOLERANCE);
+    }
+
+    private SubLine buildSubLine(Cartesian2D start, Cartesian2D end) {
+        Line line = new Line(start, end, TEST_TOLERANCE);
+        double lower = (line.toSubSpace(start)).getX();
+        double upper = (line.toSubSpace(end)).getX();
+        return new SubLine(line, new IntervalsSet(lower, upper, TEST_TOLERANCE));
+    }
+
+    private void assertVectorEquals(Cartesian2D expected, Cartesian2D actual) {
+        String msg = "Expected vector to equal " + expected + " but was " + actual + ";";
+        Assert.assertEquals(msg, expected.getX(), actual.getX(), TEST_TOLERANCE);
+        Assert.assertEquals(msg, expected.getY(), actual.getY(), TEST_TOLERANCE);
+    }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/EuclideanTestUtils.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/EuclideanTestUtils.java
new file mode 100644
index 0000000..e0f4f05
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/EuclideanTestUtils.java
@@ -0,0 +1,376 @@
+/*
+ * 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.geometry.euclidean;
+
+import java.io.IOException;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.core.partitioning.BSPTree;
+import org.apache.commons.geometry.core.partitioning.Hyperplane;
+import org.apache.commons.geometry.core.partitioning.TreeBuilder;
+import org.apache.commons.geometry.core.partitioning.TreeDumper;
+import org.apache.commons.geometry.core.partitioning.TreePrinter;
+import org.apache.commons.geometry.euclidean.oned.Cartesian1D;
+import org.apache.commons.geometry.euclidean.oned.Euclidean1D;
+import org.apache.commons.geometry.euclidean.oned.IntervalsSet;
+import org.apache.commons.geometry.euclidean.oned.OrientedPoint;
+import org.apache.commons.geometry.euclidean.oned.SubOrientedPoint;
+import org.apache.commons.geometry.euclidean.oned.Vector1D;
+import org.apache.commons.geometry.euclidean.threed.Cartesian3D;
+import org.apache.commons.geometry.euclidean.threed.Euclidean3D;
+import org.apache.commons.geometry.euclidean.threed.Plane;
+import org.apache.commons.geometry.euclidean.threed.PolyhedronsSet;
+import org.apache.commons.geometry.euclidean.threed.SubPlane;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.apache.commons.geometry.euclidean.twod.Cartesian2D;
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+import org.apache.commons.geometry.euclidean.twod.Line;
+import org.apache.commons.geometry.euclidean.twod.PolygonsSet;
+import org.apache.commons.geometry.euclidean.twod.SubLine;
+import org.apache.commons.geometry.euclidean.twod.Vector2D;
+import org.junit.Assert;
+
+/** Class containing various euclidean-related test utilities.
+ */
+public class EuclideanTestUtils {
+
+    /** Asserts that corresponding values in the given vectors are equal, using the specified
+     * tolerance value.
+     * @param expected
+     * @param actual
+     * @param tolerance
+     */
+    public static void assertVectorEquals(Vector1D expected, Vector1D actual, double tolerance) {
+        String msg = "Expected vector to equal " + expected + " but was " + actual + ";";
+        Assert.assertEquals(msg, expected.getX(), actual.getX(), tolerance);
+    }
+
+    /** Asserts that corresponding values in the given vectors are equal, using the specified
+     * tolerance value.
+     * @param expected
+     * @param actual
+     * @param tolerance
+     */
+    public static void assertVectorEquals(Vector2D expected, Vector2D actual, double tolerance) {
+        String msg = "Expected vector to equal " + expected + " but was " + actual + ";";
+        Assert.assertEquals(msg, expected.getX(), actual.getX(), tolerance);
+        Assert.assertEquals(msg, expected.getY(), actual.getY(), tolerance);
+    }
+
+    /** Asserts that corresponding values in the given vectors are equal, using the specified
+     * tolerance value.
+     * @param expected
+     * @param actual
+     * @param tolerance
+     */
+    public static void assertVectorEquals(Vector3D expected, Vector3D actual, double tolerance) {
+        String msg = "Expected vector to equal " + expected + " but was " + actual + ";";
+        Assert.assertEquals(msg, expected.getX(), actual.getX(), tolerance);
+        Assert.assertEquals(msg, expected.getY(), actual.getY(), tolerance);
+        Assert.assertEquals(msg, expected.getZ(), actual.getZ(), tolerance);
+    }
+
+    /** Asserts that the given value is positive infinity.
+     * @param value
+     */
+    public static void assertPositiveInfinity(double value) {
+        String msg = "Expected value to be positive infinity but was " + value;
+        Assert.assertTrue(msg, Double.isInfinite(value));
+        Assert.assertTrue(msg, value > 0);
+    }
+
+    /** Asserts that the given value is negative infinity..
+     * @param value
+     */
+    public static void assertNegativeInfinity(double value) {
+        String msg = "Expected value to be negative infinity but was " + value;
+        Assert.assertTrue(msg, Double.isInfinite(value));
+        Assert.assertTrue(msg, value < 0);
+    }
+
+    /** Get a string representation of an {@link IntervalsSet}.
+     * @param intervalsSet region to dump
+     * @return string representation of the region
+     */
+    public static String dump(final IntervalsSet intervalsSet) {
+        final TreeDumper<Euclidean1D> visitor = new TreeDumper<Euclidean1D>("IntervalsSet", intervalsSet.getTolerance()) {
+
+            /** {@inheritDoc} */
+            @Override
+            protected void formatHyperplane(final Hyperplane<Euclidean1D> hyperplane) {
+                final OrientedPoint h = (OrientedPoint) hyperplane;
+                getFormatter().format("%22.15e %b %22.15e",
+                                      h.getLocation().getX(), h.isDirect(), h.getTolerance());
+            }
+
+        };
+        intervalsSet.getTree(false).visit(visitor);
+        return visitor.getDump();
+    }
+
+    /** Get a string representation of a {@link PolygonsSet}.
+     * @param polygonsSet region to dump
+     * @return string representation of the region
+     */
+    public static String dump(final PolygonsSet polygonsSet) {
+        final TreeDumper<Euclidean2D> visitor = new TreeDumper<Euclidean2D>("PolygonsSet", polygonsSet.getTolerance()) {
+
+            /** {@inheritDoc} */
+            @Override
+            protected void formatHyperplane(final Hyperplane<Euclidean2D> hyperplane) {
+                final Line h = (Line) hyperplane;
+                final Cartesian2D p = h.toSpace(Cartesian1D.ZERO);
+                getFormatter().format("%22.15e %22.15e %22.15e %22.15e",
+                                      p.getX(), p.getY(), h.getAngle(), h.getTolerance());
+            }
+
+        };
+        polygonsSet.getTree(false).visit(visitor);
+        return visitor.getDump();
+    }
+
+    /** Get a string representation of a {@link PolyhedronsSet}.
+     * @param polyhedronsSet region to dump
+     * @return string representation of the region
+     */
+    public static String dump(final PolyhedronsSet polyhedronsSet) {
+        final TreeDumper<Euclidean3D> visitor = new TreeDumper<Euclidean3D>("PolyhedronsSet", polyhedronsSet.getTolerance()) {
+
+            /** {@inheritDoc} */
+            @Override
+            protected void formatHyperplane(final Hyperplane<Euclidean3D> hyperplane) {
+                final Plane h = (Plane) hyperplane;
+                final Cartesian3D p = h.toSpace(Cartesian2D.ZERO);
+                getFormatter().format("%22.15e %22.15e %22.15e %22.15e %22.15e %22.15e %22.15e",
+                                      p.getX(), p.getY(), p.getZ(),
+                                      h.getNormal().getX(), h.getNormal().getY(), h.getNormal().getZ(),
+                                      h.getTolerance());
+            }
+
+        };
+        polyhedronsSet.getTree(false).visit(visitor);
+        return visitor.getDump();
+    }
+
+    /** Parse a string representation of an {@link IntervalsSet}.
+     * @param s string to parse
+     * @return parsed region
+     * @exception IOException if the string cannot be read
+     * @exception ParseException if the string cannot be parsed
+     */
+    public static IntervalsSet parseIntervalsSet(final String s)
+        throws IOException, ParseException {
+        final TreeBuilder<Euclidean1D> builder = new TreeBuilder<Euclidean1D>("IntervalsSet", s) {
+
+            /** {@inheritDoc} */
+            @Override
+            public OrientedPoint parseHyperplane()
+                throws IOException, ParseException {
+                return new OrientedPoint(new Cartesian1D(getNumber()), getBoolean(), getNumber());
+            }
+
+        };
+        return new IntervalsSet(builder.getTree(), builder.getTolerance());
+    }
+
+    /** Parse a string representation of a {@link PolygonsSet}.
+     * @param s string to parse
+     * @return parsed region
+     * @exception IOException if the string cannot be read
+     * @exception ParseException if the string cannot be parsed
+     */
+    public static PolygonsSet parsePolygonsSet(final String s)
+        throws IOException, ParseException {
+        final TreeBuilder<Euclidean2D> builder = new TreeBuilder<Euclidean2D>("PolygonsSet", s) {
+
+            /** {@inheritDoc} */
+            @Override
+            public Line parseHyperplane()
+                throws IOException, ParseException {
+                return new Line(new Cartesian2D(getNumber(), getNumber()), getNumber(), getNumber());
+            }
+
+        };
+        return new PolygonsSet(builder.getTree(), builder.getTolerance());
+    }
+
+    /** Parse a string representation of a {@link PolyhedronsSet}.
+     * @param s string to parse
+     * @return parsed region
+     * @exception IOException if the string cannot be read
+     * @exception ParseException if the string cannot be parsed
+     */
+    public static PolyhedronsSet parsePolyhedronsSet(final String s)
+        throws IOException, ParseException {
+        final TreeBuilder<Euclidean3D> builder = new TreeBuilder<Euclidean3D>("PolyhedronsSet", s) {
+
+            /** {@inheritDoc} */
+            @Override
+            public Plane parseHyperplane()
+                throws IOException, ParseException {
+                return new Plane(new Cartesian3D(getNumber(), getNumber(), getNumber()),
+                                 new Cartesian3D(getNumber(), getNumber(), getNumber()),
+                                 getNumber());
+            }
+
+        };
+        return new PolyhedronsSet(builder.getTree(), builder.getTolerance());
+    }
+
+
+
+    /** Prints a string representation of the given 1D {@link BSPTree} to
+     * the console. This is intended for quick debugging of small trees.
+     * @param tree
+     */
+    public static void printTree1D(BSPTree<Euclidean1D> tree) {
+        TreePrinter1D printer = new TreePrinter1D();
+        System.out.println(printer.writeAsString(tree));
+    }
+
+    /** Prints a string representation of the given 2D {@link BSPTree} to
+     * the console. This is intended for quick debugging of small trees.
+     * @param tree
+     */
+    public static void printTree2D(BSPTree<Euclidean2D> tree) {
+        TreePrinter2D printer = new TreePrinter2D();
+        System.out.println(printer.writeAsString(tree));
+    }
+
+    /** Prints a string representation of the given 3D {@link BSPTree} to
+     * the console. This is intended for quick debugging of small trees.
+     * @param tree
+     */
+    public static void printTree3D(BSPTree<Euclidean3D> tree) {
+        TreePrinter3D printer = new TreePrinter3D();
+        System.out.println(printer.writeAsString(tree));
+    }
+
+
+    /** Class for creating string representations of 1D {@link BSPTree}s.
+     */
+    public static class TreePrinter1D extends TreePrinter<Euclidean1D> {
+
+        /** {@inheritDoc} */
+        @Override
+        protected void writeInternalNode(BSPTree<Euclidean1D> node) {
+            SubOrientedPoint cut = (SubOrientedPoint) node.getCut();
+
+            OrientedPoint hyper = (OrientedPoint) cut.getHyperplane();
+            write("cut = { hyperplane: ");
+            if (hyper.isDirect()) {
+                write("[" + hyper.getLocation().getX() + ", inf)");
+            }
+            else {
+                write("(-inf, " + hyper.getLocation().getX() + "]");
+            }
+
+            IntervalsSet remainingRegion = (IntervalsSet) cut.getRemainingRegion();
+            if (remainingRegion != null) {
+                write(", remainingRegion: [");
+
+                boolean isFirst = true;
+                for (double[] interval : remainingRegion) {
+                    if (isFirst) {
+                        isFirst = false;
+                    }
+                    else {
+                        write(", ");
+                    }
+                    write(Arrays.toString(interval));
+                }
+
+                write("]");
+            }
+
+            write("}");
+        }
+    }
+
+    /** Class for creating string representations of 2D {@link BSPTree}s.
+     */
+    public static class TreePrinter2D extends TreePrinter<Euclidean2D> {
+
+        /** {@inheritDoc} */
+        @Override
+        protected void writeInternalNode(BSPTree<Euclidean2D> node) {
+            SubLine cut = (SubLine) node.getCut();
+            Line line = (Line) cut.getHyperplane();
+            IntervalsSet remainingRegion = (IntervalsSet) cut.getRemainingRegion();
+
+            write("cut = { angle: " + Math.toDegrees(line.getAngle()) + ", origin: " + line.toSpace(Cartesian1D.ZERO) + "}");
+            write(", remainingRegion: [");
+
+            boolean isFirst = true;
+            for (double[] interval : remainingRegion) {
+                if (isFirst) {
+                    isFirst = false;
+                }
+                else {
+                    write(", ");
+                }
+                write(Arrays.toString(interval));
+            }
+
+            write("]");
+        }
+    }
+
+    /** Class for creating string representations of 3D {@link BSPTree}s.
+     */
+    public static class TreePrinter3D extends TreePrinter<Euclidean3D> {
+
+        /** {@inheritDoc} */
+        @Override
+        protected void writeInternalNode(BSPTree<Euclidean3D> node) {
+            SubPlane cut = (SubPlane) node.getCut();
+            Plane plane = (Plane) cut.getHyperplane();
+            PolygonsSet polygon = (PolygonsSet) cut.getRemainingRegion();
+
+            write("cut = { normal: " + plane.getNormal() + ", origin: " + plane.getOrigin() + "}");
+            write(", remainingRegion = [");
+
+            boolean isFirst = true;
+            for (Cartesian2D[] loop : polygon.getVertices()) {
+                // convert to 3-space for easier debugging
+                List<Cartesian3D> loop3 = new ArrayList<>();
+                for (Cartesian2D vertex : loop) {
+                    if (vertex != null) {
+                        loop3.add(plane.toSpace(vertex));
+                    }
+                    else {
+                        loop3.add(null);
+                    }
+                }
+
+                if (isFirst) {
+                    isFirst = false;
+                }
+                else {
+                    write(", ");
+                }
+
+                write(loop3.toString());
+            }
+
+            write("]");
+        }
+    }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Cartesian1DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Cartesian1DTest.java
new file mode 100644
index 0000000..2db8ded
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Cartesian1DTest.java
@@ -0,0 +1,385 @@
+/*
+ * 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.geometry.euclidean.oned;
+
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Space;
+import org.apache.commons.geometry.core.Vector;
+import org.apache.commons.numbers.core.Precision;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class Cartesian1DTest {
+
+    private static final double TEST_TOLERANCE = 1e-15;
+
+    @Test
+    public void testConstants() {
+        // act/assert
+        checkVector(Cartesian1D.ZERO, 0.0);
+        checkVector(Cartesian1D.ONE, 1.0);
+        checkVector(Cartesian1D.NaN, Double.NaN);
+        checkVector(Cartesian1D.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY);
+        checkVector(Cartesian1D.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
+    }
+
+    @Test
+    public void testConstructor_simple() {
+        // act/assert
+        checkVector(new Cartesian1D(2), 2);
+        checkVector(new Cartesian1D(-2), -2);
+        checkVector(new Cartesian1D(Math.PI), Math.PI);
+    }
+
+    @Test
+    public void testConstructor_multiplicative() {
+        // act/assert
+        checkVector(new Cartesian1D(2, new Cartesian1D(3)), 6);
+        checkVector(new Cartesian1D(-2, new Cartesian1D(3)), -6);
+    }
+
+    @Test
+    public void testConstructor_linear2() {
+        // act/assert
+        checkVector(new Cartesian1D(
+                2, new Cartesian1D(3),
+                5, new Cartesian1D(7)), 41);
+        checkVector(new Cartesian1D(
+                2, new Cartesian1D(3),
+                -5, new Cartesian1D(7)),-29);
+    }
+
+    @Test
+    public void testConstructor_linear3() {
+        // act/assert
+        checkVector(new Cartesian1D(
+                2, new Cartesian1D(3),
+                5, new Cartesian1D(7),
+                11, new Cartesian1D(13)), 184);
+        checkVector(new Cartesian1D(
+                2, new Cartesian1D(3),
+                5, new Cartesian1D(7),
+                -11, new Cartesian1D(13)), -102);
+    }
+
+    @Test
+    public void testConstructor_linear4() {
+        // act/assert
+        checkVector(new Cartesian1D(
+                2, new Cartesian1D(3),
+                5, new Cartesian1D(7),
+                11, new Cartesian1D(13),
+                17, new Cartesian1D(19)), 507);
+        checkVector(new Cartesian1D(
+                2, new Cartesian1D(3),
+                5, new Cartesian1D(7),
+                11, new Cartesian1D(13),
+                -17, new Cartesian1D(19)), -139);
+    }
+
+    @Test
+    public void testSpace() {
+        // act
+        Space space = new Cartesian1D(1).getSpace();
+
+        // assert
+        Assert.assertEquals(1, space.getDimension());
+    }
+
+    @Test
+    public void testZero() {
+        // act
+        Cartesian1D zero = new Cartesian1D(1).getZero();
+
+        // assert
+        Assert.assertEquals(0, zero.getX(), TEST_TOLERANCE);
+    }
+
+    @Test
+    public void testNorm1() {
+        // act/assert
+        Assert.assertEquals(0.0, Cartesian1D.ZERO.getNorm1(), TEST_TOLERANCE);
+        Assert.assertEquals(6.0, new Cartesian1D(6).getNorm1(), TEST_TOLERANCE);
+        Assert.assertEquals(6.0, new Cartesian1D(-6).getNorm1(), TEST_TOLERANCE);
+    }
+
+    @Test
+    public void testNorm() {
+        // act/assert
+        Assert.assertEquals(0.0, Cartesian1D.ZERO.getNorm(), TEST_TOLERANCE);
+        Assert.assertEquals(3.0, new Cartesian1D(3).getNorm(), TEST_TOLERANCE);
+        Assert.assertEquals(3.0, new Cartesian1D(-3).getNorm(), TEST_TOLERANCE);
+    }
+
+    @Test
+    public void testNormSq() {
+        // act/assert
+        Assert.assertEquals(0.0, new Cartesian1D(0).getNormSq(), TEST_TOLERANCE);
+        Assert.assertEquals(9.0, new Cartesian1D(3).getNormSq(), TEST_TOLERANCE);
+        Assert.assertEquals(9.0, new Cartesian1D(-3).getNormSq(), TEST_TOLERANCE);
+    }
+
+    @Test
+    public void testNormInf() {
+        // act/assert
+        Assert.assertEquals(0.0, Cartesian1D.ZERO.getNormInf(), TEST_TOLERANCE);
+        Assert.assertEquals(3.0, new Cartesian1D(3).getNormInf(), TEST_TOLERANCE);
+        Assert.assertEquals(3.0, new Cartesian1D(-3).getNormInf(), TEST_TOLERANCE);
+    }
+
+    @Test
+    public void testAdd() {
+        // arrange
+        Cartesian1D v1 = new Cartesian1D(1);
+        Cartesian1D v2 = new Cartesian1D(-3);
+
+        // act/assert
+        v1 = v1.add(v2);
+        checkVector(v1, -2);
+
+        checkVector(v2.add(v1), -5);
+        checkVector(v2.add(3, v1), -9);
+    }
+
+    @Test
+    public void testSubtract() {
+        // arrange
+        Cartesian1D v1 = new Cartesian1D(1);
+        Cartesian1D v2 = new Cartesian1D(-3);
+
+        // act/assert
+        v1 = v1.subtract(v2);
+        checkVector(v1, 4);
+
+        checkVector(v2.subtract(v1), -7);
+        checkVector(v2.subtract(3, v1), -15);
+    }
+
+    @Test
+    public void testNormalize() {
+        // act/assert
+        checkVector(new Cartesian1D(1).normalize(), 1);
+        checkVector(new Cartesian1D(-1).normalize(), -1);
+        checkVector(new Cartesian1D(5).normalize(), 1);
+        checkVector(new Cartesian1D(-5).normalize(), -1);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testNormalize_zeroNorm() {
+        // act
+        Cartesian1D.ZERO.normalize();
+    }
+
+    @Test
+    public void testNegate() {
+        // act/assert
+        checkVector(new Cartesian1D(0.1).negate(), -0.1);
+        checkVector(new Cartesian1D(-0.1).negate(), 0.1);
+    }
+
+    @Test
+    public void testScalarMultiply() {
+        // act/assert
+        checkVector(new Cartesian1D(1).scalarMultiply(3), 3);
+        checkVector(new Cartesian1D(1).scalarMultiply(-3), -3);
+
+        checkVector(new Cartesian1D(1.5).scalarMultiply(7), 10.5);
+        checkVector(new Cartesian1D(-1.5).scalarMultiply(7), -10.5);
+    }
+
+    @Test
+    public void testNaN() {
+        // act/assert
+        Assert.assertTrue(new Cartesian1D(Double.NaN).isNaN());
+        Assert.assertFalse(new Cartesian1D(1).isNaN());
+        Assert.assertFalse(new Cartesian1D(Double.NEGATIVE_INFINITY).isNaN());
+    }
+
+    @Test
+    public void testInfinite() {
+        // act/assert
+        Assert.assertTrue(new Cartesian1D(Double.NEGATIVE_INFINITY).isInfinite());
+        Assert.assertTrue(new Cartesian1D(Double.POSITIVE_INFINITY).isInfinite());
+        Assert.assertFalse(new Cartesian1D(1).isInfinite());
+        Assert.assertFalse(new Cartesian1D(Double.NaN).isInfinite());
+    }
+
+    @Test
+    public void testDistance1() {
+        // arrange
+        Cartesian1D v1 = new Cartesian1D(1);
+        Cartesian1D v2 = new Cartesian1D(-4);
+
+        // act/assert
+        Assert.assertEquals(0.0, v1.distance1(v1), TEST_TOLERANCE);
+
+        Assert.assertEquals(5.0, v1.distance1(v2), TEST_TOLERANCE);
+        Assert.assertEquals(v1.subtract(v2).getNorm1(), v1.distance1(v2), TEST_TOLERANCE);
+
+        Assert.assertEquals(0.0, new Cartesian1D(-1).distance1(new Cartesian1D(-1)), TEST_TOLERANCE);
+    }
+
+    @Test
+    public void testDistance() {
+        // arrange
+        Cartesian1D v1 = new Cartesian1D(1);
+        Cartesian1D v2 = new Cartesian1D(-4);
+
+        // act/assert
+        Assert.assertEquals(0.0, v1.distance(v1), TEST_TOLERANCE);
+
+        Assert.assertEquals(5.0, v1.distance(v2), TEST_TOLERANCE);
+        Assert.assertEquals(5.0, v1.distance((Point<Euclidean1D>) v2), TEST_TOLERANCE);
+        Assert.assertEquals(5.0, v1.distance((Vector<Euclidean1D>) v2), TEST_TOLERANCE);
+        Assert.assertEquals(v1.subtract(v2).getNorm(), v1.distance(v2), TEST_TOLERANCE);
+
+        Assert.assertEquals(0.0, new Cartesian1D(-1).distance(new Cartesian1D(-1)), TEST_TOLERANCE);
+    }
+
+    @Test
+    public void testDistance_static() {
+        // arrange
+        Cartesian1D v1 = new Cartesian1D(1);
+        Cartesian1D v2 = new Cartesian1D(-4);
+
+        // act/assert
+        Assert.assertEquals(0.0, Cartesian1D.distance(v1, v1), TEST_TOLERANCE);
+
+        Assert.assertEquals(5.0, Cartesian1D.distance(v1, v2), TEST_TOLERANCE);
+        Assert.assertEquals(v1.subtract(v2).getNorm(), Cartesian1D.distance(v1, v2), TEST_TOLERANCE);
+
+        Assert.assertEquals(0.0, Cartesian1D.distance(new Cartesian1D(-1), new Cartesian1D(-1)), TEST_TOLERANCE);
+    }
+
+    @Test
+    public void testDistanceInf() {
+        // arrange
+        Cartesian1D v1 = new Cartesian1D(1);
+        Cartesian1D v2 = new Cartesian1D(-4);
+
+        // act/assert
+        Assert.assertEquals(0.0, new Cartesian1D(-1).distanceInf(new Cartesian1D(-1)), TEST_TOLERANCE);
+        Assert.assertEquals(5.0, v1.distanceInf(v2), TEST_TOLERANCE);
+
+        Assert.assertEquals(v1.subtract(v2).getNormInf(), v1.distanceInf(v2), TEST_TOLERANCE);
+    }
+
+    @Test
+    public void testDistanceInf_static() {
+        // arrange
+        Cartesian1D v1 = new Cartesian1D(1);
+        Cartesian1D v2 = new Cartesian1D(-4);
+
+        // act/assert
+        Assert.assertEquals(0.0, Cartesian1D.distanceInf(new Cartesian1D(-1), new Cartesian1D(-1)), TEST_TOLERANCE);
+        Assert.assertEquals(5.0, Cartesian1D.distanceInf(v1, v2), TEST_TOLERANCE);
+
+        Assert.assertEquals(v1.subtract(v2).getNormInf(), Cartesian1D.distanceInf(v1, v2), TEST_TOLERANCE);
+    }
+
+    @Test
+    public void testDistanceSq() {
+        // arrange
+        Cartesian1D v1 = new Cartesian1D(1);
+        Cartesian1D v2 = new Cartesian1D(-4);
+
+        // act/assert
+        Assert.assertEquals(0.0, new Cartesian1D(-1).distanceSq(new Cartesian1D(-1)), TEST_TOLERANCE);
+        Assert.assertEquals(25.0, v1.distanceSq(v2), TEST_TOLERANCE);
+
+        Assert.assertEquals(Cartesian1D.distance(v1, v2) * Cartesian1D.distance(v1, v2),
+                            v1.distanceSq(v2), TEST_TOLERANCE);
+    }
+
+    @Test
+    public void testDistanceSq_static() {
+        // arrange
+        Cartesian1D v1 = new Cartesian1D(1);
+        Cartesian1D v2 = new Cartesian1D(-4);
+
+        // act/assert
+        Assert.assertEquals(0.0, Cartesian1D.distanceSq(new Cartesian1D(-1), new Cartesian1D(-1)), TEST_TOLERANCE);
+        Assert.assertEquals(25.0, Cartesian1D.distanceSq(v1, v2), TEST_TOLERANCE);
+
+        Assert.assertEquals(Cartesian1D.distance(v1, v2) * Cartesian1D.distance(v1, v2),
+                            Cartesian1D.distanceSq(v1, v2), TEST_TOLERANCE);
+    }
+
+    @Test
+    public void testDotProduct() {
+        // act/assert
+        Assert.assertEquals(6.0, new Cartesian1D(2).dotProduct(new Cartesian1D(3)), TEST_TOLERANCE);
+        Assert.assertEquals(-6.0, new Cartesian1D(2).dotProduct(new Cartesian1D(-3)), TEST_TOLERANCE);
+    }
+
+    @Test
+    public void testEquals() {
+        // arrange
+        Cartesian1D u1 = new Cartesian1D(1);
+        Cartesian1D u2 = new Cartesian1D(1);
+
+        // act/assert
+        Assert.assertFalse(u1.equals(null));
+        Assert.assertFalse(u1.equals(new Object()));
+
+        Assert.assertTrue(u1.equals(u1));
+        Assert.assertTrue(u1.equals(u2));
+
+        Assert.assertFalse(u1.equals(new Cartesian1D(-1)));
+        Assert.assertFalse(u1.equals(new Cartesian1D(1 + 10 * Precision.EPSILON)));
+
+        Assert.assertTrue(new Cartesian1D(Double.NaN).equals(new Cartesian1D(Double.NaN)));
+    }
+
+    @Test
+    public void testHash() {
+        // arrange
+        Cartesian1D u = new Cartesian1D(1);
+        Cartesian1D v = new Cartesian1D(1 + 10 * Precision.EPSILON);
+
+        // act/assert
+        Assert.assertTrue(u.hashCode() != v.hashCode());
+        Assert.assertEquals(new Cartesian1D(Double.NaN).hashCode(), new Cartesian1D(Double.NaN).hashCode());
+    }
+
+    @Test
+    public void testToString() {
+        // act/assert
+        Assert.assertEquals("{3}", new Cartesian1D(3).toString());
+        Assert.assertEquals("{-3}", new Cartesian1D(-3).toString());
+    }
+
+    @Test
+    public void testToString_numberFormat() {
+        // arrange
+        NumberFormat format = new DecimalFormat("0.000", new DecimalFormatSymbols(Locale.US));
+
+        // act/assert
+        Assert.assertEquals("{-1.000}", new Cartesian1D(-1).toString(format));
+        Assert.assertEquals("{3.142}", new Cartesian1D(Math.PI).toString(format));
+    }
+
+    private void checkVector(Cartesian1D v, double x) {
+        Assert.assertEquals(x, v.getX(), TEST_TOLERANCE);
+    }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Euclidean1DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Euclidean1DTest.java
new file mode 100644
index 0000000..924f02c
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/Euclidean1DTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.geometry.euclidean.oned;
+
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.Space;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class Euclidean1DTest {
+
+    @Test
+    public void testDimension() {
+        Assert.assertEquals(1, Euclidean1D.getInstance().getDimension());
+    }
+
+    @Test(expected=UnsupportedOperationException.class)
+    public void testSubSpace() {
+        Euclidean1D.getInstance().getSubSpace();
+    }
+
+    @Test
+    public void testSerialization() {
+        Space e1 = Euclidean1D.getInstance();
+        Space deserialized = (Space) GeometryTestUtils.serializeAndRecover(e1);
+        Assert.assertTrue(e1 == deserialized);
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/IntervalTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/IntervalTest.java
new file mode 100644
index 0000000..55a9eb0
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/IntervalTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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.geometry.euclidean.oned;
+
+import org.apache.commons.geometry.core.partitioning.Region;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.oned.Interval;
+import org.apache.commons.numbers.core.Precision;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class IntervalTest {
+
+    private static final double TEST_TOLERANCE = 1e-10;
+
+    @Test
+    public void testBasicProperties() {
+        // arrange
+        Interval interval = new Interval(2.3, 5.7);
+
+        // act/assert
+        Assert.assertEquals(3.4, interval.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(4.0, interval.getBarycenter(), TEST_TOLERANCE);
+        Assert.assertEquals(2.3, interval.getInf(), TEST_TOLERANCE);
+        Assert.assertEquals(5.7, interval.getSup(), TEST_TOLERANCE);
+    }
+
+    @Test
+    public void testBasicProperties_negativeValues() {
+        // arrange
+        Interval interval = new Interval(-5.7, -2.3);
+
+        // act/assert
+        Assert.assertEquals(3.4, interval.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(-4.0, interval.getBarycenter(), TEST_TOLERANCE);
+        Assert.assertEquals(-5.7, interval.getInf(), TEST_TOLERANCE);
+        Assert.assertEquals(-2.3, interval.getSup(), TEST_TOLERANCE);
+    }
+
+    // MATH-1256
+    @Test(expected = IllegalArgumentException.class)
+    public void testStrictOrdering() {
+        new Interval(0, -1);
+    }
+
+    @Test
+    public void testCheckPoint() {
+        // arrange
+        Interval interval = new Interval(2.3, 5.7);
+
+        // act/assert
+        Assert.assertEquals(Region.Location.OUTSIDE,  interval.checkPoint(1.2, TEST_TOLERANCE));
+
+        Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(2.2, TEST_TOLERANCE));
+        Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(2.3, TEST_TOLERANCE));
+        Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(2.4, TEST_TOLERANCE));
+
+        Assert.assertEquals(Region.Location.INSIDE,   interval.checkPoint(3.0, TEST_TOLERANCE));
+
+        Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(5.6, TEST_TOLERANCE));
+        Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(5.7, TEST_TOLERANCE));
+        Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(5.8, TEST_TOLERANCE));
+
+        Assert.assertEquals(Region.Location.OUTSIDE,  interval.checkPoint(8.7, TEST_TOLERANCE));
+
+        Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(Double.NEGATIVE_INFINITY, TEST_TOLERANCE));
+        Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(Double.POSITIVE_INFINITY, TEST_TOLERANCE));
+        Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(Double.NaN, TEST_TOLERANCE));
+    }
+
+    @Test
+    public void testCheckPoint_tolerance() {
+        // arrange
+        Interval interval = new Interval(2.3, 5.7);
+
+        // act/assert
+        Assert.assertEquals(Region.Location.OUTSIDE, interval.checkPoint(2.29, 1e-3));
+        Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(2.29, 1e-2));
+        Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(2.29, 1e-1));
+        Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(2.29, 1));
+        Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(2.29, 2));
+
+        Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(4.0, 1e-3));
+        Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(4.0, 1e-2));
+        Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(4.0, 1e-1));
+        Assert.assertEquals(Region.Location.INSIDE, interval.checkPoint(4.0, 1));
+        Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(4.0, 2));
+    }
+
+    @Test
+    public void testInfinite_inf() {
+        // act
+        Interval interval = new Interval(Double.NEGATIVE_INFINITY, 9);
+
+        // assert
+        Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(9.0, TEST_TOLERANCE));
+        Assert.assertEquals(Region.Location.OUTSIDE,  interval.checkPoint(9.4, TEST_TOLERANCE));
+        for (double e = 1.0; e <= 6.0; e += 1.0) {
+            Assert.assertEquals(Region.Location.INSIDE,
+                                interval.checkPoint(-1 * Math.pow(10.0, e), TEST_TOLERANCE));
+        }
+        EuclideanTestUtils.assertPositiveInfinity(interval.getSize());
+        EuclideanTestUtils.assertNegativeInfinity(interval.getInf());
+        Assert.assertEquals(9.0, interval.getSup(), TEST_TOLERANCE);
+    }
+
+    @Test
+    public void testInfinite_sup() {
+        // act
+        Interval interval = new Interval(9.0, Double.POSITIVE_INFINITY);
+
+        // assert
+        Assert.assertEquals(Region.Location.BOUNDARY, interval.checkPoint(9.0, TEST_TOLERANCE));
+        Assert.assertEquals(Region.Location.OUTSIDE,  interval.checkPoint(8.4, TEST_TOLERANCE));
+        for (double e = 1.0; e <= 6.0; e += 1.0) {
+            Assert.assertEquals(Region.Location.INSIDE,
+                                interval.checkPoint(Math.pow(10.0, e), TEST_TOLERANCE));
+        }
+        EuclideanTestUtils.assertPositiveInfinity(interval.getSize());
+        Assert.assertEquals(9.0, interval.getInf(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertPositiveInfinity(interval.getSup());
+    }
+
+    @Test
+    public void testInfinite_infAndSup() {
+        // act
+        Interval interval = new Interval(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
+
+        // assert
+        for (double e = 1.0; e <= 6.0; e += 1.0) {
+            Assert.assertEquals(Region.Location.INSIDE,
+                                interval.checkPoint(Math.pow(10.0, e), TEST_TOLERANCE));
+        }
+        EuclideanTestUtils.assertPositiveInfinity(interval.getSize());
+        EuclideanTestUtils.assertNegativeInfinity(interval.getInf());
+        EuclideanTestUtils.assertPositiveInfinity(interval.getSup());
+    }
+
+    @Test
+    public void testSinglePoint() {
+        // act
+        Interval interval = new Interval(1.0, 1.0);
+
+        // assert
+        Assert.assertEquals(0.0, interval.getSize(), Precision.SAFE_MIN);
+        Assert.assertEquals(1.0, interval.getBarycenter(), Precision.EPSILON);
+    }
+
+    @Test
+    public void testSingleInfinitePoint_positive() {
+        // act
+        Interval interval = new Interval(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
+
+        // assert
+        Assert.assertTrue(Double.isNaN(interval.getSize())); // inf - inf = NaN according to floating point spec
+        EuclideanTestUtils.assertPositiveInfinity(interval.getBarycenter());
+    }
+
+    @Test
+    public void testSingleInfinitePoint_negative() {
+        // act
+        Interval interval = new Interval(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY);
+
+        // assert
+        Assert.assertTrue(Double.isNaN(interval.getSize())); // inf - inf = NaN according to floating point spec
+        EuclideanTestUtils.assertNegativeInfinity(interval.getBarycenter());
+    }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/IntervalsSetTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/IntervalsSetTest.java
new file mode 100644
index 0000000..0ead59c
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/IntervalsSetTest.java
@@ -0,0 +1,586 @@
+/*
+ * 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.geometry.euclidean.oned;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.geometry.core.partitioning.BSPTree;
+import org.apache.commons.geometry.core.partitioning.BoundaryProjection;
+import org.apache.commons.geometry.core.partitioning.Region;
+import org.apache.commons.geometry.core.partitioning.RegionFactory;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.numbers.core.Precision;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class IntervalsSetTest {
+
+    private static final double TEST_TOLERANCE = 1e-15;
+
+    @Test
+    public void testInterval_wholeNumberLine() {
+        // act
+        IntervalsSet set = new IntervalsSet(TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN);
+        EuclideanTestUtils.assertNegativeInfinity(set.getInf());
+        EuclideanTestUtils.assertPositiveInfinity(set.getSup());
+        EuclideanTestUtils.assertPositiveInfinity(set.getSize());
+        Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(Cartesian1D.NaN, (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE);
+
+        BSPTree<Euclidean1D> tree = set.getTree(true);
+        Assert.assertEquals(Boolean.TRUE, tree.getAttribute());
+        Assert.assertNull(tree.getCut());
+        Assert.assertNull(tree.getMinus());
+        Assert.assertNull(tree.getPlus());
+
+        List<Interval> intervals = set.asList();
+        Assert.assertEquals(1, intervals.size());
+        assertInterval(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, intervals.get(0), TEST_TOLERANCE);
+
+        assertLocation(Region.Location.INSIDE, set, Double.NEGATIVE_INFINITY);
+        assertLocation(Region.Location.INSIDE, set, 0.0);
+        assertLocation(Region.Location.INSIDE, set, Double.POSITIVE_INFINITY);
+    }
+
+    @Test
+    public void testInterval_doubleOpenInterval() {
+        // act
+        IntervalsSet set = new IntervalsSet(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN);
+        EuclideanTestUtils.assertNegativeInfinity(set.getInf());
+        EuclideanTestUtils.assertPositiveInfinity(set.getSup());
+        EuclideanTestUtils.assertPositiveInfinity(set.getSize());
+        Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(Cartesian1D.NaN, (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE);
+
+        BSPTree<Euclidean1D> tree = set.getTree(true);
+        Assert.assertEquals(Boolean.TRUE, tree.getAttribute());
+        Assert.assertNull(tree.getCut());
+        Assert.assertNull(tree.getMinus());
+        Assert.assertNull(tree.getPlus());
+
+        List<Interval> intervals = set.asList();
+        Assert.assertEquals(1, intervals.size());
+        assertInterval(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, intervals.get(0), TEST_TOLERANCE);
+
+        assertLocation(Region.Location.INSIDE, set, Double.NEGATIVE_INFINITY);
+        assertLocation(Region.Location.INSIDE, set, 0.0);
+        assertLocation(Region.Location.INSIDE, set, Double.POSITIVE_INFINITY);
+    }
+
+    @Test
+    public void testInterval_openInterval_positive() {
+        // act
+        IntervalsSet set = new IntervalsSet(9.0, Double.POSITIVE_INFINITY, TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN);
+        Assert.assertEquals(9.0, set.getInf(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertPositiveInfinity(set.getSup());
+        EuclideanTestUtils.assertPositiveInfinity(set.getSize());
+        Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(Cartesian1D.NaN, (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE);
+
+        List<Interval> intervals = set.asList();
+        Assert.assertEquals(1, intervals.size());
+        assertInterval(9.0, Double.POSITIVE_INFINITY, intervals.get(0), TEST_TOLERANCE);
+
+        assertLocation(Region.Location.OUTSIDE, set, Double.NEGATIVE_INFINITY);
+        assertLocation(Region.Location.OUTSIDE, set, 0.0);
+        assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16);
+        assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16);
+        assertLocation(Region.Location.INSIDE, set, 10.0);
+        assertLocation(Region.Location.INSIDE, set, Double.POSITIVE_INFINITY);
+    }
+
+    @Test
+    public void testInterval_openInterval_negative() {
+        // act
+        IntervalsSet set = new IntervalsSet(Double.NEGATIVE_INFINITY, 9.0, TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN);
+        EuclideanTestUtils.assertNegativeInfinity(set.getInf());
+        Assert.assertEquals(9.0, set.getSup(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertPositiveInfinity(set.getSize());
+        Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(Cartesian1D.NaN, (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE);
+
+        List<Interval> intervals = set.asList();
+        Assert.assertEquals(1, intervals.size());
+        assertInterval(Double.NEGATIVE_INFINITY, 9.0, intervals.get(0), TEST_TOLERANCE);
+
+        assertLocation(Region.Location.INSIDE, set, Double.NEGATIVE_INFINITY);
+        assertLocation(Region.Location.INSIDE, set, 0.0);
+        assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16);
+        assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16);
+        assertLocation(Region.Location.OUTSIDE, set, 10.0);
+        assertLocation(Region.Location.OUTSIDE, set, Double.POSITIVE_INFINITY);
+    }
+
+    @Test
+    public void testInterval_singleClosedInterval() {
+        // act
+        IntervalsSet set = new IntervalsSet(-1.0, 9.0, TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN);
+        Assert.assertEquals(-1.0, set.getInf(), TEST_TOLERANCE);
+        Assert.assertEquals(9.0, set.getSup(), TEST_TOLERANCE);
+        Assert.assertEquals(10.0, set.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(new Cartesian1D(4.0), (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE);
+
+        List<Interval> intervals = set.asList();
+        Assert.assertEquals(1, intervals.size());
+        assertInterval(-1.0, 9.0, intervals.get(0), TEST_TOLERANCE);
+
+        assertLocation(Region.Location.OUTSIDE, set, Double.NEGATIVE_INFINITY);
+        assertLocation(Region.Location.OUTSIDE, set, -2.0);
+        assertLocation(Region.Location.INSIDE, set, 0.0);
+        assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16);
+        assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16);
+        assertLocation(Region.Location.OUTSIDE, set, 10.0);
+        assertLocation(Region.Location.OUTSIDE, set, Double.POSITIVE_INFINITY);
+    }
+
+    @Test
+    public void testInterval_singlePoint() {
+        // act
+        IntervalsSet set = new IntervalsSet(1.0, 1.0, TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN);
+        Assert.assertEquals(1.0, set.getInf(), TEST_TOLERANCE);
+        Assert.assertEquals(1.0, set.getSup(), TEST_TOLERANCE);
+        Assert.assertEquals(0.0, set.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(new Cartesian1D(1.0), (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE);
+
+        List<Interval> intervals = set.asList();
+        Assert.assertEquals(1, intervals.size());
+        assertInterval(1.0, 1.0, intervals.get(0), TEST_TOLERANCE);
+
+        assertLocation(Region.Location.OUTSIDE, set, Double.NEGATIVE_INFINITY);
+        assertLocation(Region.Location.OUTSIDE, set, 0.0);
+        assertLocation(Region.Location.BOUNDARY, set, 1.0);
+        assertLocation(Region.Location.OUTSIDE, set, 2.0);
+        assertLocation(Region.Location.OUTSIDE, set, Double.POSITIVE_INFINITY);
+    }
+
+    @Test
+    public void testFromBoundaries_wholeNumberLine() {
+        // arrange
+        List<SubHyperplane<Euclidean1D>> boundaries = new ArrayList<>();
+
+        // act
+        IntervalsSet set = new IntervalsSet(boundaries, TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN);
+        EuclideanTestUtils.assertNegativeInfinity(set.getInf());
+        EuclideanTestUtils.assertPositiveInfinity(set.getSup());
+        EuclideanTestUtils.assertPositiveInfinity(set.getSize());
+        Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(Cartesian1D.NaN, (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE);
+
+        BSPTree<Euclidean1D> tree = set.getTree(true);
+        Assert.assertEquals(Boolean.TRUE, tree.getAttribute());
+        Assert.assertNull(tree.getCut());
+        Assert.assertNull(tree.getMinus());
+        Assert.assertNull(tree.getPlus());
+
+        List<Interval> intervals = set.asList();
+        Assert.assertEquals(1, intervals.size());
+        assertInterval(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, intervals.get(0), TEST_TOLERANCE);
+
+        assertLocation(Region.Location.INSIDE, set, Double.NEGATIVE_INFINITY);
+        assertLocation(Region.Location.INSIDE, set, 0.0);
+        assertLocation(Region.Location.INSIDE, set, Double.POSITIVE_INFINITY);
+    }
+
+    @Test
+    public void testFromBoundaries_openInterval_positive() {
+        // arrange
+        List<SubHyperplane<Euclidean1D>> boundaries = new ArrayList<>();
+        boundaries.add(subOrientedPoint(9.0, false));
+
+        // act
+        IntervalsSet set = new IntervalsSet(boundaries, TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN);
+        Assert.assertEquals(9.0, set.getInf(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertPositiveInfinity(set.getSup());
+        EuclideanTestUtils.assertPositiveInfinity(set.getSize());
+        Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(Cartesian1D.NaN, (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE);
+
+        List<Interval> intervals = set.asList();
+        Assert.assertEquals(1, intervals.size());
+        assertInterval(9.0, Double.POSITIVE_INFINITY, intervals.get(0), TEST_TOLERANCE);
+
+        assertLocation(Region.Location.OUTSIDE, set, Double.NEGATIVE_INFINITY);
+        assertLocation(Region.Location.OUTSIDE, set, 0.0);
+        assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16);
+        assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16);
+        assertLocation(Region.Location.INSIDE, set, 10.0);
+        assertLocation(Region.Location.INSIDE, set, Double.POSITIVE_INFINITY);
+    }
+
+    @Test
+    public void testFromBoundaries_openInterval_negative() {
+        // arrange
+        List<SubHyperplane<Euclidean1D>> boundaries = new ArrayList<>();
+        boundaries.add(subOrientedPoint(9.0, true));
+
+        // act
+        IntervalsSet set = new IntervalsSet(boundaries, TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN);
+        EuclideanTestUtils.assertNegativeInfinity(set.getInf());
+        Assert.assertEquals(9.0, set.getSup(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertPositiveInfinity(set.getSize());
+        Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(Cartesian1D.NaN, (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE);
+
+        List<Interval> intervals = set.asList();
+        Assert.assertEquals(1, intervals.size());
+        assertInterval(Double.NEGATIVE_INFINITY, 9.0, intervals.get(0), TEST_TOLERANCE);
+
+        assertLocation(Region.Location.INSIDE, set, Double.NEGATIVE_INFINITY);
+        assertLocation(Region.Location.INSIDE, set, 0.0);
+        assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16);
+        assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16);
+        assertLocation(Region.Location.OUTSIDE, set, 10.0);
+        assertLocation(Region.Location.OUTSIDE, set, Double.POSITIVE_INFINITY);
+    }
+
+    @Test
+    public void testFromBoundaries_singleClosedInterval() {
+        // arrange
+        List<SubHyperplane<Euclidean1D>> boundaries = new ArrayList<>();
+        boundaries.add(subOrientedPoint(-1.0, false));
+        boundaries.add(subOrientedPoint(9.0, true));
+
+        // act
+        IntervalsSet set = new IntervalsSet(boundaries, TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN);
+        Assert.assertEquals(-1.0, set.getInf(), TEST_TOLERANCE);
+        Assert.assertEquals(9.0, set.getSup(), TEST_TOLERANCE);
+        Assert.assertEquals(10.0, set.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(new Cartesian1D(4.0), (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE);
+
+        List<Interval> intervals = set.asList();
+        Assert.assertEquals(1, intervals.size());
+        assertInterval(-1.0, 9.0, intervals.get(0), TEST_TOLERANCE);
+
+        assertLocation(Region.Location.OUTSIDE, set, Double.NEGATIVE_INFINITY);
+        assertLocation(Region.Location.OUTSIDE, set, -2.0);
+        assertLocation(Region.Location.INSIDE, set, 0.0);
+        assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16);
+        assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16);
+        assertLocation(Region.Location.OUTSIDE, set, 10.0);
+        assertLocation(Region.Location.OUTSIDE, set, Double.POSITIVE_INFINITY);
+    }
+
+    @Test
+    public void testFromBoundaries_multipleClosedIntervals() {
+        // arrange
+        List<SubHyperplane<Euclidean1D>> boundaries = new ArrayList<>();
+        boundaries.add(subOrientedPoint(-1.0, false));
+        boundaries.add(subOrientedPoint(2.0, true));
+        boundaries.add(subOrientedPoint(5.0, false));
+        boundaries.add(subOrientedPoint(9.0, true));
+
+        // act
+        IntervalsSet set = new IntervalsSet(boundaries, TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN);
+        Assert.assertEquals(-1.0, set.getInf(), TEST_TOLERANCE);
+        Assert.assertEquals(9.0, set.getSup(), TEST_TOLERANCE);
+        Assert.assertEquals(7.0, set.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(new Cartesian1D(29.5 / 7.0), (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE);
+
+        List<Interval> intervals = set.asList();
+        Assert.assertEquals(2, intervals.size());
+        assertInterval(-1.0, 2.0, intervals.get(0), TEST_TOLERANCE);
+        assertInterval(5.0, 9.0, intervals.get(1), TEST_TOLERANCE);
+
+        assertLocation(Region.Location.OUTSIDE, set, Double.NEGATIVE_INFINITY);
+        assertLocation(Region.Location.OUTSIDE, set, -2.0);
+        assertLocation(Region.Location.INSIDE, set, 0.0);
+        assertLocation(Region.Location.OUTSIDE, set, 3.0);
+        assertLocation(Region.Location.INSIDE, set, 6.0);
+        assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16);
+        assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16);
+        assertLocation(Region.Location.OUTSIDE, set, 10.0);
+        assertLocation(Region.Location.OUTSIDE, set, Double.POSITIVE_INFINITY);
+    }
+
+    @Test
+    public void testFromBoundaries_mixedOpenAndClosedIntervals() {
+        // arrange
+        List<SubHyperplane<Euclidean1D>> boundaries = new ArrayList<>();
+        boundaries.add(subOrientedPoint(-2.0, true));
+        boundaries.add(subOrientedPoint(-1.0, false));
+        boundaries.add(subOrientedPoint(2.0, true));
+        boundaries.add(subOrientedPoint(5.0, false));
+        boundaries.add(subOrientedPoint(9.0, true));
+        boundaries.add(subOrientedPoint(10.0, false));
+
+        // act
+        IntervalsSet set = new IntervalsSet(boundaries, TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, set.getTolerance(), Precision.SAFE_MIN);
+        EuclideanTestUtils.assertNegativeInfinity(set.getInf());
+        EuclideanTestUtils.assertPositiveInfinity(set.getSup());
+        EuclideanTestUtils.assertPositiveInfinity(set.getSize());
+        Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(new Cartesian1D(Double.NaN), (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE);
+
+        List<Interval> intervals = set.asList();
+        Assert.assertEquals(4, intervals.size());
+        assertInterval(Double.NEGATIVE_INFINITY, -2.0, intervals.get(0), TEST_TOLERANCE);
+        assertInterval(-1.0, 2.0, intervals.get(1), TEST_TOLERANCE);
+        assertInterval(5.0, 9.0, intervals.get(2), TEST_TOLERANCE);
+        assertInterval(10.0, Double.POSITIVE_INFINITY, intervals.get(3), TEST_TOLERANCE);
+
+        assertLocation(Region.Location.INSIDE, set, Double.NEGATIVE_INFINITY);
+        assertLocation(Region.Location.INSIDE, set, -3);
+        assertLocation(Region.Location.OUTSIDE, set, -1.5);
+        assertLocation(Region.Location.INSIDE, set, 0.0);
+        assertLocation(Region.Location.OUTSIDE, set, 3.0);
+        assertLocation(Region.Location.INSIDE, set, 6.0);
+        assertLocation(Region.Location.BOUNDARY, set, 9.0 - 1e-16);
+        assertLocation(Region.Location.BOUNDARY, set, 9.0 + 1e-16);
+        assertLocation(Region.Location.OUTSIDE, set, 9.5);
+        assertLocation(Region.Location.INSIDE, set, 11.0);
+        assertLocation(Region.Location.INSIDE, set, Double.POSITIVE_INFINITY);
+    }
+
+    @Test
+    public void testFromBoundaries_intervalEqualToTolerance_onlyFirstBoundaryUsed() {
+        // arrange
+        double tolerance = 1e-3;
+        double first = 1.0;
+        double second = 1.0 + tolerance;
+        List<SubHyperplane<Euclidean1D>> boundaries = new ArrayList<>();
+        boundaries.add(subOrientedPoint(first, true, tolerance));
+        boundaries.add(subOrientedPoint(second, false, tolerance));
+
+        // act
+        IntervalsSet set = new IntervalsSet(boundaries, tolerance);
+
+        // assert
+        Assert.assertEquals(tolerance, set.getTolerance(), Precision.SAFE_MIN);
+        EuclideanTestUtils.assertNegativeInfinity(set.getInf());
+        Assert.assertEquals(first, set.getSup(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertPositiveInfinity(set.getSize());
+        Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(Cartesian1D.NaN, (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE);
+
+        List<Interval> intervals = set.asList();
+        Assert.assertEquals(1, intervals.size());
+        assertInterval(Double.NEGATIVE_INFINITY, first, intervals.get(0), TEST_TOLERANCE);
+
+        assertLocation(Region.Location.INSIDE, set, 0.0);
+        assertLocation(Region.Location.BOUNDARY, set, 1.0);
+        assertLocation(Region.Location.OUTSIDE, set, 2.0);
+    }
+
+    @Test
+    public void testFromBoundaries_intervalSmallerThanTolerance_onlyFirstBoundaryUsed() {
+        // arrange
+        double tolerance = 1e-3;
+        double first = 1.0;
+        double second = 1.0 - 1e-4;
+        List<SubHyperplane<Euclidean1D>> boundaries = new ArrayList<>();
+        boundaries.add(subOrientedPoint(first, false, tolerance));
+        boundaries.add(subOrientedPoint(second, true, tolerance));
+
+        // act
+        IntervalsSet set = new IntervalsSet(boundaries, tolerance);
+
+        // assert
+        Assert.assertEquals(tolerance, set.getTolerance(), Precision.SAFE_MIN);
+        Assert.assertEquals(first, set.getInf(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertPositiveInfinity(set.getSup());
+        EuclideanTestUtils.assertPositiveInfinity(set.getSize());
+        Assert.assertEquals(0.0, set.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(Cartesian1D.NaN, (Cartesian1D) set.getBarycenter(), TEST_TOLERANCE);
+
+        List<Interval> intervals = set.asList();
+        Assert.assertEquals(1, intervals.size());
+        assertInterval(first, Double.POSITIVE_INFINITY, intervals.get(0), TEST_TOLERANCE);
+
+        assertLocation(Region.Location.OUTSIDE, set, 0.0);
+        assertLocation(Region.Location.BOUNDARY, set, 1.0);
+        assertLocation(Region.Location.INSIDE, set, 2.0);
+    }
+
+    @Test
+    public void testProjectToBoundary() {
+        // arrange
+        List<SubHyperplane<Euclidean1D>> boundaries = new ArrayList<>();
+        boundaries.add(subOrientedPoint(-2.0, true));
+        boundaries.add(subOrientedPoint(-1.0, false));
+        boundaries.add(subOrientedPoint(2.0, true));
+        boundaries.add(subOrientedPoint(5.0, false));
+        boundaries.add(subOrientedPoint(9.0, true));
+        boundaries.add(subOrientedPoint(10.0, false));
+
+        IntervalsSet set = new IntervalsSet(boundaries, TEST_TOLERANCE);
+
+        // act/assert
+        assertProjection(new Cartesian1D(-2), -1, set, new Cartesian1D(-3));
+        assertProjection(new Cartesian1D(-2), 0, set, new Cartesian1D(-2));
+        assertProjection(new Cartesian1D(-2), 0.1, set, new Cartesian1D(-1.9));
+
+        assertProjection(new Cartesian1D(-1), 0.5, set, new Cartesian1D(-1.5));
+        assertProjection(new Cartesian1D(-1), 0.1, set, new Cartesian1D(-1.1));
+        assertProjection(new Cartesian1D(-1), 0, set, new Cartesian1D(-1));
+        assertProjection(new Cartesian1D(-1), -1, set, new Cartesian1D(0));
+
+        assertProjection(new Cartesian1D(2), -1, set, new Cartesian1D(1));
+        assertProjection(new Cartesian1D(2), 0, set, new Cartesian1D(2));
+        assertProjection(new Cartesian1D(2), 1, set, new Cartesian1D(3));
+
+        assertProjection(new Cartesian1D(5), 1, set, new Cartesian1D(4));
+        assertProjection(new Cartesian1D(5), 0, set, new Cartesian1D(5));
+
+        assertProjection(new Cartesian1D(5), -1, set, new Cartesian1D(6));
+        assertProjection(new Cartesian1D(5), -2, set, new Cartesian1D(7));
+
+        assertProjection(new Cartesian1D(9), -1, set, new Cartesian1D(8));
+        assertProjection(new Cartesian1D(9), 0, set, new Cartesian1D(9));
+        assertProjection(new Cartesian1D(9), 0.1, set, new Cartesian1D(9.1));
+
+        assertProjection(new Cartesian1D(10), 0, set, new Cartesian1D(10));
+        assertProjection(new Cartesian1D(10), -1, set, new Cartesian1D(11));
+    }
+
+    @Test
+    public void testInterval() {
+        IntervalsSet set = new IntervalsSet(2.3, 5.7, 1.0e-10);
+        Assert.assertEquals(3.4, set.getSize(), 1.0e-10);
+        Assert.assertEquals(4.0, ((Cartesian1D) set.getBarycenter()).getX(), 1.0e-10);
+        Assert.assertEquals(Region.Location.BOUNDARY, set.checkPoint(new Cartesian1D(2.3)));
+        Assert.assertEquals(Region.Location.BOUNDARY, set.checkPoint(new Cartesian1D(5.7)));
+        Assert.assertEquals(Region.Location.OUTSIDE,  set.checkPoint(new Cartesian1D(1.2)));
+        Assert.assertEquals(Region.Location.OUTSIDE,  set.checkPoint(new Cartesian1D(8.7)));
+        Assert.assertEquals(Region.Location.INSIDE,   set.checkPoint(new Cartesian1D(3.0)));
+        Assert.assertEquals(2.3, set.getInf(), 1.0e-10);
+        Assert.assertEquals(5.7, set.getSup(), 1.0e-10);
+    }
+
+    @Test
+    public void testInfinite() {
+        IntervalsSet set = new IntervalsSet(9.0, Double.POSITIVE_INFINITY, 1.0e-10);
+        Assert.assertEquals(Region.Location.BOUNDARY, set.checkPoint(new Cartesian1D(9.0)));
+        Assert.assertEquals(Region.Location.OUTSIDE,  set.checkPoint(new Cartesian1D(8.4)));
+        for (double e = 1.0; e <= 6.0; e += 1.0) {
+            Assert.assertEquals(Region.Location.INSIDE,
+                                set.checkPoint(new Cartesian1D(Math.pow(10.0, e))));
+        }
+        Assert.assertTrue(Double.isInfinite(set.getSize()));
+        Assert.assertEquals(9.0, set.getInf(), 1.0e-10);
+        Assert.assertTrue(Double.isInfinite(set.getSup()));
+
+        set = (IntervalsSet) new RegionFactory<Euclidean1D>().getComplement(set);
+        Assert.assertEquals(9.0, set.getSup(), 1.0e-10);
+        Assert.assertTrue(Double.isInfinite(set.getInf()));
+
+    }
+
+    @Test
+    public void testBooleanOperations() {
+        // arrange
+        RegionFactory<Euclidean1D> factory = new RegionFactory<>();
+
+        // act
+        IntervalsSet set = (IntervalsSet)
+        factory.intersection(factory.union(factory.difference(new IntervalsSet(1.0, 6.0, TEST_TOLERANCE),
+                                                              new IntervalsSet(3.0, 5.0, TEST_TOLERANCE)),
+                                                              new IntervalsSet(9.0, Double.POSITIVE_INFINITY, TEST_TOLERANCE)),
+                                                              new IntervalsSet(Double.NEGATIVE_INFINITY, 11.0, TEST_TOLERANCE));
+
+        // arrange
+        Assert.assertEquals(1.0, set.getInf(), TEST_TOLERANCE);
+        Assert.assertEquals(11.0, set.getSup(), TEST_TOLERANCE);
+
+        Assert.assertEquals(5.0, set.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(5.9, ((Cartesian1D) set.getBarycenter()).getX(), TEST_TOLERANCE);
+
+        assertLocation(Region.Location.OUTSIDE, set, 0.0);
+        assertLocation(Region.Location.OUTSIDE, set, 4.0);
+        assertLocation(Region.Location.OUTSIDE, set, 8.0);
+        assertLocation(Region.Location.OUTSIDE, set, 12.0);
+        assertLocation(Region.Location.INSIDE, set, 1.2);
+        assertLocation(Region.Location.INSIDE, set, 5.9);
+        assertLocation(Region.Location.INSIDE, set, 9.01);
+        assertLocation(Region.Location.BOUNDARY, set, 5.0);
+        assertLocation(Region.Location.BOUNDARY, set, 11.0);
+
+        List<Interval> list = set.asList();
+        Assert.assertEquals(3, list.size());
+        assertInterval(1.0, 3.0, list.get(0), TEST_TOLERANCE);
+        assertInterval(5.0, 6.0, list.get(1), TEST_TOLERANCE);
+        assertInterval(9.0, 11.0, list.get(2), TEST_TOLERANCE);
+    }
+
+    private void assertLocation(Region.Location location, IntervalsSet set, double pt) {
+        Assert.assertEquals(location, set.checkPoint(new Cartesian1D(pt)));
+    }
+
+    private void assertInterval(double expectedInf, double expectedSup, Interval actual, double tolerance) {
+        Assert.assertEquals(expectedInf, actual.getInf(), tolerance);
+        Assert.assertEquals(expectedSup, actual.getSup(), tolerance);
+    }
+
+    private void assertProjection(Cartesian1D expectedProjection, double expectedOffset,
+            IntervalsSet set, Cartesian1D toProject) {
+        BoundaryProjection<Euclidean1D> proj = set.projectToBoundary(toProject);
+
+        EuclideanTestUtils.assertVectorEquals(toProject, (Cartesian1D) proj.getOriginal(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(expectedProjection, (Cartesian1D) proj.getProjected(), TEST_TOLERANCE);
+        Assert.assertEquals(expectedOffset, proj.getOffset(), TEST_TOLERANCE);
+    }
+
+    private SubOrientedPoint subOrientedPoint(double location, boolean direct) {
+        return subOrientedPoint(location, direct, TEST_TOLERANCE);
+    }
+
+    private SubOrientedPoint subOrientedPoint(double location, boolean direct, double tolerance) {
+        // the remaining region isn't necessary for creating 1D boundaries so we can set it to null here
+        return new SubOrientedPoint(new OrientedPoint(new Cartesian1D(location), direct, tolerance), null);
+    }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/OrientedPointTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/OrientedPointTest.java
new file mode 100644
index 0000000..75ecf3a
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/OrientedPointTest.java
@@ -0,0 +1,189 @@
+/*
+ * 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.geometry.euclidean.oned;
+
+import org.apache.commons.geometry.core.Point;
+import org.apache.commons.geometry.core.Vector;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.numbers.core.Precision;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class OrientedPointTest {
+
+    @Test
+    public void testConstructor() {
+        // act
+        OrientedPoint pt = new OrientedPoint(new Cartesian1D(2.0), true, 1e-5);
+
+        // assert
+        Assert.assertEquals(2.0, pt.getLocation().getX(), Precision.EPSILON);
+        Assert.assertTrue(pt.isDirect());
+        Assert.assertEquals(1e-5, pt.getTolerance(), Precision.EPSILON);
+    }
+
+    @Test
+    public void testCopySelf() {
+        // arrange
+        OrientedPoint orig = new OrientedPoint(new Cartesian1D(2.0), true, 1e-5);
+
+        // act
+        OrientedPoint copy = orig.copySelf();
+
+        // assert
+        Assert.assertSame(orig, copy);
+        Assert.assertEquals(2.0, copy.getLocation().getX(), Precision.EPSILON);
+        Assert.assertTrue(copy.isDirect());
+        Assert.assertEquals(1e-5, copy.getTolerance(), Precision.EPSILON);
+    }
+
+    @Test
+    public void testGetOffset_direct_point() {
+        // arrange
+        OrientedPoint pt = new OrientedPoint(new Cartesian1D(-1.0), true, 1e-5);
+
+        // act/assert
+        Assert.assertEquals(-99, pt.getOffset((Point<Euclidean1D>) new Cartesian1D(-100)), Precision.EPSILON);
+        Assert.assertEquals(-1, pt.getOffset((Point<Euclidean1D>) new Cartesian1D(-2)), Precision.EPSILON);
+        Assert.assertEquals(-0.01, pt.getOffset((Point<Euclidean1D>) new Cartesian1D(-1.01)), Precision.EPSILON);
+        Assert.assertEquals(0.0, pt.getOffset((Point<Euclidean1D>) new Cartesian1D(-1.0)), Precision.EPSILON);
+        Assert.assertEquals(0.01, pt.getOffset((Point<Euclidean1D>) new Cartesian1D(-0.99)), Precision.EPSILON);
+        Assert.assertEquals(1, pt.getOffset((Point<Euclidean1D>) new Cartesian1D(0)), Precision.EPSILON);
+        Assert.assertEquals(101, pt.getOffset((Point<Euclidean1D>) new Cartesian1D(100)), Precision.EPSILON);
+    }
+
+    @Test
+    public void testGetOffset_notDirect_point() {
+        // arrange
+        OrientedPoint pt = new OrientedPoint(new Cartesian1D(-1.0), false, 1e-5);
+
+        // act/assert
+        Assert.assertEquals(99, pt.getOffset((Point<Euclidean1D>) new Cartesian1D(-100)), Precision.EPSILON);
+        Assert.assertEquals(1, pt.getOffset((Point<Euclidean1D>) new Cartesian1D(-2)), Precision.EPSILON);
+        Assert.assertEquals(0.01, pt.getOffset((Point<Euclidean1D>) new Cartesian1D(-1.01)), Precision.EPSILON);
+        Assert.assertEquals(0.0, pt.getOffset((Point<Euclidean1D>) new Cartesian1D(-1.0)), Precision.EPSILON);
+        Assert.assertEquals(-0.01, pt.getOffset((Point<Euclidean1D>) new Cartesian1D(-0.99)), Precision.EPSILON);
+        Assert.assertEquals(-1, pt.getOffset((Point<Euclidean1D>) new Cartesian1D(0)), Precision.EPSILON);
+        Assert.assertEquals(-101, pt.getOffset((Point<Euclidean1D>) new Cartesian1D(100)), Precision.EPSILON);
+    }
+
+    @Test
+    public void testGetOffset_direct_vector() {
+        // arrange
+        OrientedPoint pt = new OrientedPoint(new Cartesian1D(-1.0), true, 1e-5);
+
+        // act/assert
+        Assert.assertEquals(-99, pt.getOffset((Vector<Euclidean1D>) new Cartesian1D(-100)), Precision.EPSILON);
+        Assert.assertEquals(-1, pt.getOffset((Vector<Euclidean1D>) new Cartesian1D(-2)), Precision.EPSILON);
+        Assert.assertEquals(-0.01, pt.getOffset((Vector<Euclidean1D>) new Cartesian1D(-1.01)), Precision.EPSILON);
+        Assert.assertEquals(-0.0, pt.getOffset((Vector<Euclidean1D>) new Cartesian1D(-1.0)), Precision.EPSILON);
+        Assert.assertEquals(0.01, pt.getOffset((Vector<Euclidean1D>) new Cartesian1D(-0.99)), Precision.EPSILON);
+        Assert.assertEquals(1, pt.getOffset((Vector<Euclidean1D>) new Cartesian1D(0)), Precision.EPSILON);
+        Assert.assertEquals(101, pt.getOffset((Vector<Euclidean1D>) new Cartesian1D(100)), Precision.EPSILON);
+    }
+
+    @Test
+    public void testGetOffset_notDirect_vector() {
+        // arrange
+        OrientedPoint pt = new OrientedPoint(new Cartesian1D(-1.0), false, 1e-5);
+
+        // act/assert
+        Assert.assertEquals(99, pt.getOffset((Vector<Euclidean1D>) new Cartesian1D(-100)), Precision.EPSILON);
+        Assert.assertEquals(1, pt.getOffset((Vector<Euclidean1D>) new Cartesian1D(-2)), Precision.EPSILON);
+        Assert.assertEquals(0.01, pt.getOffset((Vector<Euclidean1D>) new Cartesian1D(-1.01)), Precision.EPSILON);
+        Assert.assertEquals(0.0, pt.getOffset((Vector<Euclidean1D>) new Cartesian1D(-1.0)), Precision.EPSILON);
+        Assert.assertEquals(-0.01, pt.getOffset((Vector<Euclidean1D>) new Cartesian1D(-0.99)), Precision.EPSILON);
+        Assert.assertEquals(-1, pt.getOffset((Vector<Euclidean1D>) new Cartesian1D(0)), Precision.EPSILON);
+        Assert.assertEquals(-101, pt.getOffset((Vector<Euclidean1D>) new Cartesian1D(100)), Precision.EPSILON);
+    }
+
+    @Test
+    public void testWholeHyperplane() {
+        // arrange
+        OrientedPoint pt = new OrientedPoint(new Cartesian1D(1.0), false, 1e-5);
+
+        // act
+        SubOrientedPoint subPt = pt.wholeHyperplane();
+
+        // assert
+        Assert.assertSame(pt, subPt.getHyperplane());
+        Assert.assertNull(subPt.getRemainingRegion());
+    }
+
+    @Test
+    public void testWholeSpace() {
+        // arrange
+        OrientedPoint pt = new OrientedPoint(new Cartesian1D(1.0), false, 1e-5);
+
+        // act
+        IntervalsSet set = pt.wholeSpace();
+
+        // assert
+        EuclideanTestUtils.assertNegativeInfinity(set.getInf());
+        EuclideanTestUtils.assertPositiveInfinity(set.getSup());
+    }
+
+    @Test
+    public void testSameOrientationAs() {
+        // arrange
+        OrientedPoint notDirect1 = new OrientedPoint(new Cartesian1D(1.0), false, 1e-5);
+        OrientedPoint notDirect2 = new OrientedPoint(new Cartesian1D(1.0), false, 1e-5);
+        OrientedPoint direct1 = new OrientedPoint(new Cartesian1D(1.0), true, 1e-5);
+        OrientedPoint direct2 = new OrientedPoint(new Cartesian1D(1.0), true, 1e-5);
+
+        // act/assert
+        Assert.assertTrue(notDirect1.sameOrientationAs(notDirect1));
+        Assert.assertTrue(notDirect1.sameOrientationAs(notDirect2));
+        Assert.assertTrue(notDirect2.sameOrientationAs(notDirect1));
+
+        Assert.assertTrue(direct1.sameOrientationAs(direct1));
+        Assert.assertTrue(direct1.sameOrientationAs(direct2));
+        Assert.assertTrue(direct2.sameOrientationAs(direct1));
+
+        Assert.assertFalse(notDirect1.sameOrientationAs(direct1));
+        Assert.assertFalse(direct1.sameOrientationAs(notDirect1));
+    }
+
+    @Test
+    public void testProject() {
+        // arrange
+        OrientedPoint pt = new OrientedPoint(new Cartesian1D(1.0), true, 1e-5);
+
+        // act/assert
+        Assert.assertEquals(1.0, ((Cartesian1D) pt.project(new Cartesian1D(-1.0))).getX(), Precision.EPSILON);
+        Assert.assertEquals(1.0, ((Cartesian1D) pt.project(new Cartesian1D(0.0))).getX(), Precision.EPSILON);
+        Assert.assertEquals(1.0, ((Cartesian1D) pt.project(new Cartesian1D(1.0))).getX(), Precision.EPSILON);
+        Assert.assertEquals(1.0, ((Cartesian1D) pt.project(new Cartesian1D(100.0))).getX(), Precision.EPSILON);
+    }
+
+    @Test
+    public void testRevertSelf() {
+        // arrange
+        OrientedPoint pt = new OrientedPoint(new Cartesian1D(2.0), true, 1e-5);
+
+        // act
+        pt.revertSelf();
+
+        // assert
+        Assert.assertEquals(2.0, pt.getLocation().getX(), Precision.EPSILON);
+        Assert.assertFalse(pt.isDirect());
+        Assert.assertEquals(1e-5, pt.getTolerance(), Precision.EPSILON);
+
+        Assert.assertEquals(1, pt.getOffset((Vector<Euclidean1D>) new Cartesian1D(1.0)), Precision.EPSILON);
+        Assert.assertEquals(-1, pt.getOffset((Vector<Euclidean1D>) new Cartesian1D(3.0)), Precision.EPSILON);
+    }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/SubOrientedPointTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/SubOrientedPointTest.java
new file mode 100644
index 0000000..a0210ec
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/oned/SubOrientedPointTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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.geometry.euclidean.oned;
+
+import org.apache.commons.geometry.core.partitioning.Side;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane.SplitSubHyperplane;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class SubOrientedPointTest {
+    private static final double TEST_TOLERANCE = 1e-10;
+
+    @Test
+    public void testGetSize() {
+        // arrange
+        OrientedPoint hyperplane = new OrientedPoint(new Cartesian1D(1), true, TEST_TOLERANCE);
+        SubOrientedPoint pt = hyperplane.wholeHyperplane();
+
+        // act/assert
+        Assert.assertEquals(0.0, pt.getSize(), TEST_TOLERANCE);
+    }
+
+    @Test
+    public void testIsEmpty() {
+        // arrange
+        OrientedPoint hyperplane = new OrientedPoint(new Cartesian1D(1), true, TEST_TOLERANCE);
+        SubOrientedPoint pt = hyperplane.wholeHyperplane();
+
+        // act/assert
+        Assert.assertFalse(pt.isEmpty());
+    }
+
+    @Test
+    public void testBuildNew() {
+        // arrange
+        OrientedPoint originalHyperplane = new OrientedPoint(new Cartesian1D(1), true, TEST_TOLERANCE);
+        SubOrientedPoint pt = originalHyperplane.wholeHyperplane();
+
+        OrientedPoint hyperplane = new OrientedPoint(new Cartesian1D(2), true, TEST_TOLERANCE);
+        IntervalsSet intervals = new IntervalsSet(2, 3, TEST_TOLERANCE);
+
+        // act
+        SubHyperplane<Euclidean1D> result = pt.buildNew(hyperplane, intervals);
+
+        // assert
+        Assert.assertTrue(result instanceof SubOrientedPoint);
+        Assert.assertSame(hyperplane, result.getHyperplane());
+        Assert.assertSame(intervals, ((SubOrientedPoint) result).getRemainingRegion());
+    }
+
+    @Test
+    public void testSplit_resultOnMinusSide() {
+        // arrange
+        OrientedPoint hyperplane = new OrientedPoint(new Cartesian1D(1), true, TEST_TOLERANCE);
+        IntervalsSet interval = new IntervalsSet(TEST_TOLERANCE);
+        SubOrientedPoint pt = new SubOrientedPoint(hyperplane, interval);
+
+        OrientedPoint splitter = new OrientedPoint(new Cartesian1D(2), true, TEST_TOLERANCE);
+
+        // act
+        SplitSubHyperplane<Euclidean1D> split = pt.split(splitter);
+
+        // assert
+        Assert.assertEquals(Side.MINUS, split.getSide());
+
+        SubOrientedPoint minusSub = ((SubOrientedPoint) split.getMinus());
+        Assert.assertNotNull(minusSub);
+
+        OrientedPoint minusHyper = (OrientedPoint) minusSub.getHyperplane();
+        Assert.assertEquals(1, minusHyper.getLocation().getX(), TEST_TOLERANCE);
+
+        Assert.assertSame(interval, minusSub.getRemainingRegion());
+
+        Assert.assertNull(split.getPlus());
+    }
+
+    @Test
+    public void testSplit_resultOnPlusSide() {
+        // arrange
+        OrientedPoint hyperplane = new OrientedPoint(new Cartesian1D(1), true, TEST_TOLERANCE);
+        IntervalsSet interval = new IntervalsSet(TEST_TOLERANCE);
+        SubOrientedPoint pt = new SubOrientedPoint(hyperplane, interval);
+
+        OrientedPoint splitter = new OrientedPoint(new Cartesian1D(0), true, TEST_TOLERANCE);
+
+        // act
+        SplitSubHyperplane<Euclidean1D> split = pt.split(splitter);
+
+        // assert
+        Assert.assertEquals(Side.PLUS, split.getSide());
+
+        Assert.assertNull(split.getMinus());
+
+        SubOrientedPoint plusSub = ((SubOrientedPoint) split.getPlus());
+        Assert.assertNotNull(plusSub);
+
+        OrientedPoint plusHyper = (OrientedPoint) plusSub.getHyperplane();
+        Assert.assertEquals(1, plusHyper.getLocation().getX(), TEST_TOLERANCE);
+
+        Assert.assertSame(interval, plusSub.getRemainingRegion());
+    }
+
+    @Test
+    public void testSplit_equivalentHyperplanes() {
+        // arrange
+        OrientedPoint hyperplane = new OrientedPoint(new Cartesian1D(1), true, TEST_TOLERANCE);
+        IntervalsSet interval = new IntervalsSet(TEST_TOLERANCE);
+        SubOrientedPoint pt = new SubOrientedPoint(hyperplane, interval);
+
+        OrientedPoint splitter = new OrientedPoint(new Cartesian1D(1), true, TEST_TOLERANCE);
+
+        // act
+        SplitSubHyperplane<Euclidean1D> split = pt.split(splitter);
+
+        // assert
+        Assert.assertEquals(Side.HYPER, split.getSide());
+
+        Assert.assertNull(split.getMinus());
+        Assert.assertNull(split.getPlus());
+    }
+
+    @Test
+    public void testSplit_usesToleranceFromParentHyperplane() {
+        // arrange
+        OrientedPoint hyperplane = new OrientedPoint(new Cartesian1D(1), true, 0.1);
+        SubOrientedPoint pt = hyperplane.wholeHyperplane();
+
+        // act/assert
+        SplitSubHyperplane<Euclidean1D> plusSplit = pt.split(new OrientedPoint(new Cartesian1D(0.899), true, 1e-10));
+        Assert.assertNull(plusSplit.getMinus());
+        Assert.assertNotNull(plusSplit.getPlus());
+
+        SplitSubHyperplane<Euclidean1D> lowWithinTolerance = pt.split(new OrientedPoint(new Cartesian1D(0.901), true, 1e-10));
+        Assert.assertNull(lowWithinTolerance.getMinus());
+        Assert.assertNull(lowWithinTolerance.getPlus());
+
+        SplitSubHyperplane<Euclidean1D> highWithinTolerance = pt.split(new OrientedPoint(new Cartesian1D(1.09), true, 1e-10));
+        Assert.assertNull(highWithinTolerance.getMinus());
+        Assert.assertNull(highWithinTolerance.getPlus());
+
+        SplitSubHyperplane<Euclidean1D> minusSplit = pt.split(new OrientedPoint(new Cartesian1D(1.101), true, 1e-10));
+        Assert.assertNotNull(minusSplit.getMinus());
+        Assert.assertNull(minusSplit.getPlus());
+    }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Euclidean3DTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Euclidean3DTest.java
new file mode 100644
index 0000000..976cc17
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/Euclidean3DTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.geometry.euclidean.threed;
+
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.Space;
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class Euclidean3DTest {
+
+    @Test
+    public void testDimension() {
+        Assert.assertEquals(3, Euclidean3D.getInstance().getDimension());
+    }
+
+    @Test
+    public void testSubSpace() {
+        Assert.assertTrue(Euclidean2D.getInstance() == Euclidean3D.getInstance().getSubSpace());
+    }
+
+    @Test
+    public void testSerialization() {
+        Space e3 = Euclidean3D.getInstance();
+        Space deserialized = (Space) GeometryTestUtils.serializeAndRecover(e3);
+        Assert.assertTrue(e3 == deserialized);
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LineTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LineTest.java
new file mode 100644
index 0000000..749db4f
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/LineTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.geometry.euclidean.threed;
+
+import org.apache.commons.geometry.euclidean.threed.Line;
+import org.apache.commons.geometry.euclidean.threed.Cartesian3D;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class LineTest {
+
+    @Test
+    public void testContains() {
+        Cartesian3D p1 = new Cartesian3D(0, 0, 1);
+        Line l = new Line(p1, new Cartesian3D(0, 0, 2), 1.0e-10);
+        Assert.assertTrue(l.contains(p1));
+        Assert.assertTrue(l.contains(new Cartesian3D(1.0, p1, 0.3, l.getDirection())));
+        Cartesian3D u = l.getDirection().orthogonal();
+        Cartesian3D v = Cartesian3D.crossProduct(l.getDirection(), u);
+        for (double alpha = 0; alpha < 2 * Math.PI; alpha += 0.3) {
+            Assert.assertTrue(! l.contains(p1.add(new Cartesian3D(Math.cos(alpha), u,
+                                                               Math.sin(alpha), v))));
+        }
+    }
+
+    @Test
+    public void testSimilar() {
+        Cartesian3D p1  = new Cartesian3D (1.2, 3.4, -5.8);
+        Cartesian3D p2  = new Cartesian3D (3.4, -5.8, 1.2);
+        Line     lA  = new Line(p1, p2, 1.0e-10);
+        Line     lB  = new Line(p2, p1, 1.0e-10);
+        Assert.assertTrue(lA.isSimilarTo(lB));
+        Assert.assertTrue(! lA.isSimilarTo(new Line(p1, p1.add(lA.getDirection().orthogonal()), 1.0e-10)));
+    }
+
+    @Test
+    public void testPointDistance() {
+        Line l = new Line(new Cartesian3D(0, 1, 1), new Cartesian3D(0, 2, 2), 1.0e-10);
+        Assert.assertEquals(Math.sqrt(3.0 / 2.0), l.distance(new Cartesian3D(1, 0, 1)), 1.0e-10);
+        Assert.assertEquals(0, l.distance(new Cartesian3D(0, -4, -4)), 1.0e-10);
+    }
+
+    @Test
+    public void testLineDistance() {
+        Line l = new Line(new Cartesian3D(0, 1, 1), new Cartesian3D(0, 2, 2), 1.0e-10);
+        Assert.assertEquals(1.0,
+                            l.distance(new Line(new Cartesian3D(1, 0, 1), new Cartesian3D(1, 0, 2), 1.0e-10)),
+                            1.0e-10);
+        Assert.assertEquals(0.5,
+                            l.distance(new Line(new Cartesian3D(-0.5, 0, 0), new Cartesian3D(-0.5, -1, -1), 1.0e-10)),
+                            1.0e-10);
+        Assert.assertEquals(0.0,
+                            l.distance(l),
+                            1.0e-10);
+        Assert.assertEquals(0.0,
+                            l.distance(new Line(new Cartesian3D(0, -4, -4), new Cartesian3D(0, -5, -5), 1.0e-10)),
+                            1.0e-10);
+        Assert.assertEquals(0.0,
+                            l.distance(new Line(new Cartesian3D(0, -4, -4), new Cartesian3D(0, -3, -4), 1.0e-10)),
+                            1.0e-10);
+        Assert.assertEquals(0.0,
+                            l.distance(new Line(new Cartesian3D(0, -4, -4), new Cartesian3D(1, -4, -4), 1.0e-10)),
+                            1.0e-10);
+        Assert.assertEquals(Math.sqrt(8),
+                            l.distance(new Line(new Cartesian3D(0, -4, 0), new Cartesian3D(1, -4, 0), 1.0e-10)),
+                            1.0e-10);
+    }
+
+    @Test
+    public void testClosest() {
+        Line l = new Line(new Cartesian3D(0, 1, 1), new Cartesian3D(0, 2, 2), 1.0e-10);
+        Assert.assertEquals(0.0,
+                            l.closestPoint(new Line(new Cartesian3D(1, 0, 1), new Cartesian3D(1, 0, 2), 1.0e-10)).distance(new Cartesian3D(0, 0, 0)),
+                            1.0e-10);
+        Assert.assertEquals(0.5,
+                            l.closestPoint(new Line(new Cartesian3D(-0.5, 0, 0), new Cartesian3D(-0.5, -1, -1), 1.0e-10)).distance(new Cartesian3D(-0.5, 0, 0)),
+                            1.0e-10);
+        Assert.assertEquals(0.0,
+                            l.closestPoint(l).distance(new Cartesian3D(0, 0, 0)),
+                            1.0e-10);
+        Assert.assertEquals(0.0,
+                            l.closestPoint(new Line(new Cartesian3D(0, -4, -4), new Cartesian3D(0, -5, -5), 1.0e-10)).distance(new Cartesian3D(0, 0, 0)),
+                            1.0e-10);
+        Assert.assertEquals(0.0,
+                            l.closestPoint(new Line(new Cartesian3D(0, -4, -4), new Cartesian3D(0, -3, -4), 1.0e-10)).distance(new Cartesian3D(0, -4, -4)),
+                            1.0e-10);
+        Assert.assertEquals(0.0,
+                            l.closestPoint(new Line(new Cartesian3D(0, -4, -4), new Cartesian3D(1, -4, -4), 1.0e-10)).distance(new Cartesian3D(0, -4, -4)),
+                            1.0e-10);
+        Assert.assertEquals(0.0,
+                            l.closestPoint(new Line(new Cartesian3D(0, -4, 0), new Cartesian3D(1, -4, 0), 1.0e-10)).distance(new Cartesian3D(0, -2, -2)),
+                            1.0e-10);
+    }
+
+    @Test
+    public void testIntersection() {
+        Line l = new Line(new Cartesian3D(0, 1, 1), new Cartesian3D(0, 2, 2), 1.0e-10);
+        Assert.assertNull(l.intersection(new Line(new Cartesian3D(1, 0, 1), new Cartesian3D(1, 0, 2), 1.0e-10)));
+        Assert.assertNull(l.intersection(new Line(new Cartesian3D(-0.5, 0, 0), new Cartesian3D(-0.5, -1, -1), 1.0e-10)));
+        Assert.assertEquals(0.0,
+                            l.intersection(l).distance(new Cartesian3D(0, 0, 0)),
+                            1.0e-10);
+        Assert.assertEquals(0.0,
+                            l.intersection(new Line(new Cartesian3D(0, -4, -4), new Cartesian3D(0, -5, -5), 1.0e-10)).distance(new Cartesian3D(0, 0, 0)),
+                            1.0e-10);
+        Assert.assertEquals(0.0,
+                            l.intersection(new Line(new Cartesian3D(0, -4, -4), new Cartesian3D(0, -3, -4), 1.0e-10)).distance(new Cartesian3D(0, -4, -4)),
+                            1.0e-10);
+        Assert.assertEquals(0.0,
+                            l.intersection(new Line(new Cartesian3D(0, -4, -4), new Cartesian3D(1, -4, -4), 1.0e-10)).distance(new Cartesian3D(0, -4, -4)),
+                            1.0e-10);
+        Assert.assertNull(l.intersection(new Line(new Cartesian3D(0, -4, 0), new Cartesian3D(1, -4, 0), 1.0e-10)));
+    }
+
+    @Test
+    public void testRevert() {
+
+        // setup
+        Line line = new Line(new Cartesian3D(1653345.6696423641, 6170370.041579291, 90000),
+                             new Cartesian3D(1650757.5050732433, 6160710.879908984, 0.9),
+                             1.0e-10);
+        Cartesian3D expected = line.getDirection().negate();
+
+        // action
+        Line reverted = line.revert();
+
+        // verify
+        Assert.assertArrayEquals(expected.toArray(), reverted.getDirection().toArray(), 0);
+
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/OBJWriter.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/OBJWriter.java
new file mode 100644
index 0000000..0e71f0c
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/OBJWriter.java
@@ -0,0 +1,336 @@
+/*
+ * 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.geometry.euclidean.threed;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Writer;
+import java.nio.file.Files;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.apache.commons.geometry.core.partitioning.BSPTree;
+import org.apache.commons.geometry.core.partitioning.BSPTreeVisitor;
+import org.apache.commons.geometry.core.partitioning.BoundaryAttribute;
+import org.apache.commons.geometry.euclidean.twod.Cartesian2D;
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+import org.apache.commons.geometry.euclidean.twod.PolygonsSet;
+
+/** This class creates simple OBJ files from {@link PolyhedronsSet} instances.
+ * The output files can be opened in a 3D viewer for visual debugging of 3D
+ * regions. This class is only intended for use in testing.
+ *
+ * @see https://en.wikipedia.org/wiki/Wavefront_.obj_file
+ */
+public class OBJWriter {
+
+    /** Writes an OBJ file representing the given {@link PolyhedronsSet}. Only
+     * finite boundaries are written. Infinite boundaries are ignored.
+     * @param file The path of the file to write
+     * @param poly The input PolyhedronsSet
+     * @throws IOException
+     */
+    public static void write(String file, PolyhedronsSet poly) throws IOException {
+        write(new File(file), poly);
+    }
+
+    /** Writes an OBJ file representing the given {@link PolyhedronsSet}. Only
+     * finite boundaries are written. Infinite boundaries are ignored.
+     * @param file The file to write
+     * @param poly The input PolyhedronsSet
+     * @throws IOException
+     */
+    public static void write(File file, PolyhedronsSet poly) throws IOException {
+        // get the vertices and faces
+        MeshBuilder meshBuilder = new MeshBuilder(poly.getTolerance());
+        poly.getTree(true).visit(meshBuilder);
+
+        // write them to the file
+        try (Writer writer = Files.newBufferedWriter(file.toPath())) {
+            writer.write("# Generated by " + OBJWriter.class.getName() + " on " + new Date() + "\n");
+            writeVertices(writer, meshBuilder.getVertices());
+            writeFaces(writer, meshBuilder.getFaces());
+        }
+    }
+
+    /** Writes the given list of vertices to the file in the OBJ format.
+     * @param writer
+     * @param vertices
+     * @throws IOException
+     */
+    private static void writeVertices(Writer writer, List<Cartesian3D> vertices) throws IOException {
+        DecimalFormat df = new DecimalFormat("0.######");
+
+        for (Cartesian3D v : vertices) {
+            writer.write("v ");
+            writer.write(df.format(v.getX()));
+            writer.write(" ");
+            writer.write(df.format(v.getY()));
+            writer.write(" ");
+            writer.write(df.format(v.getZ()));
+            writer.write("\n");
+        }
+    }
+
+    /** Writes the given list of face vertex indices to the file in the OBJ format. The indices
+     * are expected to be 0-based and are converted to the 1-based format used by OBJ.
+     * @param writer
+     * @param faces
+     * @throws IOException
+     */
+    private static void writeFaces(Writer writer, List<int[]> faces) throws IOException {
+        for (int[] face : faces) {
+            writer.write("f ");
+            for (int idx : face) {
+                writer.write(String.valueOf(idx + 1)); // obj indices are 1-based
+                writer.write(" ");
+            }
+            writer.write("\n");
+        }
+    }
+
+    /** Class used to impose a strict sorting on 3D vertices.
+     * If all of the components of two vertices are within tolerance of each
+     * other, then the vertices are considered equal. This helps to avoid
+     * writing duplicate vertices in the OBJ output.
+     */
+    private static class VertexComparator implements Comparator<Cartesian3D> {
+
+        /** Geometric tolerance value */
+        private double tolerance;
+
+        /** Creates a new instance with the given tolerance value.
+         * @param tolerance
+         */
+        public VertexComparator(double tolerance) {
+            this.tolerance = tolerance;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public int compare(Cartesian3D a, Cartesian3D b) {
+            int result = compareDoubles(a.getX(), b.getX());
+            if (result == 0) {
+                result = compareDoubles(a.getY(), b.getY());
+                if (result == 0) {
+                    result = compareDoubles(a.getZ(), b.getZ());
+                }
+            }
+            return result;
+        }
+
+        /** Helper method to compare two double values using the
+         * configured tolerance value. If the values are within
+         * tolerance of each other, then they are considered equal.
+         * @param a
+         * @param b
+         * @return
+         */
+        private int compareDoubles(double a, double b) {
+            double diff = a - b;
+            if (diff < -tolerance) {
+                return -1;
+            }
+            else if (diff > tolerance) {
+                return 1;
+            }
+            return 0;
+        }
+    }
+
+    /** Class for converting a 3D BSPTree into a list of vertices
+     * and face vertex indices.
+     */
+    private static class MeshBuilder implements BSPTreeVisitor<Euclidean3D> {
+
+        /** Geometric tolerance */
+        private final double tolerance;
+
+        /** Map of vertices to their index in the vertices list */
+        private Map<Cartesian3D, Integer> vertexIndexMap;
+
+        /** List of unique vertices in the BSPTree boundary */
+        private List<Cartesian3D> vertices;
+
+        /**
+         * List of face vertex indices. Each face will have 3 indices. Indices
+         * are 0-based.
+         * */
+        private List<int[]> faces;
+
+        /** Creates a new instance with the given tolerance.
+         * @param tolerance
+         */
+        public MeshBuilder(double tolerance) {
+            this.tolerance = tolerance;
+            this.vertexIndexMap = new TreeMap<>(new VertexComparator(tolerance));
+            this.vertices = new ArrayList<>();
+            this.faces = new ArrayList<>();
+        }
+
+        /** Returns the list of unique vertices found in the BSPTree.
+         * @return
+         */
+        public List<Cartesian3D> getVertices() {
+            return vertices;
+        }
+
+        /** Returns the list of 0-based face vertex indices for the BSPTree. Each face is
+         * a triangle with 3 indices.
+         * @return
+         */
+        public List<int[]> getFaces() {
+            return faces;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Order visitOrder(BSPTree<Euclidean3D> node) {
+            return Order.SUB_MINUS_PLUS;
+        }
+
+        /** {@inheritDoc} */
+        @SuppressWarnings("unchecked")
+        @Override
+        public void visitInternalNode(BSPTree<Euclidean3D> node) {
+            BoundaryAttribute<Euclidean3D> attr = (BoundaryAttribute<Euclidean3D>) node.getAttribute();
+
+            if (attr.getPlusOutside() != null) {
+                addBoundary((SubPlane) attr.getPlusOutside());
+            }
+            else if (attr.getPlusInside() != null) {
+                addBoundary((SubPlane) attr.getPlusInside());
+            }
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void visitLeafNode(BSPTree<Euclidean3D> node) {
+            // do nothing
+        }
+
+        /** Adds the region boundary defined by the given {@link SubPlane}
+         * to the mesh.
+         * @param subplane
+         */
+        private void addBoundary(SubPlane subplane) {
+            Plane plane = (Plane) subplane.getHyperplane();
+            PolygonsSet poly = (PolygonsSet) subplane.getRemainingRegion();
+
+            TriangleExtractor triExtractor = new TriangleExtractor(tolerance);
+            poly.getTree(true).visit(triExtractor);
+
+            Cartesian3D v1, v2, v3;
+            for (Cartesian2D[] tri : triExtractor.getTriangles()) {
+                v1 = plane.toSpace(tri[0]);
+                v2 = plane.toSpace(tri[1]);
+                v3 = plane.toSpace(tri[2]);
+
+                faces.add(new int[] {
+                        getVertexIndex(v1),
+                        getVertexIndex(v2),
+                        getVertexIndex(v3)
+                });
+            }
+        }
+
+        /** Returns the 0-based index of the given vertex in the <code>vertices</code>
+         * list. If the vertex has not been encountered before, it is added
+         * to the list.
+         * @param vertex
+         * @return
+         */
+        private int getVertexIndex(Cartesian3D vertex) {
+            Integer idx = vertexIndexMap.get(vertex);
+            if (idx == null) {
+                idx = vertices.size();
+
+                vertices.add(vertex);
+                vertexIndexMap.put(vertex, idx);
+            }
+            return idx.intValue();
+        }
+    }
+
+    /** Visitor for extracting a collection of triangles from a 2D BSPTree.
+     */
+    private static class TriangleExtractor implements BSPTreeVisitor<Euclidean2D> {
+
+        /** Geometric tolerance */
+        private double tolerance;
+
+        /** List of extracted triangles */
+        private List<Cartesian2D[]> triangles = new ArrayList<>();
+
+        /** Creates a new instance with the given geometric tolerance.
+         * @param tolerance
+         */
+        public TriangleExtractor(double tolerance) {
+            this.tolerance = tolerance;
+        }
+
+        /** Returns the list of extracted triangles.
+         * @return
+         */
+        public List<Cartesian2D[]> getTriangles() {
+            return triangles;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Order visitOrder(BSPTree<Euclidean2D> node) {
+            return Order.SUB_MINUS_PLUS;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void visitInternalNode(BSPTree<Euclidean2D> node) {
+            // do nothing
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void visitLeafNode(BSPTree<Euclidean2D> node) {
+            if ((Boolean) node.getAttribute()) {
+                PolygonsSet convexPoly = new PolygonsSet(node.pruneAroundConvexCell(Boolean.TRUE,
+                        Boolean.FALSE, null), tolerance);
+
+                for (Cartesian2D[] loop : convexPoly.getVertices()) {
+                    if (loop.length > 0 && loop[0] != null) { // skip unclosed loops
+                        addTriangles(loop);
+                    }
+                }
+            }
+        }
+
+        /** Splits the 2D convex area defined by the given vertices into
+         * triangles and adds them to the internal list.
+         * @param vertices
+         */
+        private void addTriangles(Cartesian2D[] vertices) {
+            // use a triangle fan to add the convex region
+            for (int i=2; i<vertices.length; ++i) {
+                triangles.add(new Cartesian2D[] { vertices[0], vertices[i-1], vertices[i] });
+            }
+        }
+    }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PLYParser.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PLYParser.java
new file mode 100644
index 0000000..f8c3edf
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PLYParser.java
@@ -0,0 +1,289 @@
+/*
+ * 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.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.numbers.core.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>
+ */
+public class PLYParser {
+
+    /** Parsed vertices. */
+    private Cartesian3D[] 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 Cartesian3D[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 Cartesian3D(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<>();
+        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<Cartesian3D> 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;
+        }
+
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlaneTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlaneTest.java
new file mode 100644
index 0000000..ea4f2f1
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlaneTest.java
@@ -0,0 +1,169 @@
+/*
+ * 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.geometry.euclidean.threed;
+
+import org.apache.commons.geometry.euclidean.threed.Line;
+import org.apache.commons.geometry.euclidean.threed.Plane;
+import org.apache.commons.geometry.euclidean.threed.Rotation;
+import org.apache.commons.geometry.euclidean.threed.Cartesian3D;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class PlaneTest {
+
+    @Test
+    public void testContains() {
+        Plane p = new Plane(new Cartesian3D(0, 0, 1), new Cartesian3D(0, 0, 1), 1.0e-10);
+        Assert.assertTrue(p.contains(new Cartesian3D(0, 0, 1)));
+        Assert.assertTrue(p.contains(new Cartesian3D(17, -32, 1)));
+        Assert.assertTrue(! p.contains(new Cartesian3D(17, -32, 1.001)));
+    }
+
+    @Test
+    public void testOffset() {
+        Cartesian3D p1 = new Cartesian3D(1, 1, 1);
+        Plane p = new Plane(p1, new Cartesian3D(0.2, 0, 0), 1.0e-10);
+        Assert.assertEquals(-5.0, p.getOffset(new Cartesian3D(-4, 0, 0)), 1.0e-10);
+        Assert.assertEquals(+5.0, p.getOffset(new Cartesian3D(6, 10, -12)), 1.0e-10);
+        Assert.assertEquals(0.3,
+                            p.getOffset(new Cartesian3D(1.0, p1, 0.3, p.getNormal())),
+                            1.0e-10);
+        Assert.assertEquals(-0.3,
+                            p.getOffset(new Cartesian3D(1.0, p1, -0.3, p.getNormal())),
+                            1.0e-10);
+    }
+
+    @Test
+    public void testPoint() {
+        Plane p = new Plane(new Cartesian3D(2, -3, 1), new Cartesian3D(1, 4, 9), 1.0e-10);
+        Assert.assertTrue(p.contains(p.getOrigin()));
+    }
+
+    @Test
+    public void testThreePoints() {
+        Cartesian3D p1 = new Cartesian3D(1.2, 3.4, -5.8);
+        Cartesian3D p2 = new Cartesian3D(3.4, -5.8, 1.2);
+        Cartesian3D p3 = new Cartesian3D(-2.0, 4.3, 0.7);
+        Plane    p  = new Plane(p1, p2, p3, 1.0e-10);
+        Assert.assertTrue(p.contains(p1));
+        Assert.assertTrue(p.contains(p2));
+        Assert.assertTrue(p.contains(p3));
+    }
+
+    @Test
+    public void testRotate() {
+        Cartesian3D p1 = new Cartesian3D(1.2, 3.4, -5.8);
+        Cartesian3D p2 = new Cartesian3D(3.4, -5.8, 1.2);
+        Cartesian3D p3 = new Cartesian3D(-2.0, 4.3, 0.7);
+        Plane    p  = new Plane(p1, p2, p3, 1.0e-10);
+        Cartesian3D oldNormal = p.getNormal();
+
+        p = p.rotate(p2, new Rotation(p2.subtract(p1), 1.7, RotationConvention.VECTOR_OPERATOR));
+        Assert.assertTrue(p.contains(p1));
+        Assert.assertTrue(p.contains(p2));
+        Assert.assertTrue(! p.contains(p3));
+
+        p = p.rotate(p2, new Rotation(oldNormal, 0.1, RotationConvention.VECTOR_OPERATOR));
+        Assert.assertTrue(! p.contains(p1));
+        Assert.assertTrue(p.contains(p2));
+        Assert.assertTrue(! p.contains(p3));
+
+        p = p.rotate(p1, new Rotation(oldNormal, 0.1, RotationConvention.VECTOR_OPERATOR));
+        Assert.assertTrue(! p.contains(p1));
+        Assert.assertTrue(! p.contains(p2));
+        Assert.assertTrue(! p.contains(p3));
+
+    }
+
+    @Test
+    public void testTranslate() {
+        Cartesian3D p1 = new Cartesian3D(1.2, 3.4, -5.8);
+        Cartesian3D p2 = new Cartesian3D(3.4, -5.8, 1.2);
+        Cartesian3D p3 = new Cartesian3D(-2.0, 4.3, 0.7);
+        Plane    p  = new Plane(p1, p2, p3, 1.0e-10);
+
+        p = p.translate(new Cartesian3D(2.0, p.getU(), -1.5, p.getV()));
+        Assert.assertTrue(p.contains(p1));
+        Assert.assertTrue(p.contains(p2));
+        Assert.assertTrue(p.contains(p3));
+
+        p = p.translate(new Cartesian3D(-1.2, p.getNormal()));
+        Assert.assertTrue(! p.contains(p1));
+        Assert.assertTrue(! p.contains(p2));
+        Assert.assertTrue(! p.contains(p3));
+
+        p = p.translate(new Cartesian3D(+1.2, p.getNormal()));
+        Assert.assertTrue(p.contains(p1));
+        Assert.assertTrue(p.contains(p2));
+        Assert.assertTrue(p.contains(p3));
+
+    }
+
+    @Test
+    public void testIntersection() {
+        Plane p = new Plane(new Cartesian3D(1, 2, 3), new Cartesian3D(-4, 1, -5), 1.0e-10);
+        Line  l = new Line(new Cartesian3D(0.2, -3.5, 0.7), new Cartesian3D(1.2, -2.5, -0.3), 1.0e-10);
+        Cartesian3D point = p.intersection(l);
+        Assert.assertTrue(p.contains(point));
+        Assert.assertTrue(l.contains(point));
+        Assert.assertNull(p.intersection(new Line(new Cartesian3D(10, 10, 10),
+                                                  new Cartesian3D(10, 10, 10).add(p.getNormal().orthogonal()),
+                                                  1.0e-10)));
+    }
+
+    @Test
+    public void testIntersection2() {
+        Cartesian3D p1  = new Cartesian3D (1.2, 3.4, -5.8);
+        Cartesian3D p2  = new Cartesian3D (3.4, -5.8, 1.2);
+        Plane    pA  = new Plane(p1, p2, new Cartesian3D (-2.0, 4.3, 0.7), 1.0e-10);
+        Plane    pB  = new Plane(p1, new Cartesian3D (11.4, -3.8, 5.1), p2, 1.0e-10);
+        Line     l   = pA.intersection(pB);
+        Assert.assertTrue(l.contains(p1));
+        Assert.assertTrue(l.contains(p2));
+        Assert.assertNull(pA.intersection(pA));
+    }
+
+    @Test
+    public void testIntersection3() {
+        Cartesian3D reference = new Cartesian3D (1.2, 3.4, -5.8);
+        Plane p1 = new Plane(reference, new Cartesian3D(1, 3, 3), 1.0e-10);
+        Plane p2 = new Plane(reference, new Cartesian3D(-2, 4, 0), 1.0e-10);
+        Plane p3 = new Plane(reference, new Cartesian3D(7, 0, -4), 1.0e-10);
+        Cartesian3D p = Plane.intersection(p1, p2, p3);
+        Assert.assertEquals(reference.getX(), p.getX(), 1.0e-10);
+        Assert.assertEquals(reference.getY(), p.getY(), 1.0e-10);
+        Assert.assertEquals(reference.getZ(), p.getZ(), 1.0e-10);
+    }
+
+    @Test
+    public void testSimilar() {
+        Cartesian3D p1  = new Cartesian3D (1.2, 3.4, -5.8);
+        Cartesian3D p2  = new Cartesian3D (3.4, -5.8, 1.2);
+        Cartesian3D p3  = new Cartesian3D (-2.0, 4.3, 0.7);
+        Plane    pA  = new Plane(p1, p2, p3, 1.0e-10);
+        Plane    pB  = new Plane(p1, new Cartesian3D (11.4, -3.8, 5.1), p2, 1.0e-10);
+        Assert.assertTrue(! pA.isSimilarTo(pB));
+        Assert.assertTrue(pA.isSimilarTo(pA));
+        Assert.assertTrue(pA.isSimilarTo(new Plane(p1, p3, p2, 1.0e-10)));
+        Cartesian3D shift = new Cartesian3D(0.3, pA.getNormal());
+        Assert.assertTrue(! pA.isSimilarTo(new Plane(p1.add(shift),
+                                                     p3.add(shift),
+                                                     p2.add(shift),
+                                                     1.0e-10)));
+    }
+
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PolyhedronsSetTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PolyhedronsSetTest.java
new file mode 100644
index 0000000..4ac23be
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PolyhedronsSetTest.java
@@ -0,0 +1,1494 @@
+/*
+ * 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.geometry.euclidean.threed;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.geometry.core.partitioning.BSPTree;
+import org.apache.commons.geometry.core.partitioning.BSPTreeVisitor;
+import org.apache.commons.geometry.core.partitioning.BoundaryAttribute;
+import org.apache.commons.geometry.core.partitioning.BoundaryProjection;
+import org.apache.commons.geometry.core.partitioning.Region;
+import org.apache.commons.geometry.core.partitioning.RegionFactory;
+import org.apache.commons.geometry.core.partitioning.SubHyperplane;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.twod.Cartesian2D;
+import org.apache.commons.geometry.euclidean.twod.Euclidean2D;
+import org.apache.commons.geometry.euclidean.twod.PolygonsSet;
+import org.apache.commons.geometry.euclidean.twod.SubLine;
+import org.apache.commons.numbers.core.Precision;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class PolyhedronsSetTest {
+
+    private static final double TEST_TOLERANCE = 1e-10;
+
+    @Test
+    public void testWholeSpace() {
+        // act
+        PolyhedronsSet polySet = new PolyhedronsSet(TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, polySet.getTolerance(), Precision.EPSILON);
+        EuclideanTestUtils.assertPositiveInfinity(polySet.getSize());
+        Assert.assertEquals(0.0, polySet.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(Cartesian3D.NaN, (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE);
+        Assert.assertFalse(polySet.isEmpty());
+        Assert.assertTrue(polySet.isFull());
+
+        checkPoints(Region.Location.INSIDE, polySet,
+                new Cartesian3D(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE),
+                new Cartesian3D(-100, -100, -100),
+                new Cartesian3D(0, 0, 0),
+                new Cartesian3D(100, 100, 100),
+                new Cartesian3D(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE));
+    }
+
+    @Test
+    public void testEmptyRegion() {
+        // act
+        PolyhedronsSet polySet = new PolyhedronsSet(new BSPTree<Euclidean3D>(Boolean.FALSE), TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, polySet.getTolerance(), Precision.EPSILON);
+        Assert.assertEquals(0.0, polySet.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(0.0, polySet.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(Cartesian3D.NaN, (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE);
+        Assert.assertTrue(polySet.isEmpty());
+        Assert.assertFalse(polySet.isFull());
+
+        checkPoints(Region.Location.OUTSIDE, polySet,
+                new Cartesian3D(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE),
+                new Cartesian3D(-100, -100, -100),
+                new Cartesian3D(0, 0, 0),
+                new Cartesian3D(100, 100, 100),
+                new Cartesian3D(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE));
+    }
+
+    @Test
+    public void testHalfSpace() {
+        // arrange
+        List<SubHyperplane<Euclidean3D>> boundaries = new ArrayList<>();
+        boundaries.add(new SubPlane(new Plane(Cartesian3D.ZERO, Cartesian3D.PLUS_J, TEST_TOLERANCE),
+                new PolygonsSet(TEST_TOLERANCE)));
+
+        // act
+        PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, polySet.getTolerance(), Precision.EPSILON);
+        EuclideanTestUtils.assertPositiveInfinity(polySet.getSize());
+        EuclideanTestUtils.assertPositiveInfinity(polySet.getBoundarySize());
+        EuclideanTestUtils.assertVectorEquals(Cartesian3D.NaN, (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE);
+        Assert.assertFalse(polySet.isEmpty());
+        Assert.assertFalse(polySet.isFull());
+
+        checkPoints(Region.Location.INSIDE, polySet,
+                new Cartesian3D(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE),
+                new Cartesian3D(-100, -100, -100));
+        checkPoints(Region.Location.BOUNDARY, polySet, new Cartesian3D(0, 0, 0));
+        checkPoints(Region.Location.OUTSIDE, polySet,
+                new Cartesian3D(100, 100, 100),
+                new Cartesian3D(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE));
+    }
+
+    @Test
+    public void testInvertedRegion() {
+        // arrange
+        List<SubHyperplane<Euclidean3D>> boundaries = createBoxBoundaries(Cartesian3D.ZERO, 1.0, TEST_TOLERANCE);
+        PolyhedronsSet box = new PolyhedronsSet(boundaries, TEST_TOLERANCE);;
+
+        // act
+        PolyhedronsSet polySet = (PolyhedronsSet) new RegionFactory<Euclidean3D>().getComplement(box);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, polySet.getTolerance(), Precision.EPSILON);
+        EuclideanTestUtils.assertPositiveInfinity(polySet.getSize());
+        Assert.assertEquals(6, polySet.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(Cartesian3D.NaN, (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE);
+        Assert.assertFalse(polySet.isEmpty());
+        Assert.assertFalse(polySet.isFull());
+
+        checkPoints(Region.Location.INSIDE, polySet,
+                new Cartesian3D(-Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE),
+                new Cartesian3D(-100, -100, -100),
+                new Cartesian3D(100, 100, 100),
+                new Cartesian3D(Double.MAX_VALUE, Double.MAX_VALUE, Double.MAX_VALUE));
+        checkPoints(Region.Location.OUTSIDE, polySet,
+                new Cartesian3D(0, 0, 0));
+    }
+
+    @Test
+    public void testCreateFromBoundaries_noBoundaries_treeRepresentsWholeSpace() {
+        // arrange
+        List<SubHyperplane<Euclidean3D>> boundaries = new ArrayList<>();
+
+        // act
+        PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, polySet.getTolerance(), Precision.EPSILON);
+        EuclideanTestUtils.assertPositiveInfinity(polySet.getSize());
+        Assert.assertEquals(0.0, polySet.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(Cartesian3D.NaN, (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE);
+        Assert.assertFalse(polySet.isEmpty());
+        Assert.assertTrue(polySet.isFull());
+    }
+
+    @Test
+    public void testCreateFromBoundaries_unitBox() {
+        // arrange
+        List<SubHyperplane<Euclidean3D>> boundaries = createBoxBoundaries(Cartesian3D.ZERO, 1.0, TEST_TOLERANCE);
+
+        // act
+        PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, polySet.getTolerance(), Precision.EPSILON);
+        Assert.assertEquals(1.0, polySet.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(6.0, polySet.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(Cartesian3D.ZERO, (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE);
+        Assert.assertFalse(polySet.isEmpty());
+        Assert.assertFalse(polySet.isFull());
+
+        checkPoints(Region.Location.OUTSIDE, polySet,
+                new Cartesian3D(-1, 0, 0),
+                new Cartesian3D(1, 0, 0),
+                new Cartesian3D(0, -1, 0),
+                new Cartesian3D(0, 1, 0),
+                new Cartesian3D(0, 0, -1),
+                new Cartesian3D(0, 0, 1),
+
+                new Cartesian3D(1, 1, 1),
+                new Cartesian3D(1, 1, -1),
+                new Cartesian3D(1, -1, 1),
+                new Cartesian3D(1, -1, -1),
+                new Cartesian3D(-1, 1, 1),
+                new Cartesian3D(-1, 1, -1),
+                new Cartesian3D(-1, -1, 1),
+                new Cartesian3D(-1, -1, -1));
+
+        checkPoints(Region.Location.BOUNDARY, polySet,
+                new Cartesian3D(0.5, 0, 0),
+                new Cartesian3D(-0.5, 0, 0),
+                new Cartesian3D(0, 0.5, 0),
+                new Cartesian3D(0, -0.5, 0),
+                new Cartesian3D(0, 0, 0.5),
+                new Cartesian3D(0, 0, -0.5),
+
+                new Cartesian3D(0.5, 0.5, 0.5),
+                new Cartesian3D(0.5, 0.5, -0.5),
+                new Cartesian3D(0.5, -0.5, 0.5),
+                new Cartesian3D(0.5, -0.5, -0.5),
+                new Cartesian3D(-0.5, 0.5, 0.5),
+                new Cartesian3D(-0.5, 0.5, -0.5),
+                new Cartesian3D(-0.5, -0.5, 0.5),
+                new Cartesian3D(-0.5, -0.5, -0.5));
+
+        checkPoints(Region.Location.INSIDE, polySet,
+                new Cartesian3D(0, 0, 0),
+
+                new Cartesian3D(0.4, 0.4, 0.4),
+                new Cartesian3D(0.4, 0.4, -0.4),
+                new Cartesian3D(0.4, -0.4, 0.4),
+                new Cartesian3D(0.4, -0.4, -0.4),
+                new Cartesian3D(-0.4, 0.4, 0.4),
+                new Cartesian3D(-0.4, 0.4, -0.4),
+                new Cartesian3D(-0.4, -0.4, 0.4),
+                new Cartesian3D(-0.4, -0.4, -0.4));
+    }
+
+    @Test
+    public void testCreateFromBoundaries_twoBoxes_disjoint() {
+        // arrange
+        List<SubHyperplane<Euclidean3D>> boundaries = new ArrayList<>();
+        boundaries.addAll(createBoxBoundaries(Cartesian3D.ZERO, 1.0, TEST_TOLERANCE));
+        boundaries.addAll(createBoxBoundaries(new Cartesian3D(2, 0, 0), 1.0, TEST_TOLERANCE));
+
+        // act
+        PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, polySet.getTolerance(), Precision.EPSILON);
+        Assert.assertEquals(2.0, polySet.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(12.0, polySet.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(new Cartesian3D(1, 0, 0), (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE);
+        Assert.assertFalse(polySet.isEmpty());
+        Assert.assertFalse(polySet.isFull());
+
+        checkPoints(Region.Location.OUTSIDE, polySet,
+                new Cartesian3D(-1, 0, 0),
+                new Cartesian3D(1, 0, 0),
+                new Cartesian3D(3, 0, 0));
+
+        checkPoints(Region.Location.INSIDE, polySet,
+                new Cartesian3D(0, 0, 0),
+                new Cartesian3D(2, 0, 0));
+    }
+
+    @Test
+    public void testCreateFromBoundaries_twoBoxes_sharedSide() {
+        // arrange
+        List<SubHyperplane<Euclidean3D>> boundaries = new ArrayList<>();
+        boundaries.addAll(createBoxBoundaries(new Cartesian3D(0, 0, 0), 1.0, TEST_TOLERANCE));
+        boundaries.addAll(createBoxBoundaries(new Cartesian3D(1, 0, 0), 1.0, TEST_TOLERANCE));
+
+        // act
+        PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, polySet.getTolerance(), Precision.EPSILON);
+        Assert.assertEquals(2.0, polySet.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(10.0, polySet.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(new Cartesian3D(0.5, 0, 0), (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE);
+        Assert.assertFalse(polySet.isEmpty());
+        Assert.assertFalse(polySet.isFull());
+
+        checkPoints(Region.Location.OUTSIDE, polySet,
+                new Cartesian3D(-1, 0, 0),
+                new Cartesian3D(2, 0, 0));
+
+        checkPoints(Region.Location.INSIDE, polySet,
+                new Cartesian3D(0, 0, 0),
+                new Cartesian3D(1, 0, 0));
+    }
+
+    @Test
+    public void testCreateFromBoundaries_twoBoxes_separationLessThanTolerance() {
+        // arrange
+        double tolerance = 1e-6;
+        List<SubHyperplane<Euclidean3D>> boundaries = new ArrayList<>();
+        boundaries.addAll(createBoxBoundaries(new Cartesian3D(0, 0, 0), 1.0, tolerance));
+        boundaries.addAll(createBoxBoundaries(new Cartesian3D(1 + 1e-7, 0, 0), 1.0, tolerance));
+
+        // act
+        PolyhedronsSet polySet = new PolyhedronsSet(boundaries, tolerance);
+
+        // assert
+        Assert.assertEquals(tolerance, polySet.getTolerance(), Precision.EPSILON);
+        Assert.assertEquals(2.0, polySet.getSize(), tolerance);
+        Assert.assertEquals(10.0, polySet.getBoundarySize(), tolerance);
+        EuclideanTestUtils.assertVectorEquals(new Cartesian3D(0.5 + 5e-8, 0, 0), (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE);
+        Assert.assertFalse(polySet.isEmpty());
+        Assert.assertFalse(polySet.isFull());
+
+        checkPoints(Region.Location.OUTSIDE, polySet,
+                new Cartesian3D(-1, 0, 0),
+                new Cartesian3D(2, 0, 0));
+
+        checkPoints(Region.Location.INSIDE, polySet,
+                new Cartesian3D(0, 0, 0),
+                new Cartesian3D(1, 0, 0));
+    }
+
+    @Test
+    public void testCreateFromBoundaries_twoBoxes_sharedEdge() {
+        // arrange
+        List<SubHyperplane<Euclidean3D>> boundaries = new ArrayList<>();
+        boundaries.addAll(createBoxBoundaries(new Cartesian3D(0, 0, 0), 1.0, TEST_TOLERANCE));
+        boundaries.addAll(createBoxBoundaries(new Cartesian3D(1, 1, 0), 1.0, TEST_TOLERANCE));
+
+        // act
+        PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, polySet.getTolerance(), Precision.EPSILON);
+        Assert.assertEquals(2.0, polySet.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(12.0, polySet.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(new Cartesian3D(0.5, 0.5, 0), (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE);
+        Assert.assertFalse(polySet.isEmpty());
+        Assert.assertFalse(polySet.isFull());
+
+        checkPoints(Region.Location.OUTSIDE, polySet,
+                new Cartesian3D(-1, 0, 0),
+                new Cartesian3D(1, 0, 0),
+                new Cartesian3D(0, 1, 0),
+                new Cartesian3D(2, 1, 0));
+
+        checkPoints(Region.Location.INSIDE, polySet,
+                new Cartesian3D(0, 0, 0),
+                new Cartesian3D(1, 1, 0));
+    }
+
+    @Test
+    public void testCreateFromBoundaries_twoBoxes_sharedPoint() {
+        // arrange
+        List<SubHyperplane<Euclidean3D>> boundaries = new ArrayList<>();
+        boundaries.addAll(createBoxBoundaries(new Cartesian3D(0, 0, 0), 1.0, TEST_TOLERANCE));
+        boundaries.addAll(createBoxBoundaries(new Cartesian3D(1, 1, 1), 1.0, TEST_TOLERANCE));
+
+        // act
+        PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(TEST_TOLERANCE, polySet.getTolerance(), Precision.EPSILON);
+        Assert.assertEquals(2.0, polySet.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(12.0, polySet.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(new Cartesian3D(0.5, 0.5, 0.5), (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE);
+        Assert.assertFalse(polySet.isEmpty());
+        Assert.assertFalse(polySet.isFull());
+
+        checkPoints(Region.Location.OUTSIDE, polySet,
+                new Cartesian3D(-1, 0, 0),
+                new Cartesian3D(1, 0, 0),
+                new Cartesian3D(0, 1, 1),
+                new Cartesian3D(2, 1, 1));
+
+        checkPoints(Region.Location.INSIDE, polySet,
+                new Cartesian3D(0, 0, 0),
+                new Cartesian3D(1, 1, 1));
+    }
+
+    @Test
+    public void testCreateBox() {
+        // act
+        PolyhedronsSet tree = new PolyhedronsSet(0, 1, 0, 1, 0, 1, TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(1.0, tree.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(6.0, tree.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(new Cartesian3D(0.5, 0.5, 0.5), (Cartesian3D) tree.getBarycenter(), TEST_TOLERANCE);
+
+        for (double x = -0.25; x < 1.25; x += 0.1) {
+            boolean xOK = (x >= 0.0) && (x <= 1.0);
+            for (double y = -0.25; y < 1.25; y += 0.1) {
+                boolean yOK = (y >= 0.0) && (y <= 1.0);
+                for (double z = -0.25; z < 1.25; z += 0.1) {
+                    boolean zOK = (z >= 0.0) && (z <= 1.0);
+                    Region.Location expected =
+                        (xOK && yOK && zOK) ? Region.Location.INSIDE : Region.Location.OUTSIDE;
+                    Assert.assertEquals(expected, tree.checkPoint(new Cartesian3D(x, y, z)));
+                }
+            }
+        }
+        checkPoints(Region.Location.BOUNDARY, tree, new Cartesian3D[] {
+            new Cartesian3D(0.0, 0.5, 0.5),
+            new Cartesian3D(1.0, 0.5, 0.5),
+            new Cartesian3D(0.5, 0.0, 0.5),
+            new Cartesian3D(0.5, 1.0, 0.5),
+            new Cartesian3D(0.5, 0.5, 0.0),
+            new Cartesian3D(0.5, 0.5, 1.0)
+        });
+        checkPoints(Region.Location.OUTSIDE, tree, new Cartesian3D[] {
+            new Cartesian3D(0.0, 1.2, 1.2),
+            new Cartesian3D(1.0, 1.2, 1.2),
+            new Cartesian3D(1.2, 0.0, 1.2),
+            new Cartesian3D(1.2, 1.0, 1.2),
+            new Cartesian3D(1.2, 1.2, 0.0),
+            new Cartesian3D(1.2, 1.2, 1.0)
+        });
+    }
+
+    @Test
+    public void testInvertedBox() {
+        // arrange
+        PolyhedronsSet tree = new PolyhedronsSet(0, 1, 0, 1, 0, 1, 1.0e-10);
+
+        // act
+        tree = (PolyhedronsSet) new RegionFactory<Euclidean3D>().getComplement(tree);
+
+        // assert
+        EuclideanTestUtils.assertPositiveInfinity(tree.getSize());
+        Assert.assertEquals(6.0, tree.getBoundarySize(), 1.0e-10);
+
+        Cartesian3D barycenter = (Cartesian3D) tree.getBarycenter();
+        Assert.assertTrue(Double.isNaN(barycenter.getX()));
+        Assert.assertTrue(Double.isNaN(barycenter.getY()));
+        Assert.assertTrue(Double.isNaN(barycenter.getZ()));
+
+        for (double x = -0.25; x < 1.25; x += 0.1) {
+            boolean xOK = (x < 0.0) || (x > 1.0);
+            for (double y = -0.25; y < 1.25; y += 0.1) {
+                boolean yOK = (y < 0.0) || (y > 1.0);
+                for (double z = -0.25; z < 1.25; z += 0.1) {
+                    boolean zOK = (z < 0.0) || (z > 1.0);
+                    Region.Location expected =
+                        (xOK || yOK || zOK) ? Region.Location.INSIDE : Region.Location.OUTSIDE;
+                    Assert.assertEquals(expected, tree.checkPoint(new Cartesian3D(x, y, z)));
+                }
+            }
+        }
+        checkPoints(Region.Location.BOUNDARY, tree, new Cartesian3D[] {
+            new Cartesian3D(0.0, 0.5, 0.5),
+            new Cartesian3D(1.0, 0.5, 0.5),
+            new Cartesian3D(0.5, 0.0, 0.5),
+            new Cartesian3D(0.5, 1.0, 0.5),
+            new Cartesian3D(0.5, 0.5, 0.0),
+            new Cartesian3D(0.5, 0.5, 1.0)
+        });
+        checkPoints(Region.Location.INSIDE, tree, new Cartesian3D[] {
+            new Cartesian3D(0.0, 1.2, 1.2),
+            new Cartesian3D(1.0, 1.2, 1.2),
+            new Cartesian3D(1.2, 0.0, 1.2),
+            new Cartesian3D(1.2, 1.0, 1.2),
+            new Cartesian3D(1.2, 1.2, 0.0),
+            new Cartesian3D(1.2, 1.2, 1.0)
+        });
+    }
+
+    @Test
+    public void testTetrahedron() {
+        // arrange
+        Cartesian3D vertex1 = new Cartesian3D(1, 2, 3);
+        Cartesian3D vertex2 = new Cartesian3D(2, 2, 4);
+        Cartesian3D vertex3 = new Cartesian3D(2, 3, 3);
+        Cartesian3D vertex4 = new Cartesian3D(1, 3, 4);
+
+        // act
+        PolyhedronsSet tree =
+            (PolyhedronsSet) new RegionFactory<Euclidean3D>().buildConvex(
+                new Plane(vertex3, vertex2, vertex1, TEST_TOLERANCE),
+                new Plane(vertex2, vertex3, vertex4, TEST_TOLERANCE),
+                new Plane(vertex4, vertex3, vertex1, TEST_TOLERANCE),
+                new Plane(vertex1, vertex2, vertex4, TEST_TOLERANCE));
+
+        // assert
+        Assert.assertEquals(1.0 / 3.0, tree.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(2.0 * Math.sqrt(3.0), tree.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(new Cartesian3D(1.5, 2.5, 3.5), (Cartesian3D) tree.getBarycenter(), TEST_TOLERANCE);
+
+        double third = 1.0 / 3.0;
+        checkPoints(Region.Location.BOUNDARY, tree, new Cartesian3D[] {
+            vertex1, vertex2, vertex3, vertex4,
+            new Cartesian3D(third, vertex1, third, vertex2, third, vertex3),
+            new Cartesian3D(third, vertex2, third, vertex3, third, vertex4),
+            new Cartesian3D(third, vertex3, third, vertex4, third, vertex1),
+            new Cartesian3D(third, vertex4, third, vertex1, third, vertex2)
+        });
+        checkPoints(Region.Location.OUTSIDE, tree, new Cartesian3D[] {
+            new Cartesian3D(1, 2, 4),
+            new Cartesian3D(2, 2, 3),
+            new Cartesian3D(2, 3, 4),
+            new Cartesian3D(1, 3, 3)
+        });
+    }
+
+    @Test
+    public void testSphere() {
+        // arrange
+        // (use a high tolerance value here since the sphere is only an approximation)
+        double approximationTolerance = 0.2;
+        double radius = 1.0;
+
+        // act
+        PolyhedronsSet polySet = createSphere(new Cartesian3D(1, 2, 3), radius, 8, 16);
+
+        // assert
+        Assert.assertEquals(sphereVolume(radius), polySet.getSize(), approximationTolerance);
+        Assert.assertEquals(sphereSurface(radius), polySet.getBoundarySize(), approximationTolerance);
+        EuclideanTestUtils.assertVectorEquals(new Cartesian3D(1, 2, 3), (Cartesian3D) polySet.getBarycenter(), TEST_TOLERANCE);
+        Assert.assertFalse(polySet.isEmpty());
+        Assert.assertFalse(polySet.isFull());
+
+        checkPoints(Region.Location.OUTSIDE, polySet,
+                new Cartesian3D(-0.1, 2, 3),
+                new Cartesian3D(2.1, 2, 3),
+                new Cartesian3D(1, 0.9, 3),
+                new Cartesian3D(1, 3.1, 3),
+                new Cartesian3D(1, 2, 1.9),
+                new Cartesian3D(1, 2, 4.1),
+                new Cartesian3D(1.6, 2.6, 3.6));
+
+        checkPoints(Region.Location.INSIDE, polySet,
+                new Cartesian3D(1, 2, 3),
+                new Cartesian3D(0.1, 2, 3),
+                new Cartesian3D(1.9, 2, 3),
+                new Cartesian3D(1, 2.1, 3),
+                new Cartesian3D(1, 2.9, 3),
+                new Cartesian3D(1, 2, 2.1),
+                new Cartesian3D(1, 2, 3.9),
+                new Cartesian3D(1.5, 2.5, 3.5));
+    }
+
+    @Test
+    public void testIsometry() {
+        // arrange
+        Cartesian3D vertex1 = new Cartesian3D(1.1, 2.2, 3.3);
+        Cartesian3D vertex2 = new Cartesian3D(2.0, 2.4, 4.2);
+        Cartesian3D vertex3 = new Cartesian3D(2.8, 3.3, 3.7);
+        Cartesian3D vertex4 = new Cartesian3D(1.0, 3.6, 4.5);
+
+        // act
+        PolyhedronsSet tree =
+            (PolyhedronsSet) new RegionFactory<Euclidean3D>().buildConvex(
+                new Plane(vertex3, vertex2, vertex1, TEST_TOLERANCE),
+                new Plane(vertex2, vertex3, vertex4, TEST_TOLERANCE),
+                new Plane(vertex4, vertex3, vertex1, TEST_TOLERANCE),
+                new Plane(vertex1, vertex2, vertex4, TEST_TOLERANCE));
+
+        // assert
+        Cartesian3D barycenter = (Cartesian3D) tree.getBarycenter();
+        Cartesian3D s = new Cartesian3D(10.2, 4.3, -6.7);
+        Cartesian3D c = new Cartesian3D(-0.2, 2.1, -3.2);
+        Rotation r = new Rotation(new Cartesian3D(6.2, -4.4, 2.1), 0.12, RotationConvention.VECTOR_OPERATOR);
+
+        tree = tree.rotate(c, r).translate(s);
+
+        Cartesian3D newB =
+            new Cartesian3D(1.0, s,
+                         1.0, c,
+                         1.0, r.applyTo(barycenter.subtract(c)));
+        Assert.assertEquals(0.0,
+                            newB.subtract((Cartesian3D) tree.getBarycenter()).getNorm(),
+                            TEST_TOLERANCE);
+
+        final Cartesian3D[] expectedV = new Cartesian3D[] {
+            new Cartesian3D(1.0, s,
+                         1.0, c,
+                         1.0, r.applyTo(vertex1.subtract(c))),
+                         new Cartesian3D(1.0, s,
+                                      1.0, c,
+                                      1.0, r.applyTo(vertex2.subtract(c))),
+                                      new Cartesian3D(1.0, s,
+                                                   1.0, c,
+                                                   1.0, r.applyTo(vertex3.subtract(c))),
+                                                   new Cartesian3D(1.0, s,
+                                                                1.0, c,
+                                                                1.0, r.applyTo(vertex4.subtract(c)))
+        };
+        tree.getTree(true).visit(new BSPTreeVisitor<Euclidean3D>() {
+
+            @Override
+            public Order visitOrder(BSPTree<Euclidean3D> node) {
+                return Order.MINUS_SUB_PLUS;
+            }
+
+            @Override
+            public void visitInternalNode(BSPTree<Euclidean3D> node) {
+                @SuppressWarnings("unchecked")
+                BoundaryAttribute<Euclidean3D> attribute =
+                    (BoundaryAttribute<Euclidean3D>) node.getAttribute();
+                if (attribute.getPlusOutside() != null) {
+                    checkFacet((SubPlane) attribute.getPlusOutside());
+                }
+                if (attribute.getPlusInside() != null) {
+                    checkFacet((SubPlane) attribute.getPlusInside());
+                }
+            }
+
+            @Override
+            public void visitLeafNode(BSPTree<Euclidean3D> node) {
+            }
+
+            private void checkFacet(SubPlane facet) {
+                Plane plane = (Plane) facet.getHyperplane();
+                Cartesian2D[][] vertices =
+                    ((PolygonsSet) facet.getRemainingRegion()).getVertices();
+                Assert.assertEquals(1, vertices.length);
+                for (int i = 0; i < vertices[0].length; ++i) {
+                    Cartesian3D v = plane.toSpace(vertices[0][i]);
+                    double d = Double.POSITIVE_INFINITY;
+                    for (int k = 0; k < expectedV.length; ++k) {
+                        d = Math.min(d, v.subtract(expectedV[k]).getNorm());
+                    }
+                    Assert.assertEquals(0, d, TEST_TOLERANCE);
+                }
+            }
+
+        });
+
+    }
+
+    @Test
+    public void testBuildBox() {
+        // arrange
+        double x = 1.0;
+        double y = 2.0;
+        double z = 3.0;
+        double w = 0.1;
+        double l = 1.0;
+
+        // act
+        PolyhedronsSet tree =
+            new PolyhedronsSet(x - l, x + l, y - w, y + w, z - w, z + w, TEST_TOLERANCE);
+
+        // assert
+        EuclideanTestUtils.assertVectorEquals(new Cartesian3D(x, y, z), (Cartesian3D) tree.getBarycenter(), TEST_TOLERANCE);
+        Assert.assertEquals(8 * l * w * w, tree.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(8 * w * (2 * l + w), tree.getBoundarySize(), TEST_TOLERANCE);
+    }
+
+    @Test
+    public void testCross() {
+        // arrange
+        double x = 1.0;
+        double y = 2.0;
+        double z = 3.0;
+        double w = 0.1;
+        double l = 1.0;
+        PolyhedronsSet xBeam =
+            new PolyhedronsSet(x - l, x + l, y - w, y + w, z - w, z + w, TEST_TOLERANCE);
+        PolyhedronsSet yBeam =
+            new PolyhedronsSet(x - w, x + w, y - l, y + l, z - w, z + w, TEST_TOLERANCE);
+        PolyhedronsSet zBeam =
+            new PolyhedronsSet(x - w, x + w, y - w, y + w, z - l, z + l, TEST_TOLERANCE);
+        RegionFactory<Euclidean3D> factory = new RegionFactory<>();
+
+        // act
+        PolyhedronsSet tree = (PolyhedronsSet) factory.union(xBeam, factory.union(yBeam, zBeam));
+
+        // assert
+        EuclideanTestUtils.assertVectorEquals(new Cartesian3D(x, y, z), (Cartesian3D) tree.getBarycenter(), TEST_TOLERANCE);
+        Assert.assertEquals(8 * w * w * (3 * l - 2 * w), tree.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(24 * w * (2 * l - w), tree.getBoundarySize(), TEST_TOLERANCE);
+    }
+
+    // Issue MATH-780
+    // See https://issues.apache.org/jira/browse/MATH-780
+    @Test
+    public void testCreateFromBoundaries_handlesSmallBoundariesCreatedDuringConstruction() {
+        // arrange
+        float[] coords = {
+            1.000000f, -1.000000f, -1.000000f,
+            1.000000f, -1.000000f, 1.000000f,
+            -1.000000f, -1.000000f, 1.000000f,
+            -1.000000f, -1.000000f, -1.000000f,
+            1.000000f, 1.000000f, -1f,
+            0.999999f, 1.000000f, 1.000000f,   // 1.000000f, 1.000000f, 1.000000f,
+            -1.000000f, 1.000000f, 1.000000f,
+            -1.000000f, 1.000000f, -1.000000f};
+        int[] indices = {
+            0, 1, 2, 0, 2, 3,
+            4, 7, 6, 4, 6, 5,
+            0, 4, 5, 0, 5, 1,
+            1, 5, 6, 1, 6, 2,
+            2, 6, 7, 2, 7, 3,
+            4, 0, 3, 4, 3, 7};
+        ArrayList<SubHyperplane<Euclidean3D>> subHyperplaneList = new ArrayList<>();
+        for (int idx = 0; idx < indices.length; idx += 3) {
+            int idxA = indices[idx] * 3;
+            int idxB = indices[idx + 1] * 3;
+            int idxC = indices[idx + 2] * 3;
+            Cartesian3D v_1 = new Cartesian3D(coords[idxA], coords[idxA + 1], coords[idxA + 2]);
+            Cartesian3D v_2 = new Cartesian3D(coords[idxB], coords[idxB + 1], coords[idxB + 2]);
+            Cartesian3D v_3 = new Cartesian3D(coords[idxC], coords[idxC + 1], coords[idxC + 2]);
+            Cartesian3D[] vertices = {v_1, v_2, v_3};
+            Plane polyPlane = new Plane(v_1, v_2, v_3, TEST_TOLERANCE);
+            ArrayList<SubHyperplane<Euclidean2D>> lines = new ArrayList<>();
+
+            Cartesian2D[] projPts = new Cartesian2D[vertices.length];
+            for (int ptIdx = 0; ptIdx < projPts.length; ptIdx++) {
+                projPts[ptIdx] = polyPlane.toSubSpace(vertices[ptIdx]);
+            }
+
+            SubLine lineInPlane = null;
+            for (int ptIdx = 0; ptIdx < projPts.length; ptIdx++) {
+                lineInPlane = new SubLine(projPts[ptIdx], projPts[(ptIdx + 1) % projPts.length], TEST_TOLERANCE);
+                lines.add(lineInPlane);
+            }
+            Region<Euclidean2D> polyRegion = new PolygonsSet(lines, TEST_TOLERANCE);
+            SubPlane polygon = new SubPlane(polyPlane, polyRegion);
+            subHyperplaneList.add(polygon);
+        }
+
+        // act
+        PolyhedronsSet polyhedronsSet = new PolyhedronsSet(subHyperplaneList, TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(8.0, polyhedronsSet.getSize(), 3.0e-6);
+        Assert.assertEquals(24.0, polyhedronsSet.getBoundarySize(), 5.0e-6);
+    }
+
+    @Test
+    public void testTooThinBox() {
+        // act
+        PolyhedronsSet polyhedronsSet = new PolyhedronsSet(0.0, 0.0, 0.0, 1.0, 0.0, 1.0, TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(0.0, polyhedronsSet.getSize(), TEST_TOLERANCE);
+    }
+
+    @Test
+    public void testWrongUsage() {
+        // the following is a wrong usage of the constructor.
+        // as explained in the javadoc, the failure is NOT detected at construction
+        // time but occurs later on
+        PolyhedronsSet ps = new PolyhedronsSet(new BSPTree<Euclidean3D>(), TEST_TOLERANCE);
+        Assert.assertNotNull(ps);
+        try {
+            ps.checkPoint(Cartesian3D.ZERO);
+            Assert.fail("an exception should have been thrown");
+        } catch (NullPointerException npe) {
+            // this is expected
+        }
+    }
+
+    @Test
+    public void testDumpParse() throws IOException, ParseException {
+        // arrange
+        double tol=1e-8;
+
+        Cartesian3D[] verts=new Cartesian3D[8];
+        double xmin=-1,xmax=1;
+        double ymin=-1,ymax=1;
+        double zmin=-1,zmax=1;
+        verts[0]=new Cartesian3D(xmin,ymin,zmin);
+        verts[1]=new Cartesian3D(xmax,ymin,zmin);
+        verts[2]=new Cartesian3D(xmax,ymax,zmin);
+        verts[3]=new Cartesian3D(xmin,ymax,zmin);
+        verts[4]=new Cartesian3D(xmin,ymin,zmax);
+        verts[5]=new Cartesian3D(xmax,ymin,zmax);
+        verts[6]=new Cartesian3D(xmax,ymax,zmax);
+        verts[7]=new Cartesian3D(xmin,ymax,zmax);
+        //
+        int[][] faces=new int[12][];
+        faces[0]=new int[]{3,1,0};  // bottom (-z)
+        faces[1]=new int[]{1,3,2};  // bottom (-z)
+        faces[2]=new int[]{5,7,4};  // top (+z)
+        faces[3]=new int[]{7,5,6};  // top (+z)
+        faces[4]=new int[]{2,5,1};  // right (+x)
+        faces[5]=new int[]{5,2,6};  // right (+x)
+        faces[6]=new int[]{4,3,0};  // left (-x)
+        faces[7]=new int[]{3,4,7};  // left (-x)
+        faces[8]=new int[]{4,1,5};  // front (-y)
+        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)
+
+        PolyhedronsSet polyset = new PolyhedronsSet(Arrays.asList(verts), Arrays.asList(faces), tol);
+
+        // act
+        String dump = EuclideanTestUtils.dump(polyset);
+        PolyhedronsSet parsed = EuclideanTestUtils.parsePolyhedronsSet(dump);
+
+        // assert
+        Assert.assertEquals(8.0, polyset.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(24.0, polyset.getBoundarySize(), TEST_TOLERANCE);
+
+        Assert.assertEquals(8.0, parsed.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(24.0, parsed.getBoundarySize(), TEST_TOLERANCE);
+        Assert.assertTrue(new RegionFactory<Euclidean3D>().difference(polyset, parsed).isEmpty());
+    }
+
+    @Test
+    public void testCreateFromBRep_connectedFacets() 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(), TEST_TOLERANCE);
+        Assert.assertEquals( 5.0, polyhedron.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(22.0, polyhedron.getBoundarySize(), TEST_TOLERANCE);
+    }
+
+    @Test
+    public void testCreateFromBRep_verticesTooClose() throws IOException, ParseException {
+        checkError("pentomino-N-too-close.ply", "Vertices are too close");
+    }
+
+    @Test
+    public void testCreateFromBRep_hole() throws IOException, ParseException {
+        checkError("pentomino-N-hole.ply", "connected to one facet only");
+    }
+
+    @Test
+    public void testCreateFromBRep_nonPlanar() throws IOException, ParseException {
+        checkError("pentomino-N-out-of-plane.ply", "out of plane");
+    }
+
+    @Test
+    public void testCreateFromBRep_badOrientation() throws IOException, ParseException {
+        checkError("pentomino-N-bad-orientation.ply", "Facet orientation mismatch");
+    }
+
+    @Test
+    public void testCreateFromBRep_wrongNumberOfPoints() throws IOException, ParseException {
+        checkError(Arrays.asList(Cartesian3D.ZERO, Cartesian3D.PLUS_I, Cartesian3D.PLUS_J, Cartesian3D.PLUS_K),
+                   Arrays.asList(new int[] { 0, 1, 2 }, new int[] {2, 3}),
+                   "");
+    }
+
+    private void checkError(final String resourceName, final String expected) {
+        try (InputStream stream = getClass().getResourceAsStream(resourceName)) {
+            PLYParser parser = new PLYParser(stream);
+            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<Cartesian3D> vertices, final List<int[]> facets,
+                            final String expected) {
+        try {
+            new PolyhedronsSet(vertices, facets, TEST_TOLERANCE);
+            Assert.fail("an exception should have been thrown");
+        } catch (IllegalArgumentException e) {
+            String actual = e.getMessage();
+            Assert.assertTrue("Expected string to contain \"" + expected + "\" but was \"" + actual + "\"",
+                    actual.contains(expected));
+        }
+    }
+
+    @Test
+    public void testFirstIntersection() {
+        // arrange
+        List<SubHyperplane<Euclidean3D>> boundaries = createBoxBoundaries(Cartesian3D.ZERO, 2.0, TEST_TOLERANCE);
+        PolyhedronsSet polySet = new PolyhedronsSet(boundaries, TEST_TOLERANCE);
+
+        Line xPlus = new Line(Cartesian3D.ZERO, Cartesian3D.PLUS_I, TEST_TOLERANCE);
+        Line xMinus = new Line(Cartesian3D.ZERO, Cartesian3D.MINUS_I, TEST_TOLERANCE);
+
+        Line yPlus = new Line(Cartesian3D.ZERO, Cartesian3D.PLUS_J, TEST_TOLERANCE);
+        Line yMinus = new Line(Cartesian3D.ZERO, Cartesian3D.MINUS_J, TEST_TOLERANCE);
+
+        Line zPlus = new Line(Cartesian3D.ZERO, Cartesian3D.PLUS_K, TEST_TOLERANCE);
+        Line zMinus = new Line(Cartesian3D.ZERO, Cartesian3D.MINUS_K, TEST_TOLERANCE);
+
+        // act/assert
+        assertSubPlaneNormal(new Cartesian3D(-1, 0, 0), polySet.firstIntersection(new Cartesian3D(-1.1, 0, 0), xPlus));
+        assertSubPlaneNormal(new Cartesian3D(-1, 0, 0), polySet.firstIntersection(new Cartesian3D(-1, 0, 0), xPlus));
+        assertSubPlaneNormal(new Cartesian3D(1, 0, 0), polySet.firstIntersection(new Cartesian3D(-0.9, 0, 0), xPlus));
+        Assert.assertEquals(null, polySet.firstIntersection(new Cartesian3D(1.1, 0, 0), xPlus));
+
+        assertSubPlaneNormal(new Cartesian3D(1, 0, 0), polySet.firstIntersection(new Cartesian3D(1.1, 0, 0), xMinus));
+        assertSubPlaneNormal(new Cartesian3D(1, 0, 0), polySet.firstIntersection(new Cartesian3D(1, 0, 0), xMinus));
+        assertSubPlaneNormal(new Cartesian3D(-1, 0, 0), polySet.firstIntersection(new Cartesian3D(0.9, 0, 0), xMinus));
+        Assert.assertEquals(null, polySet.firstIntersection(new Cartesian3D(-1.1, 0, 0), xMinus));
+
+        assertSubPlaneNormal(new Cartesian3D(0, -1, 0), polySet.firstIntersection(new Cartesian3D(0, -1.1, 0), yPlus));
+        assertSubPlaneNormal(new Cartesian3D(0, -1, 0), polySet.firstIntersection(new Cartesian3D(0, -1, 0), yPlus));
+        assertSubPlaneNormal(new Cartesian3D(0, 1, 0), polySet.firstIntersection(new Cartesian3D(0, -0.9, 0), yPlus));
+        Assert.assertEquals(null, polySet.firstIntersection(new Cartesian3D(0, 1.1, 0), yPlus));
+
+        assertSubPlaneNormal(new Cartesian3D(0, 1, 0), polySet.firstIntersection(new Cartesian3D(0, 1.1, 0), yMinus));
+        assertSubPlaneNormal(new Cartesian3D(0, 1, 0), polySet.firstIntersection(new Cartesian3D(0, 1, 0), yMinus));
+        assertSubPlaneNormal(new Cartesian3D(0, -1, 0), polySet.firstIntersection(new Cartesian3D(0, 0.9, 0), yMinus));
+        Assert.assertEquals(null, polySet.firstIntersection(new Cartesian3D(0, -1.1, 0), yMinus));
+
+        assertSubPlaneNormal(new Cartesian3D(0, 0, -1), polySet.firstIntersection(new Cartesian3D(0, 0, -1.1), zPlus));
+        assertSubPlaneNormal(new Cartesian3D(0, 0, -1), polySet.firstIntersection(new Cartesian3D(0, 0, -1), zPlus));
+        assertSubPlaneNormal(new Cartesian3D(0, 0, 1), polySet.firstIntersection(new Cartesian3D(0, 0, -0.9), zPlus));
+        Assert.assertEquals(null, polySet.firstIntersection(new Cartesian3D(0, 0, 1.1), zPlus));
+
+        assertSubPlaneNormal(new Cartesian3D(0, 0, 1), polySet.firstIntersection(new Cartesian3D(0, 0, 1.1), zMinus));
+        assertSubPlaneNormal(new Cartesian3D(0, 0, 1), polySet.firstIntersection(new Cartesian3D(0, 0, 1), zMinus));
+        assertSubPlaneNormal(new Cartesian3D(0, 0, -1), polySet.firstIntersection(new Cartesian3D(0, 0, 0.9), zMinus));
+        Assert.assertEquals(null, polySet.firstIntersection(new Cartesian3D(0, 0, -1.1), zMinus));
+    }
+
+    // Issue 1211
+    // See https://issues.apache.org/jira/browse/MATH-1211
+    @Test
+    public void testFirstIntersection_onlyReturnsPointsInDirectionOfRay() throws IOException, ParseException {
+        // arrange
+        PolyhedronsSet polyset = EuclideanTestUtils.parsePolyhedronsSet(loadTestData("issue-1211.bsp"));
+        UniformRandomProvider random = RandomSource.create(RandomSource.WELL_1024_A, 0xb97c9d1ade21e40al);
+
+        // act/assert
+        int nrays = 1000;
+        for (int i = 0; i < nrays; i++) {
+            Cartesian3D origin    = Cartesian3D.ZERO;
+            Cartesian3D direction = new Cartesian3D(2 * random.nextDouble() - 1,
+                                              2 * random.nextDouble() - 1,
+                                              2 * random.nextDouble() - 1).normalize();
+            Line line = new Line(origin, origin.add(direction), polyset.getTolerance());
+            SubHyperplane<Euclidean3D> plane = polyset.firstIntersection(origin, line);
+            if (plane != null) {
+                Cartesian3D intersectionPoint = ((Plane)plane.getHyperplane()).intersection(line);
+                double dotProduct = direction.dotProduct(intersectionPoint.subtract(origin));
+                Assert.assertTrue(dotProduct > 0);
+            }
+        }
+    }
+
+    @Test
+    public void testBoolean_union() throws IOException {
+        // arrange
+        double tolerance = 0.05;
+        double size = 1.0;
+        double radius = size * 0.5;
+        PolyhedronsSet box = new PolyhedronsSet(0, size, 0, size, 0, size, TEST_TOLERANCE);
+        PolyhedronsSet sphere = createSphere(new Cartesian3D(size * 0.5, size * 0.5, size), radius, 8, 16);
+
+        // act
+        PolyhedronsSet result = (PolyhedronsSet) new RegionFactory<Euclidean3D>().union(box, sphere);
+
+        // OBJWriter.write("union.obj", result);
+
+        // assert
+        Assert.assertEquals(cubeVolume(size) + (sphereVolume(radius) * 0.5),
+                result.getSize(), tolerance);
+        Assert.assertEquals(cubeSurface(size) - circleSurface(radius) + (0.5 * sphereSurface(radius)),
+                result.getBoundarySize(), tolerance);
+        Assert.assertFalse(result.isEmpty());
+        Assert.assertFalse(result.isFull());
+
+        checkPoints(Region.Location.OUTSIDE, result,
+                new Cartesian3D(-0.1, 0.5, 0.5),
+                new Cartesian3D(1.1, 0.5, 0.5),
+                new Cartesian3D(0.5, -0.1, 0.5),
+                new Cartesian3D(0.5, 1.1, 0.5),
+                new Cartesian3D(0.5, 0.5, -0.1),
+                new Cartesian3D(0.5, 0.5, 1.6));
+
+        checkPoints(Region.Location.INSIDE, result,
+                new Cartesian3D(0.1, 0.5, 0.5),
+                new Cartesian3D(0.9, 0.5, 0.5),
+                new Cartesian3D(0.5, 0.1, 0.5),
+                new Cartesian3D(0.5, 0.9, 0.5),
+                new Cartesian3D(0.5, 0.5, 0.1),
+                new Cartesian3D(0.5, 0.5, 1.4));
+    }
+
+    @Test
+    public void testUnion_self() {
+        // arrange
+        double tolerance = 0.2;
+        double radius = 1.0;
+
+        PolyhedronsSet sphere = createSphere(Cartesian3D.ZERO, radius, 8, 16);
+
+        // act
+        PolyhedronsSet result = (PolyhedronsSet) new RegionFactory<Euclidean3D>().union(sphere, sphere.copySelf());
+
+        // assert
+        Assert.assertEquals(sphereVolume(radius), result.getSize(), tolerance);
+        Assert.assertEquals(sphereSurface(radius), result.getBoundarySize(), tolerance);
+        EuclideanTestUtils.assertVectorEquals(Cartesian3D.ZERO, (Cartesian3D) result.getBarycenter(), TEST_TOLERANCE);
+        Assert.assertFalse(result.isEmpty());
+        Assert.assertFalse(result.isFull());
+
+        checkPoints(Region.Location.OUTSIDE, result,
+                new Cartesian3D(-1.1, 0, 0),
+                new Cartesian3D(1.1, 0, 0),
+                new Cartesian3D(0, -1.1, 0),
+                new Cartesian3D(0, 1.1, 0),
+                new Cartesian3D(0, 0, -1.1),
+                new Cartesian3D(0, 0, 1.1));
+
+        checkPoints(Region.Location.INSIDE, result,
+                new Cartesian3D(-0.9, 0, 0),
+                new Cartesian3D(0.9, 0, 0),
+                new Cartesian3D(0, -0.9, 0),
+                new Cartesian3D(0, 0.9, 0),
+                new Cartesian3D(0, 0, -0.9),
+                new Cartesian3D(0, 0, 0.9),
+                Cartesian3D.ZERO);
+    }
+
+    @Test
+    public void testBoolean_intersection() throws IOException {
+        // arrange
+        double tolerance = 0.05;
+        double size = 1.0;
+        double radius = size * 0.5;
+        PolyhedronsSet box = new PolyhedronsSet(0, size, 0, size, 0, size, TEST_TOLERANCE);
+        PolyhedronsSet sphere = createSphere(new Cartesian3D(size * 0.5, size * 0.5, size), radius, 8, 16);
+
+        // act
+        PolyhedronsSet result = (PolyhedronsSet) new RegionFactory<Euclidean3D>().intersection(box, sphere);
+
+        // OBJWriter.write("intersection.obj", result);
+
+        // assert
+        Assert.assertEquals((sphereVolume(radius) * 0.5), result.getSize(), tolerance);
+        Assert.assertEquals(circleSurface(radius) + (0.5 * sphereSurface(radius)),
+                result.getBoundarySize(), tolerance);
+        Assert.assertFalse(result.isEmpty());
+        Assert.assertFalse(result.isFull());
+
+        checkPoints(Region.Location.OUTSIDE, result,
+                new Cartesian3D(-0.1, 0.5, 1.0),
+                new Cartesian3D(1.1, 0.5, 1.0),
+                new Cartesian3D(0.5, -0.1, 1.0),
+                new Cartesian3D(0.5, 1.1, 1.0),
+                new Cartesian3D(0.5, 0.5, 0.4),
+                new Cartesian3D(0.5, 0.5, 1.1));
+
+        checkPoints(Region.Location.INSIDE, result,
+                new Cartesian3D(0.1, 0.5, 0.9),
+                new Cartesian3D(0.9, 0.5, 0.9),
+                new Cartesian3D(0.5, 0.1, 0.9),
+                new Cartesian3D(0.5, 0.9, 0.9),
+                new Cartesian3D(0.5, 0.5, 0.6),
+                new Cartesian3D(0.5, 0.5, 0.9));
+    }
+
+    @Test
+    public void testIntersection_self() {
+        // arrange
+        double tolerance = 0.2;
+        double radius = 1.0;
+
+        PolyhedronsSet sphere = createSphere(Cartesian3D.ZERO, radius, 8, 16);
+
+        // act
+        PolyhedronsSet result = (PolyhedronsSet) new RegionFactory<Euclidean3D>().intersection(sphere, sphere.copySelf());
+
+        // assert
+        Assert.assertEquals(sphereVolume(radius), result.getSize(), tolerance);
+        Assert.assertEquals(sphereSurface(radius), result.getBoundarySize(), tolerance);
+        EuclideanTestUtils.assertVectorEquals(Cartesian3D.ZERO, (Cartesian3D) result.getBarycenter(), TEST_TOLERANCE);
+        Assert.assertFalse(result.isEmpty());
+        Assert.assertFalse(result.isFull());
+
+        checkPoints(Region.Location.OUTSIDE, result,
+                new Cartesian3D(-1.1, 0, 0),
+                new Cartesian3D(1.1, 0, 0),
+                new Cartesian3D(0, -1.1, 0),
+                new Cartesian3D(0, 1.1, 0),
+                new Cartesian3D(0, 0, -1.1),
+                new Cartesian3D(0, 0, 1.1));
+
+        checkPoints(Region.Location.INSIDE, result,
+                new Cartesian3D(-0.9, 0, 0),
+                new Cartesian3D(0.9, 0, 0),
+                new Cartesian3D(0, -0.9, 0),
+                new Cartesian3D(0, 0.9, 0),
+                new Cartesian3D(0, 0, -0.9),
+                new Cartesian3D(0, 0, 0.9),
+                Cartesian3D.ZERO);
+    }
+
+    @Test
+    public void testBoolean_xor_twoCubes() throws IOException {
+        // arrange
+        double size = 1.0;
+        PolyhedronsSet box1 = new PolyhedronsSet(
+                0, size,
+                0, size,
+                0, size, TEST_TOLERANCE);
+        PolyhedronsSet box2 = new PolyhedronsSet(
+                0.5, size + 0.5,
+                0.5, size + 0.5,
+                0.5, size + 0.5, TEST_TOLERANCE);
+
+        // act
+        PolyhedronsSet result = (PolyhedronsSet) new RegionFactory<Euclidean3D>().xor(box1, box2);
+
+        // OBJWriter.write("xor_twoCubes.obj", result);
+
+        Assert.assertEquals((2 * cubeVolume(size)) - (2 * cubeVolume(size * 0.5)), result.getSize(), TEST_TOLERANCE);
+
+        // assert
+        Assert.assertEquals(2 * cubeSurface(size), result.getBoundarySize(), TEST_TOLERANCE);
+        Assert.assertFalse(result.isEmpty());
+        Assert.assertFalse(result.isFull());
+
+        checkPoints(Region.Location.OUTSIDE, result,
+                new Cartesian3D(-0.1, -0.1, -0.1),
+                new Cartesian3D(0.75, 0.75, 0.75),
+                new Cartesian3D(1.6, 1.6, 1.6));
+
+        checkPoints(Region.Location.BOUNDARY, result,
+                new Cartesian3D(0, 0, 0),
+                new Cartesian3D(0.5, 0.5, 0.5),
+                new Cartesian3D(1, 1, 1),
+                new Cartesian3D(1.5, 1.5, 1.5));
+
+        checkPoints(Region.Location.INSIDE, result,
+                new Cartesian3D(0.1, 0.1, 0.1),
+                new Cartesian3D(0.4, 0.4, 0.4),
+                new Cartesian3D(1.1, 1.1, 1.1),
+                new Cartesian3D(1.4, 1.4, 1.4));
+    }
+
+    @Test
+    public void testBoolean_xor_cubeAndSphere() throws IOException {
+        // arrange
+        double tolerance = 0.05;
+        double size = 1.0;
+        double radius = size * 0.5;
+        PolyhedronsSet box = new PolyhedronsSet(0, size, 0, size, 0, size, TEST_TOLERANCE);
+        PolyhedronsSet sphere = createSphere(new Cartesian3D(size * 0.5, size * 0.5, size), radius, 8, 16);
+
+        // act
+        PolyhedronsSet result = (PolyhedronsSet) new RegionFactory<Euclidean3D>().xor(box, sphere);
+
+        // OBJWriter.write("xor_cubeAndSphere.obj", result);
+
+        Assert.assertEquals(cubeVolume(size), result.getSize(), tolerance);
+
+        // assert
+        Assert.assertEquals(cubeSurface(size) + (sphereSurface(radius)),
+                result.getBoundarySize(), tolerance);
+        Assert.assertFalse(result.isEmpty());
+        Assert.assertFalse(result.isFull());
+
+        checkPoints(Region.Location.OUTSIDE, result,
+                new Cartesian3D(-0.1, 0.5, 0.5),
+                new Cartesian3D(1.1, 0.5, 0.5),
+                new Cartesian3D(0.5, -0.1, 0.5),
+                new Cartesian3D(0.5, 1.1, 0.5),
+                new Cartesian3D(0.5, 0.5, -0.1),
+                new Cartesian3D(0.5, 0.5, 1.6),
+                new Cartesian3D(0.5, 0.5, 0.9));
+
+        checkPoints(Region.Location.INSIDE, result,
+                new Cartesian3D(0.1, 0.5, 0.5),
+                new Cartesian3D(0.9, 0.5, 0.5),
+                new Cartesian3D(0.5, 0.1, 0.5),
+                new Cartesian3D(0.5, 0.9, 0.5),
+                new Cartesian3D(0.5, 0.5, 0.1),
+                new Cartesian3D(0.5, 0.5, 1.4));
+    }
+
+    @Test
+    public void testXor_self() {
+        // arrange
+        double radius = 1.0;
+
+        PolyhedronsSet sphere = createSphere(Cartesian3D.ZERO, radius, 8, 16);
+
+        // act
+        PolyhedronsSet result = (PolyhedronsSet) new RegionFactory<Euclidean3D>().xor(sphere, sphere.copySelf());
+
+        // assert
+        Assert.assertEquals(0.0, result.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(0.0, result.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(Cartesian3D.NaN, (Cartesian3D) result.getBarycenter(), TEST_TOLERANCE);
+        Assert.assertTrue(result.isEmpty());
+        Assert.assertFalse(result.isFull());
+
+        checkPoints(Region.Location.OUTSIDE, result,
+                new Cartesian3D(-1.1, 0, 0),
+                new Cartesian3D(1.1, 0, 0),
+                new Cartesian3D(0, -1.1, 0),
+                new Cartesian3D(0, 1.1, 0),
+                new Cartesian3D(0, 0, -1.1),
+                new Cartesian3D(0, 0, 1.1),
+                new Cartesian3D(-0.9, 0, 0),
+                new Cartesian3D(0.9, 0, 0),
+                new Cartesian3D(0, -0.9, 0),
+                new Cartesian3D(0, 0.9, 0),
+                new Cartesian3D(0, 0, -0.9),
+                new Cartesian3D(0, 0, 0.9),
+                Cartesian3D.ZERO);
+    }
+
+    @Test
+    public void testBoolean_difference() throws IOException {
+        // arrange
+        double tolerance = 0.05;
+        double size = 1.0;
+        double radius = size * 0.5;
+        PolyhedronsSet box = new PolyhedronsSet(0, size, 0, size, 0, size, TEST_TOLERANCE);
+        PolyhedronsSet sphere = createSphere(new Cartesian3D(size * 0.5, size * 0.5, size), radius, 8, 16);
+
+        // act
+        PolyhedronsSet result = (PolyhedronsSet) new RegionFactory<Euclidean3D>().difference(box, sphere);
+
+        // OBJWriter.write("difference.obj", result);
+
+        // assert
+        Assert.assertEquals(cubeVolume(size) - (sphereVolume(radius) * 0.5), result.getSize(), tolerance);
+        Assert.assertEquals(cubeSurface(size) - circleSurface(radius) + (0.5 * sphereSurface(radius)),
+                result.getBoundarySize(), tolerance);
+        Assert.assertFalse(result.isEmpty());
+        Assert.assertFalse(result.isFull());
+
+        checkPoints(Region.Location.OUTSIDE, result,
+                new Cartesian3D(-0.1, 0.5, 1.0),
+                new Cartesian3D(1.1, 0.5, 1.0),
+                new Cartesian3D(0.5, -0.1, 1.0),
+                new Cartesian3D(0.5, 1.1, 1.0),
+                new Cartesian3D(0.5, 0.5, -0.1),
+                new Cartesian3D(0.5, 0.5, 0.6));
+
+        checkPoints(Region.Location.INSIDE, result,
+                new Cartesian3D(0.1, 0.5, 0.4),
+                new Cartesian3D(0.9, 0.5, 0.4),
+                new Cartesian3D(0.5, 0.1, 0.4),
+                new Cartesian3D(0.5, 0.9, 0.4),
+                new Cartesian3D(0.5, 0.5, 0.1),
+                new Cartesian3D(0.5, 0.5, 0.4));
+    }
+
+    @Test
+    public void testDifference_self() {
+        // arrange
+        double radius = 1.0;
+
+        PolyhedronsSet sphere = createSphere(Cartesian3D.ZERO, radius, 8, 16);
+
+        // act
+        PolyhedronsSet result = (PolyhedronsSet) new RegionFactory<Euclidean3D>().difference(sphere, sphere.copySelf());
+
+        // assert
+        Assert.assertEquals(0.0, result.getSize(), TEST_TOLERANCE);
+        Assert.assertEquals(0.0, result.getBoundarySize(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(Cartesian3D.NaN, (Cartesian3D) result.getBarycenter(), TEST_TOLERANCE);
+        Assert.assertTrue(result.isEmpty());
+        Assert.assertFalse(result.isFull());
+
+        checkPoints(Region.Location.OUTSIDE, result,
+                new Cartesian3D(-1.1, 0, 0),
+                new Cartesian3D(1.1, 0, 0),
+                new Cartesian3D(0, -1.1, 0),
+                new Cartesian3D(0, 1.1, 0),
+                new Cartesian3D(0, 0, -1.1),
+                new Cartesian3D(0, 0, 1.1),
+                new Cartesian3D(-0.9, 0, 0),
+                new Cartesian3D(0.9, 0, 0),
+                new Cartesian3D(0, -0.9, 0),
+                new Cartesian3D(0, 0.9, 0),
+                new Cartesian3D(0, 0, -0.9),
+                new Cartesian3D(0, 0, 0.9),
+                Cartesian3D.ZERO);
+    }
+
+    @Test
+    public void testBoolean_multiple() throws IOException {
+        // arrange
+        double tolerance = 0.05;
+        double size = 1.0;
+        double radius = size * 0.5;
+        PolyhedronsSet box = new PolyhedronsSet(0, size, 0, size, 0, size, TEST_TOLERANCE);
+        PolyhedronsSet sphereToAdd = createSphere(new Cartesian3D(size * 0.5, size * 0.5, size), radius, 8, 16);
+        PolyhedronsSet sphereToRemove1 = createSphere(new Cartesian3D(size * 0.5, 0, size * 0.5), radius, 8, 16);
+        PolyhedronsSet sphereToRemove2 = createSphere(new Cartesian3D(size * 0.5, 1, size * 0.5), radius, 8, 16);
+
+        RegionFactory<Euclidean3D> factory = new RegionFactory<Euclidean3D>();
+
+        // act
+        PolyhedronsSet result = (PolyhedronsSet) factory.union(box, sphereToAdd);
+        result = (PolyhedronsSet) factory.difference(result, sphereToRemove1);
+        result = (PolyhedronsSet) factory.difference(result, sphereToRemove2);
+
+        // OBJWriter.write("multiple.obj", result);
+
+        // assert
+        Assert.assertEquals(cubeVolume(size) - (sphereVolume(radius) * 0.5),
+                result.getSize(), tolerance);
+        Assert.assertEquals(cubeSurface(size) - (3.0 * circleSurface(radius)) + (1.5 * sphereSurface(radius)),
+                result.getBoundarySize(), tolerance);
+        Assert.assertFalse(result.isEmpty());
+        Assert.assertFalse(result.isFull());
+
+        checkPoints(Region.Location.OUTSIDE, result,
+                new Cartesian3D(-0.1, 0.5, 0.5),
+                new Cartesian3D(1.1, 0.5, 0.5),
+                new Cartesian3D(0.5, 0.4, 0.5),
+                new Cartesian3D(0.5, 0.6, 0.5),
+                new Cartesian3D(0.5, 0.5, -0.1),
+                new Cartesian3D(0.5, 0.5, 1.6));
+
+        checkPoints(Region.Location.INSIDE, result,
+                new Cartesian3D(0.1, 0.5, 0.1),
+                new Cartesian3D(0.9, 0.5, 0.1),
+                new Cartesian3D(0.5, 0.4, 0.1),
+                new Cartesian3D(0.5, 0.6, 0.1),
+                new Cartesian3D(0.5, 0.5, 0.1),
+                new Cartesian3D(0.5, 0.5, 1.4));
+    }
+
+    @Test
+    public void testProjectToBoundary() {
+        // arrange
+        PolyhedronsSet polySet = new PolyhedronsSet(0, 1, 0, 1, 0, 1, TEST_TOLERANCE);
+
+        // act/assert
+        checkProjectToBoundary(polySet, new Cartesian3D(0.4, 0.5, 0.5),
+                new Cartesian3D(0, 0.5, 0.5), -0.4);
+        checkProjectToBoundary(polySet, new Cartesian3D(1.5, 0.5, 0.5),
+                new Cartesian3D(1, 0.5, 0.5), 0.5);
+        checkProjectToBoundary(polySet, new Cartesian3D(2, 2, 2),
+                new Cartesian3D(1, 1, 1), Math.sqrt(3));
+    }
+
+    @Test
+    public void testProjectToBoundary_invertedRegion() {
+        // arrange
+        PolyhedronsSet polySet = new PolyhedronsSet(0, 1, 0, 1, 0, 1, TEST_TOLERANCE);
+        polySet = (PolyhedronsSet) new RegionFactory<Euclidean3D>().getComplement(polySet);
+
+        // act/assert
+        checkProjectToBoundary(polySet, new Cartesian3D(0.4, 0.5, 0.5),
+                new Cartesian3D(0, 0.5, 0.5), 0.4);
+        checkProjectToBoundary(polySet, new Cartesian3D(1.5, 0.5, 0.5),
+                new Cartesian3D(1, 0.5, 0.5), -0.5);
+        checkProjectToBoundary(polySet, new Cartesian3D(2, 2, 2),
+                new Cartesian3D(1, 1, 1), -Math.sqrt(3));
+    }
+
+    private void checkProjectToBoundary(PolyhedronsSet poly, Cartesian3D toProject,
+            Cartesian3D expectedPoint, double expectedOffset) {
+        BoundaryProjection<Euclidean3D> proj = poly.projectToBoundary(toProject);
+
+        EuclideanTestUtils.assertVectorEquals(toProject, (Cartesian3D) proj.getOriginal(), TEST_TOLERANCE);
+        EuclideanTestUtils.assertVectorEquals(expectedPoint, (Cartesian3D) proj.getProjected(), TEST_TOLERANCE);
+        Assert.assertEquals(expectedOffset, proj.getOffset(), TEST_TOLERANCE);
+    }
+
+    private String loadTestData(final String resourceName)
+            throws IOException {
+        try (Reader reader = new InputStreamReader(getClass().getResourceAsStream(resourceName), "UTF-8")) {
+            StringBuilder builder = new StringBuilder();
+            for (int c = reader.read(); c >= 0; c = reader.read()) {
+                builder.append((char) c);
+            }
+            return builder.toString();
+        }
+    }
+
+    private void checkPoints(Region.Location expected, PolyhedronsSet poly, Cartesian3D ... points) {
+        for (int i = 0; i < points.length; ++i) {
+            Assert.assertEquals("Incorrect location for " + points[i], expected, poly.checkPoint(points[i]));
+        }
+    }
+
+    private List<SubHyperplane<Euclidean3D>> createBoxBoundaries(Cartesian3D center, double size, double tolerance) {
+        List<SubHyperplane<Euclidean3D>> boundaries = new ArrayList<>();
+
+        double offset = size * 0.5;
+
+        Plane xMinus = new Plane(center.add(new Cartesian3D(-offset, 0, 0)), Cartesian3D.MINUS_I, tolerance);
+        Plane xPlus = new Plane(center.add(new Cartesian3D(offset, 0, 0)), Cartesian3D.PLUS_I, tolerance);
+        Plane yPlus = new Plane(center.add(new Cartesian3D(0, offset, 0)), Cartesian3D.PLUS_J, tolerance);
+        Plane yMinus = new Plane(center.add(new Cartesian3D(0, -offset, 0)), Cartesian3D.MINUS_J, tolerance);

  (This diff was longer than 20,000 lines, and has been truncated...)


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@xxxxxxxxxxxxxxxx


With regards,
Apache Git Services

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@xxxxxxxxxxxxxxxxxx
For additional commands, e-mail: dev-help@xxxxxxxxxxxxxxxxxx