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.euclidean.threed.AffineTransformMatrix3D;
    26  import org.apache.commons.geometry.euclidean.threed.ConvexVolume;
    27  import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
    28  import org.apache.commons.geometry.euclidean.threed.Planes;
    29  import org.apache.commons.geometry.euclidean.threed.Vector3D;
    30  import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
    31  import org.apache.commons.numbers.core.Precision;
    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 Precision.DoubleEquivalence 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 Precision.DoubleEquivalence 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 Precision.DoubleEquivalence 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 Precision.DoubleEquivalence 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<? extends Vector3D> vertices, final boolean reverse,
    193             final Precision.DoubleEquivalence precision) {
    194
    195         final Vector3D pa = vertices.get(a);
    196         final Vector3D pb = vertices.get(b);
    197         final Vector3D pc = vertices.get(c);
    198         final Vector3D pd = vertices.get(d);
    199
    200         final List<Vector3D> loop = reverse ?
    201                 Arrays.asList(pd, pc, pb, pa) :
    202                 Arrays.asList(pa, pb, pc, pd);
    203
    204         return Planes.convexPolygonFromVertices(loop, precision);
    205     }
    206
    207     /** Ensure that the given points defining one side of a parallelepiped face are separated by a non-zero
    208      * distance, as determined by the precision context.
    209      * @param a first vertex
    210      * @param b second vertex
    211      * @param precision precision used to evaluate the distance between the two points
    212      * @throws IllegalArgumentException if the given points are equivalent according to the precision context
    213      */
    214     private static void ensureNonZeroSideLength(final Vector3D a, final Vector3D b,
    215             final Precision.DoubleEquivalence precision) {
    216         if (precision.eqZero(a.distance(b))) {
    217             throw new IllegalArgumentException(MessageFormat.format(
    218                     "Parallelepiped has zero size: vertices {0} and {1} are equivalent", a, b));
    219         }
    220     }
    221
    222     /** Class designed to aid construction of {@link Parallelepiped} instances. Parallelepipeds are constructed
    223      * by transforming the vertices of a unit cube centered at the origin with a transform built from
    224      * the values configured here. The transformations applied are <em>scaling</em>, <em>rotation</em>,
    225      * and <em>translation</em>, in that order. When applied in this order, the scale factors determine
    226      * the width, height, and depth of the parallelepiped; the rotation determines the orientation; and the
    227      * translation determines the position of the center point.
    228      */
    229     public static final class Builder {
    230
    231         /** Amount to scale the parallelepiped. */
    232         private Vector3D scale = Vector3D.of(1, 1, 1);
    233
    234         /** The rotation of the parallelepiped. */
    235         private QuaternionRotation rotation = QuaternionRotation.identity();
    236
    237         /** Amount to translate the parallelepiped. */
    238         private Vector3D position = Vector3D.ZERO;
    239
    240         /** Precision context used to construct boundaries. */
    241         private final Precision.DoubleEquivalence precision;
    242
    243         /** Construct a new instance configured with the given precision context.
    244          * @param precision precision context used to create boundaries
    245          */
    246         private Builder(final Precision.DoubleEquivalence precision) {
    247             this.precision = precision;
    248         }
    249
    250         /** Set the center position of the created parallelepiped.
    251          * @param pos center position of the created parallelepiped
    252          * @return this instance
    253          */
    254         public Builder setPosition(final Vector3D pos) {
    255             this.position = pos;
    256             return this;
    257         }
    258
    259         /** Set the scaling for the created parallelepiped. The scale values determine
    260          * the lengths of the respective sides in the created parallelepiped.
    261          * @param scaleFactors scale factors
    262          * @return this instance
    263          */
    264         public Builder setScale(final Vector3D scaleFactors) {
    265             this.scale = scaleFactors;
    266             return this;
    267         }
    268
    269         /** Set the scaling for the created parallelepiped. The scale values determine
    270          * the lengths of the respective sides in the created parallelepiped.
    271          * @param x x scale factor
    272          * @param y y scale factor
    273          * @param z z scale factor
    274          * @return this instance
    275          */
    276         public Builder setScale(final double x, final double y, final double z) {
    277             return setScale(Vector3D.of(x, y, z));
    278         }
    279
    280         /** Set the scaling for the created parallelepiped. The given scale factor is applied
    281          * to the x, y, and z directions.
    282          * @param scaleFactor scale factor for the x, y, and z directions
    283          * @return this instance
    284          */
    285         public Builder setScale(final double scaleFactor) {
    286             return setScale(scaleFactor, scaleFactor, scaleFactor);
    287         }
    288
    289         /** Set the rotation of the created parallelepiped.
    290          * @param rot the rotation of the created parallelepiped
    291          * @return this instance
    292          */
    293         public Builder setRotation(final QuaternionRotation rot) {
    294             this.rotation = rot;
    295             return this;
    296         }
    297
    298         /** Build a new parallelepiped instance with the values configured in this builder.
    299          * @return a new parallelepiped instance
    300          * @throws IllegalArgumentException if the length of any side of the parallelepiped is zero,
    301          *      as determined by the configured precision context
    302          * @see Parallelepiped#fromTransformedUnitCube(Transform, Precision.DoubleEquivalence)
    303          */
    304         public Parallelepiped build() {
    305             final AffineTransformMatrix3D transform = AffineTransformMatrix3D.createScale(scale)
    306                     .rotate(rotation)
    307                     .translate(position);
    308
    309             return fromTransformedUnitCube(transform, precision);
    310         }
    311     }
    312 }