You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by er...@apache.org on 2018/04/20 14:36:12 UTC

[commons-geometry] 01/06: GEOMETRY-1: moving over existing geometry code from commons-math

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

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

commit fdbc5cd789284dd41e4f2d7efdaaecfec70f814e
Author: Matt Juntunen <ma...@hotmail.com>
AuthorDate: Wed Apr 18 23:20:56 2018 -0400

    GEOMETRY-1: moving over existing geometry code from commons-math
---
 .../pom.xml                                        |   29 +-
 .../org/apache/commons/geometry/core/Geometry.java |   32 +
 .../org/apache/commons/geometry/core/Point.java    |   45 +
 .../org/apache/commons/geometry/core/Space.java    |   39 +
 .../org/apache/commons/geometry/core/Vector.java   |  160 ++
 .../apache/commons/geometry/core/package-info.java |   25 +
 .../geometry/core/partitioning/AbstractRegion.java |  545 ++++++
 .../core/partitioning/AbstractSubHyperplane.java   |  189 ++
 .../geometry/core/partitioning/BSPTree.java        |  775 ++++++++
 .../geometry/core/partitioning/BSPTreeVisitor.java |  112 ++
 .../core/partitioning/BoundaryAttribute.java       |   97 ++
 .../core/partitioning/BoundaryBuilder.java         |   97 ++
 .../core/partitioning/BoundaryProjection.java      |   82 +
 .../core/partitioning/BoundaryProjector.java       |  201 +++
 .../core/partitioning/BoundarySizeVisitor.java     |   67 +
 .../core/partitioning/Characterization.java        |  196 +++
 .../geometry/core/partitioning/Embedding.java      |   67 +
 .../geometry/core/partitioning/Hyperplane.java     |   94 +
 .../geometry/core/partitioning/InsideFinder.java   |  149 ++
 .../geometry/core/partitioning/NodesSet.java       |   72 +
 .../commons/geometry/core/partitioning/Region.java |  205 +++
 .../geometry/core/partitioning/RegionFactory.java  |  384 ++++
 .../commons/geometry/core/partitioning/Side.java   |   36 +
 .../geometry/core/partitioning/SubHyperplane.java  |  142 ++
 .../geometry/core/partitioning/Transform.java      |   78 +
 .../geometry/core/partitioning/package-info.java   |  114 ++
 .../commons/geometry/core/GeometryTestUtils.java   |   69 +
 .../geometry/core/partitioning/TreeBuilder.java    |  164 ++
 .../geometry/core/partitioning/TreeDumper.java     |  106 ++
 .../geometry/core/partitioning/TreePrinter.java    |  137 ++
 .../pom.xml                                        |   46 +-
 .../commons/geometry/enclosing/Encloser.java       |   35 +
 .../commons/geometry/enclosing/EnclosingBall.java  |  103 ++
 .../geometry/enclosing/SupportBallGenerator.java   |   41 +
 .../commons/geometry/enclosing/WelzlEncloser.java  |  180 ++
 .../commons/geometry/enclosing/package-info.java   |   24 +
 .../threed/enclosing/SphereGenerator.java          |  154 ++
 .../euclidean/twod/enclosing/DiskGenerator.java    |  109 ++
 .../geometry/enclosing/WelzlEncloser2DTest.java    |  179 ++
 .../geometry/enclosing/WelzlEncloser3DTest.java    |  187 ++
 .../threed/enclosing/SphereGeneratorTest.java      |  186 ++
 .../twod/enclosing/DiskGeneratorTest.java          |  121 ++
 commons-geometry-euclidean-twod/pom.xml            |   46 -
 commons-geometry-euclidean/pom.xml                 |   92 +
 .../geometry/euclidean/oned/Cartesian1D.java       |  382 ++++
 .../geometry/euclidean/oned/Euclidean1D.java       |   80 +
 .../commons/geometry/euclidean/oned/Interval.java  |   89 +
 .../geometry/euclidean/oned/IntervalsSet.java      |  619 +++++++
 .../geometry/euclidean/oned/OrientedPoint.java     |  140 ++
 .../geometry/euclidean/oned/SubOrientedPoint.java  |   76 +
 .../commons/geometry/euclidean/oned/Vector1D.java  |   31 +
 .../geometry/euclidean/oned/package-info.java      |   24 +
 .../geometry/euclidean/threed/Cartesian3D.java     |  621 +++++++
 .../geometry/euclidean/threed/Euclidean3D.java     |   75 +
 .../commons/geometry/euclidean/threed/Line.java    |  274 +++
 .../euclidean/threed/OutlineExtractor.java         |  264 +++
 .../commons/geometry/euclidean/threed/Plane.java   |  498 ++++++
 .../geometry/euclidean/threed/PolyhedronsSet.java  |  705 ++++++++
 .../geometry/euclidean/threed/Rotation.java        | 1419 +++++++++++++++
 .../euclidean/threed/RotationConvention.java       |   78 +
 .../geometry/euclidean/threed/RotationOrder.java   |  172 ++
 .../commons/geometry/euclidean/threed/Segment.java |   65 +
 .../commons/geometry/euclidean/threed/SubLine.java |  147 ++
 .../geometry/euclidean/threed/SubPlane.java        |  105 ++
 .../geometry/euclidean/threed/Vector3D.java        |   45 +
 .../geometry/euclidean/threed/package-info.java    |   24 +
 .../geometry/euclidean/twod/Cartesian2D.java       |  491 ++++++
 .../geometry/euclidean/twod/Euclidean2D.java       |   75 +
 .../commons/geometry/euclidean/twod/Line.java      |  559 ++++++
 .../geometry/euclidean/twod/NestedLoops.java       |  195 +++
 .../geometry/euclidean/twod/PolygonsSet.java       | 1105 ++++++++++++
 .../commons/geometry/euclidean/twod/Segment.java   |  109 ++
 .../commons/geometry/euclidean/twod/SubLine.java   |  198 +++
 .../commons/geometry/euclidean/twod/Vector2D.java  |   37 +
 .../geometry/euclidean/twod/package-info.java      |   24 +
 .../core/partitioning/CharacterizationTest.java    |  427 +++++
 .../geometry/euclidean/EuclideanTestUtils.java     |  376 ++++
 .../geometry/euclidean/oned/Cartesian1DTest.java   |  385 ++++
 .../geometry/euclidean/oned/Euclidean1DTest.java   |   43 +
 .../geometry/euclidean/oned/IntervalTest.java      |  182 ++
 .../geometry/euclidean/oned/IntervalsSetTest.java  |  586 +++++++
 .../geometry/euclidean/oned/OrientedPointTest.java |  189 ++
 .../euclidean/oned/SubOrientedPointTest.java       |  160 ++
 .../geometry/euclidean/threed/Euclidean3DTest.java |   44 +
 .../geometry/euclidean/threed/LineTest.java        |  146 ++
 .../geometry/euclidean/threed/OBJWriter.java       |  336 ++++
 .../geometry/euclidean/threed/PLYParser.java       |  289 +++
 .../geometry/euclidean/threed/PlaneTest.java       |  169 ++
 .../euclidean/threed/PolyhedronsSetTest.java       | 1494 ++++++++++++++++
 .../euclidean/threed/RotationOrderTest.java        |   59 +
 .../geometry/euclidean/threed/RotationTest.java    |  812 +++++++++
 .../geometry/euclidean/threed/SubLineTest.java     |  167 ++
 .../geometry/euclidean/threed/Vector3DTest.java    |  407 +++++
 .../geometry/euclidean/twod/Cartesian2DTest.java   |  232 +++
 .../geometry/euclidean/twod/Euclidean2DTest.java   |   44 +
 .../commons/geometry/euclidean/twod/LineTest.java  |  131 ++
 .../geometry/euclidean/twod/NestedLoopsTest.java   |   66 +
 .../geometry/euclidean/twod/PolygonsSetTest.java   | 1842 ++++++++++++++++++++
 .../geometry/euclidean/twod/SegmentTest.java       |   42 +
 .../geometry/euclidean/twod/SubLineTest.java       |  159 ++
 .../geometry/euclidean/threed/issue-1211.bsp       |   15 +
 .../threed/pentomino-N-bad-orientation.ply         |   40 +
 .../geometry/euclidean/threed/pentomino-N-hole.ply |   39 +
 .../euclidean/threed/pentomino-N-out-of-plane.ply  |   40 +
 .../euclidean/threed/pentomino-N-too-close.ply     |   86 +
 .../geometry/euclidean/threed/pentomino-N.ply      |   39 +
 commons-geometry-hull/pom.xml                      |   84 +
 .../twod/hull/AbstractConvexHullGenerator2D.java   |  108 ++
 .../euclidean/twod/hull/AklToussaintHeuristic.java |  152 ++
 .../geometry/euclidean/twod/hull/ConvexHull2D.java |  169 ++
 .../euclidean/twod/hull/ConvexHullGenerator2D.java |   34 +
 .../euclidean/twod/hull/MonotoneChain.java         |  180 ++
 .../geometry/euclidean/twod/hull/package-info.java |   25 +
 .../apache/commons/geometry/hull/ConvexHull.java   |   46 +
 .../commons/geometry/hull/ConvexHullGenerator.java |   44 +
 .../apache/commons/geometry/hull/package-info.java |   24 +
 .../twod/hull/AklToussaintHeuristicTest.java       |   41 +
 .../hull/ConvexHullGenerator2DAbstractTest.java    |  437 +++++
 .../euclidean/twod/hull/MonotoneChainTest.java     |   54 +
 .../pom.xml                                        |   39 +-
 .../geometry/spherical/SphericalCoordinates.java   |  394 +++++
 .../commons/geometry/spherical/oned/Arc.java       |  128 ++
 .../commons/geometry/spherical/oned/ArcsSet.java   |  950 ++++++++++
 .../geometry/spherical/oned/LimitAngle.java        |  133 ++
 .../commons/geometry/spherical/oned/S1Point.java   |  158 ++
 .../commons/geometry/spherical/oned/Sphere1D.java  |   86 +
 .../geometry/spherical/oned/SubLimitAngle.java     |   65 +
 .../geometry/spherical/oned/package-info.java      |   30 +
 .../commons/geometry/spherical/twod/Circle.java    |  335 ++++
 .../commons/geometry/spherical/twod/Edge.java      |  221 +++
 .../geometry/spherical/twod/EdgesBuilder.java      |  169 ++
 .../spherical/twod/PropertiesComputer.java         |  173 ++
 .../commons/geometry/spherical/twod/S2Point.java   |  235 +++
 .../commons/geometry/spherical/twod/Sphere2D.java  |   81 +
 .../spherical/twod/SphericalPolygonsSet.java       |  562 ++++++
 .../commons/geometry/spherical/twod/SubCircle.java |   70 +
 .../commons/geometry/spherical/twod/Vertex.java    |  123 ++
 .../geometry/spherical/twod/package-info.java      |   30 +
 .../spherical/SphericalCoordinatesTest.java        |   83 +
 .../geometry/spherical/SphericalTestUtils.java     |  121 ++
 pom.xml                                            |   81 +-
 141 files changed, 29369 insertions(+), 79 deletions(-)

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
... 20526 lines suppressed ...

-- 
To stop receiving notification emails like this one, please contact
erans@apache.org.