001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.geometry.euclidean.threed.shape;
018
019import java.text.MessageFormat;
020import java.util.Arrays;
021import java.util.List;
022import java.util.stream.Collectors;
023
024import org.apache.commons.geometry.core.Transform;
025import org.apache.commons.geometry.core.precision.DoublePrecisionContext;
026import org.apache.commons.geometry.euclidean.threed.AffineTransformMatrix3D;
027import org.apache.commons.geometry.euclidean.threed.ConvexVolume;
028import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
029import org.apache.commons.geometry.euclidean.threed.Planes;
030import org.apache.commons.geometry.euclidean.threed.Vector3D;
031import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
032
033/** Class representing parallelepipeds, i.e. 3 dimensional figures formed by six
034 * parallelograms. For example, cubes and rectangular prisms are parallelepipeds.
035 * @see <a href="https://en.wikipedia.org/wiki/Parallelepiped">Parallelepiped</a>
036 */
037public final class Parallelepiped extends ConvexVolume {
038
039    /** Vertices defining a cube with sides of length 1 centered at the origin. */
040    private static final List<Vector3D> UNIT_CUBE_VERTICES = Arrays.asList(
041                Vector3D.of(-0.5, -0.5, -0.5),
042                Vector3D.of(0.5, -0.5, -0.5),
043                Vector3D.of(0.5, 0.5, -0.5),
044                Vector3D.of(-0.5, 0.5, -0.5),
045
046                Vector3D.of(-0.5, -0.5, 0.5),
047                Vector3D.of(0.5, -0.5, 0.5),
048                Vector3D.of(0.5, 0.5, 0.5),
049                Vector3D.of(-0.5, 0.5, 0.5)
050            );
051
052    /** Simple constructor. Callers are responsible for ensuring that the given boundaries
053     * represent a parallelepiped. No validation is performed.
054     * @param boundaries the boundaries of the parallelepiped; this must be a list
055     *      with 6 elements
056     */
057    private Parallelepiped(final List<PlaneConvexSubset> boundaries) {
058        super(boundaries);
059    }
060
061    /** Construct a new instance representing a unit cube centered at the origin. The vertices of this
062     * cube are:
063     * <pre>
064     * [
065     *      (-0.5, -0.5, -0.5),
066     *      (0.5, -0.5, -0.5),
067     *      (0.5, 0.5, -0.5),
068     *      (-0.5, 0.5, -0.5),
069     *
070     *      (-0.5, -0.5, 0.5),
071     *      (0.5, -0.5, 0.5),
072     *      (0.5, 0.5, 0.5),
073     *      (-0.5, 0.5, 0.5)
074     * ]
075     * </pre>
076     * @param precision precision context used to construct boundaries
077     * @return a new instance representing a unit cube centered at the origin
078     */
079    public static Parallelepiped unitCube(final DoublePrecisionContext precision) {
080        return fromTransformedUnitCube(AffineTransformMatrix3D.identity(), precision);
081    }
082
083    /** Return a new instance representing an axis-aligned parallelepiped, ie, a rectangular prism.
084     * The points {@code a} and {@code b} are taken to represent opposite corner points in the prism and may be
085     * specified in any order.
086     * @param a first corner point in the prism (opposite of {@code b})
087     * @param b second corner point in the prism (opposite of {@code a})
088     * @param precision precision context used to construct boundaries
089     * @return a new instance representing an axis-aligned rectangular prism
090     * @throws IllegalArgumentException if the width, height, or depth of the defined prism is zero
091     *      as evaluated by the precision context.
092     */
093    public static Parallelepiped axisAligned(final Vector3D a, final Vector3D b,
094            final DoublePrecisionContext precision) {
095
096        final double minX = Math.min(a.getX(), b.getX());
097        final double maxX = Math.max(a.getX(), b.getX());
098
099        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}