GEOMETRY-121: adding EuclideanUtils class
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/internal/EuclideanUtils.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/internal/EuclideanUtils.java
new file mode 100644
index 0000000..a692b69
--- /dev/null
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/internal/EuclideanUtils.java
@@ -0,0 +1,127 @@
+/*
+ * 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.internal;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+
+/** Class containing utilities and algorithms intended to be internal to the library.
+ * Absolutely no guarantees are made regarding the stability of this API.
+ */
+public final class EuclideanUtils {
+
+    /** Utility class; no instantiation. */
+    private EuclideanUtils() { }
+
+    /** Convert a convex polygon defined by a list of vertices into a triangle fan. The vertex forming the largest
+     * interior angle in the polygon is selected as the base of the triangle fan. Callers are responsible for
+     * ensuring that the given list of vertices define a geometrically valid convex polygon; no validation (except
+     * for a check on the minimum number of vertices) is performed.
+     * @param <T> triangle result type
+     * @param vertices vertices defining a convex polygon
+     * @param fn function accepting the vertices of each triangle as a list and returning the object used
+     *      to represent that triangle in the result; each argument to this function is guaranteed to
+     *      contain 3 vertices
+     * @return a list containing the return results of the function when passed the vertices for each
+     *      triangle in order
+     * @throws IllegalArgumentException if fewer than 3 vertices are given
+     */
+    public static <T> List<T> convexPolygonToTriangleFan(final List<Vector3D> vertices,
+           final Function<List<Vector3D>, T> fn) {
+        final int size = vertices.size();
+        if (size < 3) {
+            throw new IllegalArgumentException("Cannot create triangle fan: 3 or more vertices are required " +
+                    "but found only " + vertices.size());
+        } else if (size == 3) {
+            return Collections.singletonList(fn.apply(vertices));
+        }
+
+        final List<T> triangles = new ArrayList<>(size - 2);
+
+        final int fanIdx = findBestTriangleFanIndex(vertices);
+        int vertexIdx = (fanIdx + 1) % size;
+
+        final Vector3D fanBase = vertices.get(fanIdx);
+        Vector3D vertexA = vertices.get(vertexIdx);
+        Vector3D vertexB;
+
+        vertexIdx = (vertexIdx + 1) % size;
+        while (vertexIdx != fanIdx) {
+            vertexB = vertices.get(vertexIdx);
+
+            triangles.add(fn.apply(Arrays.asList(fanBase, vertexA, vertexB)));
+
+            vertexA = vertexB;
+            vertexIdx = (vertexIdx + 1) % size;
+        }
+
+        return triangles;
+    }
+
+    /** Find the index of the best vertex to use as the base for a triangle fan split of the convex polygon
+     * defined by the given vertices. The best vertex is the one that forms the largest interior angle in the
+     * polygon since a split at that point will help prevent the creation of very thin triangles.
+     * @param vertices vertices defining the convex polygon; must not be empty; no validation is performed
+     *      to ensure that the vertices actually define a convex polygon
+     * @return the index of the best vertex to use as the base for a triangle fan split of the convex polygon
+     */
+    private static int findBestTriangleFanIndex(final List<Vector3D> vertices) {
+        final Iterator<Vector3D> it = vertices.iterator();
+
+        Vector3D curPt = it.next();
+        Vector3D nextPt;
+
+        final Vector3D lastVec = vertices.get(vertices.size() - 1).directionTo(curPt);
+        Vector3D incomingVec = lastVec;
+        Vector3D outgoingVec;
+
+        int bestIdx = 0;
+        double bestDot = -1.0;
+
+        int idx = 0;
+        double dot;
+        while (it.hasNext()) {
+            nextPt = it.next();
+            outgoingVec = curPt.directionTo(nextPt);
+
+            dot = incomingVec.dot(outgoingVec);
+            if (dot > bestDot) {
+                bestIdx = idx;
+                bestDot = dot;
+            }
+
+            curPt = nextPt;
+            incomingVec = outgoingVec;
+
+            ++idx;
+        }
+
+        // handle the last vertex on its own
+        dot = incomingVec.dot(lastVec);
+        if (dot > bestDot) {
+            bestIdx = idx;
+        }
+
+        return bestIdx;
+    }
+}
diff --git a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Planes.java b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Planes.java
index 1ad8127..30622d8 100644
--- a/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Planes.java
+++ b/commons-geometry-euclidean/src/main/java/org/apache/commons/geometry/euclidean/threed/Planes.java
@@ -20,16 +20,14 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.Iterator;
 import java.util.List;
 import java.util.function.BiFunction;
-import java.util.function.Function;
 
 import org.apache.commons.geometry.core.partitioning.HyperplaneBoundedRegion;
 import org.apache.commons.geometry.core.partitioning.Split;
 import org.apache.commons.geometry.core.partitioning.SplitLocation;
 import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.internal.EuclideanUtils;
 import org.apache.commons.geometry.euclidean.threed.line.Line3D;
 import org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D;
 import org.apache.commons.geometry.euclidean.twod.ConvexArea;
@@ -400,98 +398,6 @@
         return new PlaneRegionExtruder(plane, extrusionVector, precision).extrude(region);
     }
 
-    /** Convert a convex polygon defined by a list of vertices into a triangle fan. The vertex forming the largest
-     * interior angle in the polygon is selected as the base of the triangle fan. Callers are responsible for
-     * ensuring that the given list of vertices define a geometrically valid convex polygon; no validation (except
-     * for a check on the minimum number of vertices) is performed.
-     * @param <T> triangle result type
-     * @param vertices vertices defining a convex polygon
-     * @param fn function accepting the vertices of each triangle as a list and returning the object used
-     *      to represent that triangle in the result; each argument to this function is guaranteed to
-     *      contain 3 vertices
-     * @return a list containing the return results of the function when passed the vertices for each
-     *      triangle in order
-     * @throws IllegalArgumentException if fewer than 3 vertices are given
-     */
-    public static <T> List<T> convexPolygonToTriangleFan(final List<Vector3D> vertices,
-            final Function<List<Vector3D>, T> fn) {
-        final int size = vertices.size();
-        if (size < 3) {
-            throw new IllegalArgumentException("Cannot create triangle fan: 3 or more vertices are required " +
-                    "but found only " + vertices.size());
-        } else if (size == 3) {
-            return Collections.singletonList(fn.apply(vertices));
-        }
-
-        final List<T> triangles = new ArrayList<>(size - 2);
-
-        final int fanIdx = findBestTriangleFanIndex(vertices);
-        int vertexIdx = (fanIdx + 1) % size;
-
-        final Vector3D fanBase = vertices.get(fanIdx);
-        Vector3D vertexA = vertices.get(vertexIdx);
-        Vector3D vertexB;
-
-        vertexIdx = (vertexIdx + 1) % size;
-        while (vertexIdx != fanIdx) {
-            vertexB = vertices.get(vertexIdx);
-
-            triangles.add(fn.apply(Arrays.asList(fanBase, vertexA, vertexB)));
-
-            vertexA = vertexB;
-            vertexIdx = (vertexIdx + 1) % size;
-        }
-
-        return triangles;
-    }
-
-    /** Find the index of the best vertex to use as the base for a triangle fan split of the convex polygon
-     * defined by the given vertices. The best vertex is the one that forms the largest interior angle in the
-     * polygon since a split at that point will help prevent the creation of very thin triangles.
-     * @param vertices vertices defining the convex polygon; must not be empty; no validation is performed
-     *      to ensure that the vertices actually define a convex polygon
-     * @return the index of the best vertex to use as the base for a triangle fan split of the convex polygon
-     */
-    private static int findBestTriangleFanIndex(final List<Vector3D> vertices) {
-        final Iterator<Vector3D> it = vertices.iterator();
-
-        Vector3D curPt = it.next();
-        Vector3D nextPt;
-
-        final Vector3D lastVec = vertices.get(vertices.size() - 1).directionTo(curPt);
-        Vector3D incomingVec = lastVec;
-        Vector3D outgoingVec;
-
-        int bestIdx = 0;
-        double bestDot = -1.0;
-
-        int idx = 0;
-        double dot;
-        while (it.hasNext()) {
-            nextPt = it.next();
-            outgoingVec = curPt.directionTo(nextPt);
-
-            dot = incomingVec.dot(outgoingVec);
-            if (dot > bestDot) {
-                bestIdx = idx;
-                bestDot = dot;
-            }
-
-            curPt = nextPt;
-            incomingVec = outgoingVec;
-
-            ++idx;
-        }
-
-        // handle the last vertex on its own
-        dot = incomingVec.dot(lastVec);
-        if (dot > bestDot) {
-            bestIdx = idx;
-        }
-
-        return bestIdx;
-    }
-
     /** Get the unique intersection of the plane subset with the given line. Null is
      * returned if no unique intersection point exists (ie, the line and plane are
      * parallel or coincident) or the line does not intersect the plane subset.
@@ -622,7 +528,7 @@
      * @throws IllegalArgumentException if fewer than 3 vertices are given
      */
     static List<Triangle3D> convexPolygonToTriangleFan(final Plane plane, final List<Vector3D> vertices) {
-        return convexPolygonToTriangleFan(vertices,
+        return EuclideanUtils.convexPolygonToTriangleFan(vertices,
                 tri -> new SimpleTriangle3D(plane, tri.get(0), tri.get(1), tri.get(2)));
     }
 
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/internal/EuclideanUtilsTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/internal/EuclideanUtilsTest.java
new file mode 100644
index 0000000..4082a60
--- /dev/null
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/internal/EuclideanUtilsTest.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.geometry.euclidean.internal;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+
+import org.apache.commons.geometry.core.GeometryTestUtils;
+import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
+import org.apache.commons.geometry.core.precision.EpsilonDoublePrecisionContext;
+import org.apache.commons.geometry.euclidean.EuclideanTestUtils;
+import org.apache.commons.geometry.euclidean.threed.Vector3D;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class EuclideanUtilsTest {
+
+    private static final double TEST_EPS = 1e-10;
+
+    private static final DoublePrecisionContext TEST_PRECISION =
+            new EpsilonDoublePrecisionContext(TEST_EPS);
+
+    @Test
+    public void testConvexPolygonToTriangleFan_threeVertices() {
+        // arrange
+        final Vector3D p1 = Vector3D.ZERO;
+        final Vector3D p2 = Vector3D.of(1, 0, 0);
+        final Vector3D p3 = Vector3D.of(0, 1, 0);
+
+        final List<List<Vector3D>> tris = new ArrayList<>();
+
+        // act
+        EuclideanUtils.convexPolygonToTriangleFan(Arrays.asList(p1, p2, p3), tris::add);
+
+        // assert
+        Assertions.assertEquals(1, tris.size());
+        EuclideanTestUtils.assertVertexLoopSequence(Arrays.asList(p1, p2, p3), tris.get(0), TEST_PRECISION);
+    }
+
+    @Test
+    public void testConvexPolygonToTriangleFan_fourVertices() {
+        // arrange
+        final Vector3D p1 = Vector3D.ZERO;
+        final Vector3D p2 = Vector3D.of(1, 0, 0);
+        final Vector3D p3 = Vector3D.of(1, 1, 0);
+        final Vector3D p4 = Vector3D.of(0, 1, 0);
+
+        final List<List<Vector3D>> tris = new ArrayList<>();
+
+        // act
+        EuclideanUtils.convexPolygonToTriangleFan(Arrays.asList(p1, p2, p3, p4), tris::add);
+
+        // assert
+        Assertions.assertEquals(2, tris.size());
+
+        EuclideanTestUtils.assertVertexLoopSequence(Arrays.asList(p1, p2, p3), tris.get(0), TEST_PRECISION);
+        EuclideanTestUtils.assertVertexLoopSequence(Arrays.asList(p1, p3, p4), tris.get(1), TEST_PRECISION);
+    }
+
+    @Test
+    public void testConvexPolygonToTriangleFan_fourVertices_chooseLargestInteriorAngleForBase() {
+        // arrange
+        final Vector3D p1 = Vector3D.ZERO;
+        final Vector3D p2 = Vector3D.of(1, 0, 0);
+        final Vector3D p3 = Vector3D.of(2, 1, 0);
+        final Vector3D p4 = Vector3D.of(1.5, 1, 0);
+
+        final List<List<Vector3D>> tris = new ArrayList<>();
+
+        // act
+        EuclideanUtils.convexPolygonToTriangleFan(Arrays.asList(p1, p2, p3, p4), tris::add);
+
+        // assert
+        Assertions.assertEquals(2, tris.size());
+        EuclideanTestUtils.assertVertexLoopSequence(Arrays.asList(p4, p1, p2), tris.get(0), TEST_PRECISION);
+        EuclideanTestUtils.assertVertexLoopSequence(Arrays.asList(p4, p2, p3), tris.get(1), TEST_PRECISION);
+    }
+
+    @Test
+    public void testConvexPolygonToTriangleFan_fourVertices_distancesLessThanPrecision() {
+        // This test checks that the triangle fan algorithm is not affected by the distances between
+        // the vertices, just as long as the points are not exactly equal. Callers are responsible for
+        // ensuring that the points are actually distinct according to the relevant precision context.
+
+        // arrange
+        final Vector3D p1 = Vector3D.ZERO;
+        final Vector3D p2 = Vector3D.of(1e-20, 0, 0);
+        final Vector3D p3 = Vector3D.of(1e-20, 1e-20, 0);
+        final Vector3D p4 = Vector3D.of(0, 1e-20, 0);
+
+        final List<List<Vector3D>> tris = new ArrayList<>();
+
+        // act
+        EuclideanUtils.convexPolygonToTriangleFan(Arrays.asList(p1, p2, p3, p4), tris::add);
+
+        // assert
+        Assertions.assertEquals(2, tris.size());
+        EuclideanTestUtils.assertVertexLoopSequence(Arrays.asList(p1, p2, p3), tris.get(0), TEST_PRECISION);
+        EuclideanTestUtils.assertVertexLoopSequence(Arrays.asList(p1, p3, p4), tris.get(1), TEST_PRECISION);
+    }
+
+    @Test
+    public void testConvexPolygonToTriangleFan_sixVertices() {
+        // arrange
+        final Vector3D p1 = Vector3D.ZERO;
+        final Vector3D p2 = Vector3D.of(1, -1, 0);
+        final Vector3D p3 = Vector3D.of(1.5, -1, 0);
+        final Vector3D p4 = Vector3D.of(5, 0, 0);
+        final Vector3D p5 = Vector3D.of(3, 1, 0);
+        final Vector3D p6 = Vector3D.of(0.5, 1, 0);
+
+        final List<List<Vector3D>> tris = new ArrayList<>();
+
+        // act
+        EuclideanUtils.convexPolygonToTriangleFan(Arrays.asList(p1, p2, p3, p4, p5, p6), tris::add);
+
+        // assert
+        Assertions.assertEquals(4, tris.size());
+        EuclideanTestUtils.assertVertexLoopSequence(Arrays.asList(p3, p4, p5), tris.get(0), TEST_PRECISION);
+        EuclideanTestUtils.assertVertexLoopSequence(Arrays.asList(p3, p5, p6), tris.get(1), TEST_PRECISION);
+        EuclideanTestUtils.assertVertexLoopSequence(Arrays.asList(p3, p6, p1), tris.get(2), TEST_PRECISION);
+        EuclideanTestUtils.assertVertexLoopSequence(Arrays.asList(p3, p1, p2), tris.get(3), TEST_PRECISION);
+    }
+
+    @Test
+    public void testConvexPolygonToTriangleFan_notEnoughVertices() {
+        // arrange
+        final String baseMsg = "Cannot create triangle fan: 3 or more vertices are required but found only ";
+
+        // act/assert
+        GeometryTestUtils.assertThrowsWithMessage(() -> {
+            EuclideanUtils.convexPolygonToTriangleFan(Collections.emptyList(), Function.identity());
+        }, IllegalArgumentException.class, baseMsg + "0");
+
+        GeometryTestUtils.assertThrowsWithMessage(() -> {
+            EuclideanUtils.convexPolygonToTriangleFan(Collections.singletonList(Vector3D.ZERO), Function.identity());
+        }, IllegalArgumentException.class, baseMsg + "1");
+
+        GeometryTestUtils.assertThrowsWithMessage(() -> {
+            EuclideanUtils.convexPolygonToTriangleFan(Arrays.asList(Vector3D.ZERO, Vector3D.of(1, 0, 0)), Function.identity());
+        }, IllegalArgumentException.class, baseMsg + "2");
+    }
+}
diff --git a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlanesTest.java b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlanesTest.java
index bfbb491..00c2994 100644
--- a/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlanesTest.java
+++ b/commons-geometry-euclidean/src/test/java/org/apache/commons/geometry/euclidean/threed/PlanesTest.java
@@ -559,59 +559,6 @@
     }
 
     @Test
-    public void testConvexPolygonToTriangleFan_fourVertices_chooseLargestInteriorAngleForBase() {
-        // arrange
-        final Plane plane = Planes.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION);
-        final Vector3D p1 = Vector3D.ZERO;
-        final Vector3D p2 = Vector3D.of(1, 0, 0);
-        final Vector3D p3 = Vector3D.of(2, 1, 0);
-        final Vector3D p4 = Vector3D.of(1.5, 1, 0);
-
-        // act
-        final List<Triangle3D> tris = Planes.convexPolygonToTriangleFan(plane, Arrays.asList(p1, p2, p3, p4));
-
-        // assert
-        Assertions.assertEquals(2, tris.size());
-
-        final Triangle3D a = tris.get(0);
-        Assertions.assertSame(plane, a.getPlane());
-        EuclideanTestUtils.assertVertexLoopSequence(Arrays.asList(p4, p1, p2), a.getVertices(), TEST_PRECISION);
-
-        final Triangle3D b = tris.get(1);
-        Assertions.assertSame(plane, b.getPlane());
-        EuclideanTestUtils.assertVertexLoopSequence(Arrays.asList(p4, p2, p3), b.getVertices(), TEST_PRECISION);
-    }
-
-    @Test
-    public void testConvexPolygonToTriangleFan_fourVertices_distancesLessThanPrecision() {
-        // This test checks that the triangle fan algorithm is not affected by the distances between
-        // the vertices, just as long as the points are not exactly equal. Callers are responsible for
-        // ensuring that the points are actually distinct according to the relevant precision context.
-
-        // arrange
-        final Plane plane = Planes.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION);
-        final Vector3D p1 = Vector3D.ZERO;
-        final Vector3D p2 = Vector3D.of(1e-20, 0, 0);
-        final Vector3D p3 = Vector3D.of(1e-20, 1e-20, 0);
-        final Vector3D p4 = Vector3D.of(0, 1e-20, 0);
-
-        // act
-        final List<Triangle3D> tris = Planes.convexPolygonToTriangleFan(plane, Arrays.asList(p1, p2, p3, p4));
-
-        // assert
-        Assertions.assertEquals(2, tris.size());
-
-        final Triangle3D a = tris.get(0);
-        Assertions.assertSame(plane, a.getPlane());
-        EuclideanTestUtils.assertVertexLoopSequence(Arrays.asList(p1, p2, p3), a.getVertices(), TEST_PRECISION);
-
-        final Triangle3D b = tris.get(1);
-        Assertions.assertSame(plane, b.getPlane());
-        EuclideanTestUtils.assertVertexLoopSequence(Arrays.asList(p1, p3, p4), b.getVertices(), TEST_PRECISION);
-    }
-
-
-    @Test
     public void testConvexPolygonToTriangleFan_sixVertices() {
         // arrange
         final Plane plane = Planes.fromNormal(Vector3D.Unit.PLUS_Z, TEST_PRECISION);
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlBoundaryWriteHandler3D.java b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlBoundaryWriteHandler3D.java
index 47b6877..f785cac 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlBoundaryWriteHandler3D.java
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/StlBoundaryWriteHandler3D.java
@@ -23,9 +23,9 @@
 import java.util.List;
 import java.util.stream.Stream;
 
+import org.apache.commons.geometry.euclidean.internal.EuclideanUtils;
 import org.apache.commons.geometry.euclidean.threed.BoundarySource3D;
 import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
-import org.apache.commons.geometry.euclidean.threed.Planes;
 import org.apache.commons.geometry.euclidean.threed.Triangle3D;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
 import org.apache.commons.geometry.euclidean.threed.mesh.TriangleMesh;
@@ -149,7 +149,8 @@
                 facet = it.next();
                 attributeValue = getFacetAttributeValue(facet);
 
-                for (final List<Vector3D> tri : Planes.convexPolygonToTriangleFan(facet.getVertices(), t -> t)) {
+                for (final List<Vector3D> tri :
+                    EuclideanUtils.convexPolygonToTriangleFan(facet.getVertices(), t -> t)) {
 
                     dataWriter.writeTriangle(
                             tri.get(0),
diff --git a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlWriter.java b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlWriter.java
index e13cd08..22cf33b 100644
--- a/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlWriter.java
+++ b/commons-geometry-io-euclidean/src/main/java/org/apache/commons/geometry/io/euclidean/threed/stl/TextStlWriter.java
@@ -20,8 +20,8 @@
 import java.io.Writer;
 import java.util.List;
 
+import org.apache.commons.geometry.euclidean.internal.EuclideanUtils;
 import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
-import org.apache.commons.geometry.euclidean.threed.Planes;
 import org.apache.commons.geometry.euclidean.threed.Triangle3D;
 import org.apache.commons.geometry.euclidean.threed.Vector3D;
 import org.apache.commons.geometry.io.core.utils.AbstractTextFormatWriter;
@@ -131,7 +131,7 @@
      * @throws IOException if an I/O error occurs
      */
     public void writeTriangles(final List<Vector3D> vertices, final Vector3D normal) throws IOException {
-        for (final List<Vector3D> triangle : Planes.convexPolygonToTriangleFan(vertices, t -> t)) {
+        for (final List<Vector3D> triangle : EuclideanUtils.convexPolygonToTriangleFan(vertices, t -> t)) {
             writeTriangle(
                     triangle.get(0),
                     triangle.get(1),