View Javadoc

    1   /*
    2    * Licensed to the Apache Software Foundation (ASF) under one or more
    3    * contributor license agreements.  See the NOTICE file distributed with
    4    * this work for additional information regarding copyright ownership.
    5    * The ASF licenses this file to You under the Apache License, Version 2.0
    6    * (the "License"); you may not use this file except in compliance with
    7    * the License.  You may obtain a copy of the License at
    8    *
    9    *      http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  package org.apache.commons.geometry.euclidean.threed.shape;
    18
    19  import java.text.MessageFormat;
    20  import java.util.Arrays;
    21  import java.util.List;
    22  import java.util.stream.Collectors;
    23
    24  import org.apache.commons.geometry.core.Transform;
    25  import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
    26  import org.apache.commons.geometry.euclidean.threed.AffineTransformMatrix3D;
    27  import org.apache.commons.geometry.euclidean.threed.ConvexVolume;
    28  import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
    29  import org.apache.commons.geometry.euclidean.threed.Planes;
    30  import org.apache.commons.geometry.euclidean.threed.Vector3D;
    31  import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
    32
    33  /** Class representing parallelepipeds, i.e. 3 dimensional figures formed by six
    34   * parallelograms. For example, cubes and rectangular prisms are parallelepipeds.
    35   * @see <a href="https://en.wikipedia.org/wiki/Parallelepiped">Parallelepiped</a>
    36   */
    37  public final class Parallelepiped extends ConvexVolume {
    38
    39      /** Vertices defining a cube with sides of length 1 centered at the origin. */
    40      private static final List<Vector3D> UNIT_CUBE_VERTICES = Arrays.asList(
    41                  Vector3D.of(-0.5, -0.5, -0.5),
    42                  Vector3D.of(0.5, -0.5, -0.5),
    43                  Vector3D.of(0.5, 0.5, -0.5),
    44                  Vector3D.of(-0.5, 0.5, -0.5),
    45
    46                  Vector3D.of(-0.5, -0.5, 0.5),
    47                  Vector3D.of(0.5, -0.5, 0.5),
    48                  Vector3D.of(0.5, 0.5, 0.5),
    49                  Vector3D.of(-0.5, 0.5, 0.5)
    50              );
    51
    52      /** Simple constructor. Callers are responsible for ensuring that the given boundaries
    53       * represent a parallelepiped. No validation is performed.
    54       * @param boundaries the boundaries of the parallelepiped; this must be a list
    55       *      with 6 elements
    56       */
    57      private Parallelepiped(final List<PlaneConvexSubset> boundaries) {
    58          super(boundaries);
    59      }
    60
    61      /** Construct a new instance representing a unit cube centered at the origin. The vertices of this
    62       * cube are:
    63       * <pre>
    64       * [
    65       *      (-0.5, -0.5, -0.5),
    66       *      (0.5, -0.5, -0.5),
    67       *      (0.5, 0.5, -0.5),
    68       *      (-0.5, 0.5, -0.5),
    69       *
    70       *      (-0.5, -0.5, 0.5),
    71       *      (0.5, -0.5, 0.5),
    72       *      (0.5, 0.5, 0.5),
    73       *      (-0.5, 0.5, 0.5)
    74       * ]
    75       * </pre>
    76       * @param precision precision context used to construct boundaries
    77       * @return a new instance representing a unit cube centered at the origin
    78       */
    79      public static Parallelepiped unitCube(final DoublePrecisionContext precision) {
    80          return fromTransformedUnitCube(AffineTransformMatrix3D.identity(), precision);
    81      }
    82
    83      /** Return a new instance representing an axis-aligned parallelepiped, ie, a rectangular prism.
    84       * The points {@code a} and {@code b} are taken to represent opposite corner points in the prism and may be
    85       * specified in any order.
    86       * @param a first corner point in the prism (opposite of {@code b})
    87       * @param b second corner point in the prism (opposite of {@code a})
    88       * @param precision precision context used to construct boundaries
    89       * @return a new instance representing an axis-aligned rectangular prism
    90       * @throws IllegalArgumentException if the width, height, or depth of the defined prism is zero
    91       *      as evaluated by the precision context.
    92       */
    93      public static Parallelepiped axisAligned(final Vector3D a, final Vector3D b,
    94              final DoublePrecisionContext precision) {
    95
    96          final double minX = Math.min(a.getX(), b.getX());
    97          final double maxX = Math.max(a.getX(), b.getX());
    98
    99          final double minY = Math.min(a.getY(), b.getY());
    100         final double maxY = Math.max(a.getY(), b.getY());
    101
    102         final double minZ = Math.min(a.getZ(), b.getZ());
    103         final double maxZ = Math.max(a.getZ(), b.getZ());
    104
    105         final double xDelta = maxX - minX;
    106         final double yDelta = maxY - minY;
    107         final double zDelta = maxZ - minZ;
    108
    109         final Vector3D scale = Vector3D.of(xDelta, yDelta, zDelta);
    110         final Vector3D position = Vector3D.of(
    111                     (0.5 * xDelta) + minX,
    112                     (0.5 * yDelta) + minY,
    113                     (0.5 * zDelta) + minZ
    114                 );
    115
    116         return builder(precision)
    117                 .setScale(scale)
    118                 .setPosition(position)
    119                 .build();
    120     }
    121
    122     /** Construct a new instance by transforming a unit cube centered at the origin. The vertices of
    123      * this input cube are:
    124      * <pre>
    125      * [
    126      *      (-0.5, -0.5, -0.5),
    127      *      (0.5, -0.5, -0.5),
    128      *      (0.5, 0.5, -0.5),
    129      *      (-0.5, 0.5, -0.5),
    130      *
    131      *      (-0.5, -0.5, 0.5),
    132      *      (0.5, -0.5, 0.5),
    133      *      (0.5, 0.5, 0.5),
    134      *      (-0.5, 0.5, 0.5)
    135      * ]
    136      * </pre>
    137      * @param transform transform to apply to the vertices of the unit cube
    138      * @param precision precision context used to construct boundaries
    139      * @return a new instance created by transforming the vertices of a unit cube centered at the origin
    140      * @throws IllegalArgumentException if the width, height, or depth of the defined shape is zero
    141      *      as evaluated by the precision context.
    142      */
    143     public static Parallelepiped fromTransformedUnitCube(final Transform<Vector3D> transform,
    144             final DoublePrecisionContext precision) {
    145
    146         final List<Vector3D> vertices = UNIT_CUBE_VERTICES.stream()
    147                 .map(transform)
    148                 .collect(Collectors.toList());
    149         final boolean reverse = !transform.preservesOrientation();
    150
    151         // check lengths in each dimension
    152         ensureNonZeroSideLength(vertices.get(0), vertices.get(1), precision);
    153         ensureNonZeroSideLength(vertices.get(1), vertices.get(2), precision);
    154         ensureNonZeroSideLength(vertices.get(0), vertices.get(4), precision);
    155
    156         final List<PlaneConvexSubset> boundaries = Arrays.asList(
    157                     // planes orthogonal to x
    158                     createFace(0, 4, 7, 3, vertices, reverse, precision),
    159                     createFace(1, 2, 6, 5, vertices, reverse, precision),
    160
    161                     // planes orthogonal to y
    162                     createFace(0, 1, 5, 4, vertices, reverse, precision),
    163                     createFace(3, 7, 6, 2, vertices, reverse, precision),
    164
    165                     // planes orthogonal to z
    166                     createFace(0, 3, 2, 1, vertices, reverse, precision),
    167                     createFace(4, 5, 6, 7, vertices, reverse, precision)
    168                 );
    169
    170         return new Parallelepiped(boundaries);
    171     }
    172
    173     /** Return a new {@link Builder} instance to use for constructing parallelepipeds.
    174      * @param precision precision context used to create boundaries
    175      * @return a new {@link Builder} instance
    176      */
    177     public static Builder builder(final DoublePrecisionContext precision) {
    178         return new Builder(precision);
    179     }
    180
    181     /** Create a single face of a parallelepiped using the indices of elements in the given vertex list.
    182      * @param a first vertex index
    183      * @param b second vertex index
    184      * @param c third vertex index
    185      * @param d fourth vertex index
    186      * @param vertices list of vertices for the parallelepiped
    187      * @param reverse if true, reverse the orientation of the face
    188      * @param precision precision context used to create the face
    189      * @return a parallelepiped face created from the indexed vertices
    190      */
    191     private static PlaneConvexSubset createFace(final int a, final int b, final int c, final int d,
    192             final List<Vector3D> vertices, final boolean reverse, final DoublePrecisionContext precision) {
    193
    194         final Vector3D pa = vertices.get(a);
    195         final Vector3D pb = vertices.get(b);
    196         final Vector3D pc = vertices.get(c);
    197         final Vector3D pd = vertices.get(d);
    198
    199         final List<Vector3D> loop = reverse ?
    200                 Arrays.asList(pd, pc, pb, pa) :
    201                 Arrays.asList(pa, pb, pc, pd);
    202
    203         return Planes.convexPolygonFromVertices(loop, precision);
    204     }
    205
    206     /** Ensure that the given points defining one side of a parallelepiped face are separated by a non-zero
    207      * distance, as determined by the precision context.
    208      * @param a first vertex
    209      * @param b second vertex
    210      * @param precision precision used to evaluate the distance between the two points
    211      * @throws IllegalArgumentException if the given points are equivalent according to the precision context
    212      */
    213     private static void ensureNonZeroSideLength(final Vector3D a, final Vector3D b,
    214             final DoublePrecisionContext precision) {
    215         if (precision.eqZero(a.distance(b))) {
    216             throw new IllegalArgumentException(MessageFormat.format(
    217                     "Parallelepiped has zero size: vertices {0} and {1} are equivalent", a, b));
    218         }
    219     }
    220
    221     /** Class designed to aid construction of {@link Parallelepiped} instances. Parallelepipeds are constructed
    222      * by transforming the vertices of a unit cube centered at the origin with a transform built from
    223      * the values configured here. The transformations applied are <em>scaling</em>, <em>rotation</em>,
    224      * and <em>translation</em>, in that order. When applied in this order, the scale factors determine
    225      * the width, height, and depth of the parallelepiped; the rotation determines the orientation; and the
    226      * translation determines the position of the center point.
    227      */
    228     public static final class Builder {
    229
    230         /** Amount to scale the parallelepiped. */
    231         private Vector3D scale = Vector3D.of(1, 1, 1);
    232
    233         /** The rotation of the parallelepiped. */
    234         private QuaternionRotation rotation = QuaternionRotation.identity();
    235
    236         /** Amount to translate the parallelepiped. */
    237         private Vector3D position = Vector3D.ZERO;
    238
    239         /** Precision context used to construct boundaries. */
    240         private final DoublePrecisionContext precision;
    241
    242         /** Construct a new instance configured with the given precision context.
    243          * @param precision precision context used to create boundaries
    244          */
    245         private Builder(final DoublePrecisionContext precision) {
    246             this.precision = precision;
    247         }
    248
    249         /** Set the center position of the created parallelepiped.
    250          * @param pos center position of the created parallelepiped
    251          * @return this instance
    252          */
    253         public Builder setPosition(final Vector3D pos) {
    254             this.position = pos;
    255             return this;
    256         }
    257
    258         /** Set the scaling for the created parallelepiped. The scale values determine
    259          * the lengths of the respective sides in the created parallelepiped.
    260          * @param scaleFactors scale factors
    261          * @return this instance
    262          */
    263         public Builder setScale(final Vector3D scaleFactors) {
    264             this.scale = scaleFactors;
    265             return this;
    266         }
    267
    268         /** Set the scaling for the created parallelepiped. The scale values determine
    269          * the lengths of the respective sides in the created parallelepiped.
    270          * @param x x scale factor
    271          * @param y y scale factor
    272          * @param z z scale factor
    273          * @return this instance
    274          */
    275         public Builder setScale(final double x, final double y, final double z) {
    276             return setScale(Vector3D.of(x, y, z));
    277         }
    278
    279         /** Set the scaling for the created parallelepiped. The given scale factor is applied
    280          * to the x, y, and z directions.
    281          * @param scaleFactor scale factor for the x, y, and z directions
    282          * @return this instance
    283          */
    284         public Builder setScale(final double scaleFactor) {
    285             return setScale(scaleFactor, scaleFactor, scaleFactor);
    286         }
    287
    288         /** Set the rotation of the created parallelepiped.
    289          * @param rot the rotation of the created parallelepiped
    290          * @return this instance
    291          */
    292         public Builder setRotation(final QuaternionRotation rot) {
    293             this.rotation = rot;
    294             return this;
    295         }
    296
    297         /** Build a new parallelepiped instance with the values configured in this builder.
    298          * @return a new parallelepiped instance
    299          * @throws IllegalArgumentException if the length of any side of the parallelepiped is zero,
    300          *      as determined by the configured precision context
    301          * @see Parallelepiped#fromTransformedUnitCube(Transform, DoublePrecisionContext)
    302          */
    303         public Parallelepiped build() {
    304             final AffineTransformMatrix3D transform = AffineTransformMatrix3D.createScale(scale)
    305                     .rotate(rotation)
    306                     .translate(position);
    307
    308             return fromTransformedUnitCube(transform, precision);
    309         }
    310     }
    311 }