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 }