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.euclidean.threed.AffineTransformMatrix3D;
026import org.apache.commons.geometry.euclidean.threed.ConvexVolume;
027import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
028import org.apache.commons.geometry.euclidean.threed.Planes;
029import org.apache.commons.geometry.euclidean.threed.Vector3D;
030import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
031import org.apache.commons.numbers.core.Precision;
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 Precision.DoubleEquivalence 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 Precision.DoubleEquivalence 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 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}