001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.geometry.euclidean.threed.shape;
018
019import java.text.MessageFormat;
020import java.util.List;
021import java.util.stream.Collectors;
022import java.util.stream.Stream;
023
024import org.apache.commons.geometry.core.partitioning.bsp.RegionCutRule;
025import org.apache.commons.geometry.euclidean.AbstractNSphere;
026import org.apache.commons.geometry.euclidean.threed.Plane;
027import org.apache.commons.geometry.euclidean.threed.Planes;
028import org.apache.commons.geometry.euclidean.threed.RegionBSPTree3D;
029import org.apache.commons.geometry.euclidean.threed.RegionBSPTree3D.RegionNode3D;
030import org.apache.commons.geometry.euclidean.threed.Vector3D;
031import org.apache.commons.geometry.euclidean.threed.line.Line3D;
032import org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D;
033import org.apache.commons.geometry.euclidean.threed.line.LinecastPoint3D;
034import org.apache.commons.geometry.euclidean.threed.line.Linecastable3D;
035import org.apache.commons.geometry.euclidean.threed.mesh.SimpleTriangleMesh;
036import org.apache.commons.geometry.euclidean.threed.mesh.TriangleMesh;
037import org.apache.commons.numbers.core.Precision;
038
039/** Class representing a 3 dimensional sphere in Euclidean space.
040 */
041public final class Sphere extends AbstractNSphere<Vector3D> implements Linecastable3D {
042
043    /** Message used when requesting a sphere approximation with an invalid subdivision number. */
044    private static final String INVALID_SUBDIVISION_MESSAGE =
045        "Number of sphere approximation subdivisions must be greater than or equal to zero; was {0}";
046
047    /** Constant equal to {@code 4 * pi}. */
048    private static final double FOUR_PI = 4.0 * Math.PI;
049
050    /** Constant equal to {@code (4/3) * pi}. */
051    private static final double FOUR_THIRDS_PI = FOUR_PI / 3.0;
052
053    /** Construct a new sphere from its component parts.
054     * @param center the center of the sphere
055     * @param radius the sphere radius
056     * @param precision precision context used to compare floating point numbers
057     * @throws IllegalArgumentException if center is not finite or radius is not finite or is
058     *      less than or equal to zero as evaluated by the given precision context
059     */
060    private Sphere(final Vector3D center, final double radius, final Precision.DoubleEquivalence precision) {
061        super(center, radius, precision);
062    }
063
064    /** {@inheritDoc} */
065    @Override
066    public double getSize() {
067        final double r = getRadius();
068        return FOUR_THIRDS_PI * r * r * r;
069    }
070
071    /** {@inheritDoc} */
072    @Override
073    public double getBoundarySize() {
074        final double r = getRadius();
075        return FOUR_PI * r * r;
076    }
077
078    /** {@inheritDoc} */
079    @Override
080    public Vector3D project(final Vector3D pt) {
081        return project(pt, Vector3D.Unit.PLUS_X);
082    }
083
084    /** Build an approximation of this sphere using a {@link RegionBSPTree3D}. The approximation is constructed by
085     * taking an octahedron (8-sided polyhedron with triangular faces) inscribed in the sphere and subdividing each
086     * triangular face {@code subdivisions} number of times, each time projecting the newly created vertices onto the
087     * sphere surface. Each triangle subdivision produces 4 triangles, meaning that the total number of triangles
088     * inserted into tree is equal to \(8 \times 4^s\), where \(s\) is the number of subdivisions. For
089     * example, calling this method with {@code subdivisions} equal to {@code 3} will produce a tree having
090     * \(8 \times 4^3 = 512\) triangular facets inserted. See the table below for other examples. The returned BSP
091     * tree also contains structural cuts to reduce the overall height of the tree.
092     *
093     * <table>
094     *  <caption>Subdivisions to Triangle Counts</caption>
095     *  <thead>
096     *      <tr>
097     *          <th>Subdivisions</th>
098     *          <th>Triangles</th>
099     *      </tr>
100     *  </thead>
101     *  <tbody>
102     *      <tr><td>0</td><td>8</td></tr>
103     *      <tr><td>1</td><td>32</td></tr>
104     *      <tr><td>2</td><td>128</td></tr>
105     *      <tr><td>3</td><td>512</td></tr>
106     *      <tr><td>4</td><td>2048</td></tr>
107     *      <tr><td>5</td><td>8192</td></tr>
108     *  </tbody>
109     * </table>
110     *
111     * <p>Care must be taken when using this method with large subdivision numbers so that floating point errors
112     * do not interfere with the creation of the planes and triangles in the tree. For example, if the number of
113     * subdivisions is too high, the subdivided triangle points may become equivalent according to the sphere's
114     * {@link #getPrecision() precision context} and plane creation may fail. Or plane creation may succeed but
115     * insertion of the plane into the tree may fail for similar reasons. In general, it is best to use the lowest
116     * subdivision number practical for the intended purpose.</p>
117     * @param subdivisions the number of triangle subdivisions to use when creating the tree; the total number of
118     *      triangular facets inserted into the returned tree is equal to \(8 \times 4^s\), where \(s\) is the number
119     *      of subdivisions
120     * @return a BSP tree containing an approximation of the sphere
121     * @throws IllegalArgumentException if {@code subdivisions} is less than zero
122     * @throws IllegalStateException if tree creation fails for the given subdivision count
123     * @see #toTriangleMesh(int)
124     */
125    public RegionBSPTree3D toTree(final int subdivisions) {
126        if (subdivisions < 0) {
127            throw new IllegalArgumentException(MessageFormat.format(INVALID_SUBDIVISION_MESSAGE, subdivisions));
128        }
129        return new SphereTreeApproximationBuilder(this, subdivisions).build();
130    }
131
132    /** Build an approximation of this sphere using a {@link TriangleMesh}. The approximation is constructed by
133     * taking an octahedron (8-sided polyhedron with triangular faces) inscribed in the sphere and subdividing each
134     * triangular face {@code subdivisions} number of times, each time projecting the newly created vertices onto the
135     * sphere surface. Each triangle subdivision produces 4 triangles, meaning that the total number of triangles
136     * in the returned mesh is equal to \(8 \times 4^s\), where \(s\) is the number of subdivisions. For
137     * example, calling this method with {@code subdivisions} equal to {@code 3} will produce a mesh having
138     * \(8 \times 4^3 = 512\) triangular facets inserted. See the table below for other examples.
139     *
140     * <table>
141     *  <caption>Subdivisions to Triangle Counts</caption>
142     *  <thead>
143     *      <tr>
144     *          <th>Subdivisions</th>
145     *          <th>Triangles</th>
146     *      </tr>
147     *  </thead>
148     *  <tbody>
149     *      <tr><td>0</td><td>8</td></tr>
150     *      <tr><td>1</td><td>32</td></tr>
151     *      <tr><td>2</td><td>128</td></tr>
152     *      <tr><td>3</td><td>512</td></tr>
153     *      <tr><td>4</td><td>2048</td></tr>
154     *      <tr><td>5</td><td>8192</td></tr>
155     *  </tbody>
156     * </table>
157     *
158     * <p><strong>BSP Tree Conversion</strong></p>
159     * <p>Inserting the boundaries of a sphere mesh approximation directly into a BSP tree will invariably result
160     * in poor performance: since the region is convex the constructed BSP tree degenerates into a simple linked
161     * list of nodes. If a BSP tree is needed, users should prefer the {@link #toTree(int)} method, which creates
162     * balanced tree approximations directly, or the {@link RegionBSPTree3D.PartitionedRegionBuilder3D} class,
163     * which can be used to insert the mesh faces into a pre-partitioned tree.
164     * </p>
165     * @param subdivisions the number of triangle subdivisions to use when creating the mesh; the total number of
166     *      triangular faces in the returned mesh is equal to \(8 \times 4^s\), where \(s\) is the number
167     *      of subdivisions
168     * @return a triangle mesh approximation of the sphere
169     * @throws IllegalArgumentException if {@code subdivisions} is less than zero
170     * @see #toTree(int)
171     */
172    public TriangleMesh toTriangleMesh(final int subdivisions) {
173        if (subdivisions < 0) {
174            throw new IllegalArgumentException(MessageFormat.format(INVALID_SUBDIVISION_MESSAGE, subdivisions));
175        }
176        return new SphereMeshApproximationBuilder(this, subdivisions).build();
177    }
178
179    /** Get the intersections of the given line with this sphere. The returned list will
180     * contain either 0, 1, or 2 points.
181     * <ul>
182     *      <li><strong>2 points</strong> - The line is a secant line and intersects the sphere at two
183     *      distinct points. The points are ordered such that the first point in the list is the first point
184     *      encountered when traveling in the direction of the line. (In other words, the points are ordered
185     *      by increasing abscissa value.)
186     *      </li>
187     *      <li><strong>1 point</strong> - The line is a tangent line and only intersects the sphere at a
188     *      single point (as evaluated by the sphere's precision context).
189     *      </li>
190     *      <li><strong>0 points</strong> - The line does not intersect the sphere.</li>
191     * </ul>
192     * @param line line to intersect with the sphere
193     * @return a list of intersection points between the given line and this sphere
194     */
195    public List<Vector3D> intersections(final Line3D line) {
196        return intersections(line, Line3D::abscissa, Line3D::distance);
197    }
198
199    /** Get the first intersection point between the given line and this sphere, or null
200     * if no such point exists. The "first" intersection point is the first such point
201     * encountered when traveling in the direction of the line from infinity.
202     * @param line line to intersect with the sphere
203     * @return the first intersection point between the given line and this instance or
204     *      null if no such point exists
205     */
206    public Vector3D firstIntersection(final Line3D line) {
207        return firstIntersection(line, Line3D::abscissa, Line3D::distance);
208    }
209
210    /** {@inheritDoc} */
211    @Override
212    public List<LinecastPoint3D> linecast(final LineConvexSubset3D subset) {
213        return getLinecastStream(subset)
214                .collect(Collectors.toList());
215    }
216
217    /** {@inheritDoc} */
218    @Override
219    public LinecastPoint3D linecastFirst(final LineConvexSubset3D subset) {
220        return getLinecastStream(subset)
221                .findFirst()
222                .orElse(null);
223    }
224
225    /** Get a stream containing the linecast intersection points of the given
226     * line subset with this instance.
227     * @param subset line subset to intersect against this instance
228     * @return a stream containing linecast intersection points
229     */
230    private Stream<LinecastPoint3D> getLinecastStream(final LineConvexSubset3D subset) {
231        return intersections(subset.getLine()).stream()
232            .filter(subset::contains)
233            .map(pt -> new LinecastPoint3D(pt, getCenter().directionTo(pt), subset.getLine()));
234    }
235
236    /** Construct a sphere from a center point and radius.
237     * @param center the center of the sphere
238     * @param radius the sphere radius
239     * @param precision precision context used to compare floating point numbers
240     * @return a sphere constructed from the given center point and radius
241     * @throws IllegalArgumentException if center is not finite or radius is not finite or is
242     *      less than or equal to zero as evaluated by the given precision context
243     */
244    public static Sphere from(final Vector3D center, final double radius, final Precision.DoubleEquivalence precision) {
245        return new Sphere(center, radius, precision);
246    }
247
248    /** Internal class used to construct hyperplane-bounded approximations of spheres as BSP trees. The class
249     * begins with an octahedron inscribed in the sphere and then subdivides each triangular face a specified
250     * number of times.
251     */
252    private static final class SphereTreeApproximationBuilder {
253
254        /** Threshold used to determine when to stop inserting structural cuts and begin adding facets. */
255        private static final int PARTITION_THRESHOLD = 2;
256
257        /** The sphere that an approximation is being created for. */
258        private final Sphere sphere;
259
260        /** The number of triangular subdivisions to use. */
261        private final int subdivisions;
262
263        /** Construct a new builder for creating a BSP tree approximation of the given sphere.
264         * @param sphere the sphere to create an approximation of
265         * @param subdivisions the number of triangle subdivisions to use in tree creation
266         */
267        SphereTreeApproximationBuilder(final Sphere sphere, final int subdivisions) {
268            this.sphere = sphere;
269            this.subdivisions = subdivisions;
270        }
271
272        /** Build the sphere approximation BSP tree.
273         * @return the sphere approximation BSP tree
274         * @throws IllegalStateException if tree creation fails for the configured subdivision count
275         */
276        RegionBSPTree3D build() {
277            final RegionBSPTree3D tree = RegionBSPTree3D.empty();
278
279            final Vector3D center = sphere.getCenter();
280            final double radius = sphere.getRadius();
281            final Precision.DoubleEquivalence precision = sphere.getPrecision();
282
283            // insert the primary split planes
284            final Plane plusXPlane = Planes.fromPointAndNormal(center, Vector3D.Unit.PLUS_X, precision);
285            final Plane plusYPlane = Planes.fromPointAndNormal(center, Vector3D.Unit.PLUS_Y, precision);
286            final Plane plusZPlane = Planes.fromPointAndNormal(center, Vector3D.Unit.PLUS_Z, precision);
287
288            tree.insert(plusXPlane.span(), RegionCutRule.INHERIT);
289            tree.insert(plusYPlane.span(), RegionCutRule.INHERIT);
290            tree.insert(plusZPlane.span(), RegionCutRule.INHERIT);
291
292            // create the vertices for the octahedron
293            final double cx = center.getX();
294            final double cy = center.getY();
295            final double cz = center.getZ();
296
297            final Vector3D maxX = Vector3D.of(cx + radius, cy, cz);
298            final Vector3D minX = Vector3D.of(cx - radius, cy, cz);
299
300            final Vector3D maxY = Vector3D.of(cx, cy + radius, cz);
301            final Vector3D minY = Vector3D.of(cx, cy - radius, cz);
302
303            final Vector3D maxZ = Vector3D.of(cx, cy, cz + radius);
304            final Vector3D minZ = Vector3D.of(cx, cy, cz - radius);
305
306            // partition and subdivide the face triangles and insert them into the tree
307            final RegionNode3D root = tree.getRoot();
308
309            try {
310                partitionAndInsert(root.getMinus().getMinus().getMinus(), minX, minZ, minY, 0);
311                partitionAndInsert(root.getMinus().getMinus().getPlus(), minX, minY, maxZ, 0);
312
313                partitionAndInsert(root.getMinus().getPlus().getMinus(), minX, maxY, minZ, 0);
314                partitionAndInsert(root.getMinus().getPlus().getPlus(), minX, maxZ, maxY, 0);
315
316                partitionAndInsert(root.getPlus().getMinus().getMinus(), maxX, minY, minZ, 0);
317                partitionAndInsert(root.getPlus().getMinus().getPlus(), maxX, maxZ, minY, 0);
318
319                partitionAndInsert(root.getPlus().getPlus().getMinus(), maxX, minZ, maxY, 0);
320                partitionAndInsert(root.getPlus().getPlus().getPlus(), maxX, maxY, maxZ, 0);
321            } catch (final IllegalStateException | IllegalArgumentException exc) {
322                // standardize any tree construction failure as an IllegalStateException
323                throw new IllegalStateException("Failed to construct sphere approximation with subdivision count " +
324                        subdivisions + ": " + exc.getMessage(), exc);
325            }
326
327            return tree;
328        }
329
330        /** Recursively insert structural BSP tree cuts into the given node and then insert subdivided triangles
331         * when a target subdivision level is reached. The structural BSP tree cuts are used to help reduce the
332         * overall depth of the BSP tree.
333         * @param node the node to insert into
334         * @param p1 first triangle point
335         * @param p2 second triangle point
336         * @param p3 third triangle point
337         * @param level current subdivision level
338         */
339        private void partitionAndInsert(final RegionNode3D node,
340                                        final Vector3D p1, final Vector3D p2, final Vector3D p3, final int level) {
341
342            if (subdivisions - level > PARTITION_THRESHOLD) {
343                final int nextLevel = level + 1;
344
345                final Vector3D center = sphere.getCenter();
346
347                final Vector3D m1 = sphere.project(p1.lerp(p2, 0.5));
348                final Vector3D m2 = sphere.project(p2.lerp(p3, 0.5));
349                final Vector3D m3 = sphere.project(p3.lerp(p1, 0.5));
350
351                RegionNode3D curNode = node;
352
353                checkedCut(curNode, createPlane(m3, m2, center), RegionCutRule.INHERIT);
354                partitionAndInsert(curNode.getPlus(), m3, m2, p3, nextLevel);
355
356                curNode = curNode.getMinus();
357                checkedCut(curNode, createPlane(m2, m1, center), RegionCutRule.INHERIT);
358                partitionAndInsert(curNode.getPlus(), m1, p2, m2, nextLevel);
359
360                curNode = curNode.getMinus();
361                checkedCut(curNode, createPlane(m1, m3, center), RegionCutRule.INHERIT);
362                partitionAndInsert(curNode.getPlus(), p1, m1, m3, nextLevel);
363
364                partitionAndInsert(curNode.getMinus(), m1, m2, m3, nextLevel);
365            } else {
366                insertSubdividedTriangles(node, p1, p2, p3, level);
367            }
368        }
369
370        /** Recursively insert subdivided triangles into the given node. Each triangle is inserted into the minus
371         * side of the previous triangle.
372         * @param node the node to insert into
373         * @param p1 first triangle point
374         * @param p2 second triangle point
375         * @param p3 third triangle point
376         * @param level the current subdivision level
377         * @return the node representing the inside of the region after insertion of all triangles
378         */
379        private RegionNode3D insertSubdividedTriangles(final RegionNode3D node,
380                                                       final Vector3D p1, final Vector3D p2, final Vector3D p3,
381                                                       final int level) {
382
383            if (level >= subdivisions) {
384                // base case
385                checkedCut(node, createPlane(p1, p2, p3), RegionCutRule.MINUS_INSIDE);
386                return node.getMinus();
387            } else {
388                final int nextLevel = level + 1;
389
390                final Vector3D m1 = sphere.project(p1.lerp(p2, 0.5));
391                final Vector3D m2 = sphere.project(p2.lerp(p3, 0.5));
392                final Vector3D m3 = sphere.project(p3.lerp(p1, 0.5));
393
394                RegionNode3D curNode = node;
395                curNode = insertSubdividedTriangles(curNode, p1, m1, m3, nextLevel);
396                curNode = insertSubdividedTriangles(curNode, m1, p2, m2, nextLevel);
397                curNode = insertSubdividedTriangles(curNode, m3, m2, p3, nextLevel);
398                curNode = insertSubdividedTriangles(curNode, m1, m2, m3, nextLevel);
399
400                return curNode;
401            }
402        }
403
404        /** Create a plane from the given points, using the precision context of the sphere.
405         * @param p1 first point
406         * @param p2 second point
407         * @param p3 third point
408         * @return a plane defined by the given points
409         */
410        private Plane createPlane(final Vector3D p1, final Vector3D p2, final Vector3D p3) {
411            return Planes.fromPoints(p1, p2, p3, sphere.getPrecision());
412        }
413
414        /** Insert the cut into the given node, throwing an exception if no portion of the cutter intersects
415         * the node.
416         * @param node node to cut
417         * @param cutter plane to use to cut the node
418         * @param cutRule cut rule to apply
419         * @throws IllegalStateException if no portion of the cutter plane intersects the node
420         */
421        private void checkedCut(final RegionNode3D node, final Plane cutter, final RegionCutRule cutRule) {
422            if (!node.insertCut(cutter, cutRule)) {
423                throw new IllegalStateException("Failed to cut BSP tree node with plane: " + cutter);
424            }
425        }
426    }
427
428    /** Internal class used to construct geodesic mesh sphere approximations. The class begins with an octahedron
429     * inscribed in the sphere and then subdivides each triangular face a specified number of times.
430     */
431    private static final class SphereMeshApproximationBuilder {
432
433        /** The sphere that an approximation is being created for. */
434        private final Sphere sphere;
435
436        /** The number of triangular subdivisions to use. */
437        private final int subdivisions;
438
439        /** Mesh builder object. */
440        private final SimpleTriangleMesh.Builder builder;
441
442        /** Construct a new builder for creating a mesh approximation of the given sphere.
443         * @param sphere the sphere to create an approximation of
444         * @param subdivisions the number of triangle subdivisions to use in mesh creation
445         */
446        SphereMeshApproximationBuilder(final Sphere sphere, final int subdivisions) {
447            this.sphere = sphere;
448            this.subdivisions = subdivisions;
449            this.builder = SimpleTriangleMesh.builder(sphere.getPrecision());
450        }
451
452        /** Build the mesh approximation of the configured sphere.
453         * @return the mesh approximation of the configured sphere
454         */
455        public SimpleTriangleMesh build() {
456            final Vector3D center = sphere.getCenter();
457            final double radius = sphere.getRadius();
458
459            // create the vertices for the octahedron
460            final double cx = center.getX();
461            final double cy = center.getY();
462            final double cz = center.getZ();
463
464            final Vector3D maxX = Vector3D.of(cx + radius, cy, cz);
465            final Vector3D minX = Vector3D.of(cx - radius, cy, cz);
466
467            final Vector3D maxY = Vector3D.of(cx, cy + radius, cz);
468            final Vector3D minY = Vector3D.of(cx, cy - radius, cz);
469
470            final Vector3D maxZ = Vector3D.of(cx, cy, cz + radius);
471            final Vector3D minZ = Vector3D.of(cx, cy, cz - radius);
472
473            addSubdivided(minX, minZ, minY, 0);
474            addSubdivided(minX, minY, maxZ, 0);
475
476            addSubdivided(minX, maxY, minZ, 0);
477            addSubdivided(minX, maxZ, maxY, 0);
478
479            addSubdivided(maxX, minY, minZ, 0);
480            addSubdivided(maxX, maxZ, minY, 0);
481
482            addSubdivided(maxX, minZ, maxY, 0);
483            addSubdivided(maxX, maxY, maxZ, 0);
484
485            return builder.build();
486        }
487
488        /** Recursively subdivide and add triangular faces between the given outer boundary points.
489         * @param p1 first point
490         * @param p2 second point
491         * @param p3 third point
492         * @param level recursion level; counts up
493         */
494        private void addSubdivided(final Vector3D p1, final Vector3D p2, final Vector3D p3, final int level) {
495            if (level >= subdivisions) {
496                // base case
497                builder.addFaceUsingVertices(p1, p2, p3);
498            } else {
499                // subdivide
500                final int nextLevel = level + 1;
501
502                final Vector3D m1 = sphere.project(p1.lerp(p2, 0.5));
503                final Vector3D m2 = sphere.project(p2.lerp(p3, 0.5));
504                final Vector3D m3 = sphere.project(p3.lerp(p1, 0.5));
505
506                addSubdivided(p1, m1, m3, nextLevel);
507                addSubdivided(m1, p2, m2, nextLevel);
508                addSubdivided(m3, m2, p3, nextLevel);
509                addSubdivided(m1, m2, m3, nextLevel);
510            }
511        }
512    }
513}