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 }