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 }