Parallelepiped.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.geometry.euclidean.threed.shape;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.geometry.core.Transform;
import org.apache.commons.geometry.euclidean.threed.AffineTransformMatrix3D;
import org.apache.commons.geometry.euclidean.threed.ConvexVolume;
import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
import org.apache.commons.geometry.euclidean.threed.Planes;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
import org.apache.commons.numbers.core.Precision;
/** Class representing parallelepipeds, i.e. 3 dimensional figures formed by six
* parallelograms. For example, cubes and rectangular prisms are parallelepipeds.
* @see <a href="https://en.wikipedia.org/wiki/Parallelepiped">Parallelepiped</a>
*/
public final class Parallelepiped extends ConvexVolume {
/** Vertices defining a cube with sides of length 1 centered at the origin. */
private static final List<Vector3D> UNIT_CUBE_VERTICES = Arrays.asList(
Vector3D.of(-0.5, -0.5, -0.5),
Vector3D.of(0.5, -0.5, -0.5),
Vector3D.of(0.5, 0.5, -0.5),
Vector3D.of(-0.5, 0.5, -0.5),
Vector3D.of(-0.5, -0.5, 0.5),
Vector3D.of(0.5, -0.5, 0.5),
Vector3D.of(0.5, 0.5, 0.5),
Vector3D.of(-0.5, 0.5, 0.5)
);
/** Simple constructor. Callers are responsible for ensuring that the given boundaries
* represent a parallelepiped. No validation is performed.
* @param boundaries the boundaries of the parallelepiped; this must be a list
* with 6 elements
*/
private Parallelepiped(final List<PlaneConvexSubset> boundaries) {
super(boundaries);
}
/** Construct a new instance representing a unit cube centered at the origin. The vertices of this
* cube are:
* <pre>
* [
* (-0.5, -0.5, -0.5),
* (0.5, -0.5, -0.5),
* (0.5, 0.5, -0.5),
* (-0.5, 0.5, -0.5),
*
* (-0.5, -0.5, 0.5),
* (0.5, -0.5, 0.5),
* (0.5, 0.5, 0.5),
* (-0.5, 0.5, 0.5)
* ]
* </pre>
* @param precision precision context used to construct boundaries
* @return a new instance representing a unit cube centered at the origin
*/
public static Parallelepiped unitCube(final Precision.DoubleEquivalence precision) {
return fromTransformedUnitCube(AffineTransformMatrix3D.identity(), precision);
}
/** Return a new instance representing an axis-aligned parallelepiped, ie, a rectangular prism.
* The points {@code a} and {@code b} are taken to represent opposite corner points in the prism and may be
* specified in any order.
* @param a first corner point in the prism (opposite of {@code b})
* @param b second corner point in the prism (opposite of {@code a})
* @param precision precision context used to construct boundaries
* @return a new instance representing an axis-aligned rectangular prism
* @throws IllegalArgumentException if the width, height, or depth of the defined prism is zero
* as evaluated by the precision context.
*/
public static Parallelepiped axisAligned(final Vector3D a, final Vector3D b,
final Precision.DoubleEquivalence precision) {
final double minX = Math.min(a.getX(), b.getX());
final double maxX = Math.max(a.getX(), b.getX());
final double minY = Math.min(a.getY(), b.getY());
final double maxY = Math.max(a.getY(), b.getY());
final double minZ = Math.min(a.getZ(), b.getZ());
final double maxZ = Math.max(a.getZ(), b.getZ());
final double xDelta = maxX - minX;
final double yDelta = maxY - minY;
final double zDelta = maxZ - minZ;
final Vector3D scale = Vector3D.of(xDelta, yDelta, zDelta);
final Vector3D position = Vector3D.of(
(0.5 * xDelta) + minX,
(0.5 * yDelta) + minY,
(0.5 * zDelta) + minZ
);
return builder(precision)
.setScale(scale)
.setPosition(position)
.build();
}
/** Construct a new instance by transforming a unit cube centered at the origin. The vertices of
* this input cube are:
* <pre>
* [
* (-0.5, -0.5, -0.5),
* (0.5, -0.5, -0.5),
* (0.5, 0.5, -0.5),
* (-0.5, 0.5, -0.5),
*
* (-0.5, -0.5, 0.5),
* (0.5, -0.5, 0.5),
* (0.5, 0.5, 0.5),
* (-0.5, 0.5, 0.5)
* ]
* </pre>
* @param transform transform to apply to the vertices of the unit cube
* @param precision precision context used to construct boundaries
* @return a new instance created by transforming the vertices of a unit cube centered at the origin
* @throws IllegalArgumentException if the width, height, or depth of the defined shape is zero
* as evaluated by the precision context.
*/
public static Parallelepiped fromTransformedUnitCube(final Transform<Vector3D> transform,
final Precision.DoubleEquivalence precision) {
final List<Vector3D> vertices = UNIT_CUBE_VERTICES.stream()
.map(transform)
.collect(Collectors.toList());
final boolean reverse = !transform.preservesOrientation();
// check lengths in each dimension
ensureNonZeroSideLength(vertices.get(0), vertices.get(1), precision);
ensureNonZeroSideLength(vertices.get(1), vertices.get(2), precision);
ensureNonZeroSideLength(vertices.get(0), vertices.get(4), precision);
final List<PlaneConvexSubset> boundaries = Arrays.asList(
// planes orthogonal to x
createFace(0, 4, 7, 3, vertices, reverse, precision),
createFace(1, 2, 6, 5, vertices, reverse, precision),
// planes orthogonal to y
createFace(0, 1, 5, 4, vertices, reverse, precision),
createFace(3, 7, 6, 2, vertices, reverse, precision),
// planes orthogonal to z
createFace(0, 3, 2, 1, vertices, reverse, precision),
createFace(4, 5, 6, 7, vertices, reverse, precision)
);
return new Parallelepiped(boundaries);
}
/** Return a new {@link Builder} instance to use for constructing parallelepipeds.
* @param precision precision context used to create boundaries
* @return a new {@link Builder} instance
*/
public static Builder builder(final Precision.DoubleEquivalence precision) {
return new Builder(precision);
}
/** Create a single face of a parallelepiped using the indices of elements in the given vertex list.
* @param a first vertex index
* @param b second vertex index
* @param c third vertex index
* @param d fourth vertex index
* @param vertices list of vertices for the parallelepiped
* @param reverse if true, reverse the orientation of the face
* @param precision precision context used to create the face
* @return a parallelepiped face created from the indexed vertices
*/
private static PlaneConvexSubset createFace(final int a, final int b, final int c, final int d,
final List<? extends Vector3D> vertices, final boolean reverse,
final Precision.DoubleEquivalence precision) {
final Vector3D pa = vertices.get(a);
final Vector3D pb = vertices.get(b);
final Vector3D pc = vertices.get(c);
final Vector3D pd = vertices.get(d);
final List<Vector3D> loop = reverse ?
Arrays.asList(pd, pc, pb, pa) :
Arrays.asList(pa, pb, pc, pd);
return Planes.convexPolygonFromVertices(loop, precision);
}
/** Ensure that the given points defining one side of a parallelepiped face are separated by a non-zero
* distance, as determined by the precision context.
* @param a first vertex
* @param b second vertex
* @param precision precision used to evaluate the distance between the two points
* @throws IllegalArgumentException if the given points are equivalent according to the precision context
*/
private static void ensureNonZeroSideLength(final Vector3D a, final Vector3D b,
final Precision.DoubleEquivalence precision) {
if (precision.eqZero(a.distance(b))) {
throw new IllegalArgumentException(MessageFormat.format(
"Parallelepiped has zero size: vertices {0} and {1} are equivalent", a, b));
}
}
/** Class designed to aid construction of {@link Parallelepiped} instances. Parallelepipeds are constructed
* by transforming the vertices of a unit cube centered at the origin with a transform built from
* the values configured here. The transformations applied are <em>scaling</em>, <em>rotation</em>,
* and <em>translation</em>, in that order. When applied in this order, the scale factors determine
* the width, height, and depth of the parallelepiped; the rotation determines the orientation; and the
* translation determines the position of the center point.
*/
public static final class Builder {
/** Amount to scale the parallelepiped. */
private Vector3D scale = Vector3D.of(1, 1, 1);
/** The rotation of the parallelepiped. */
private QuaternionRotation rotation = QuaternionRotation.identity();
/** Amount to translate the parallelepiped. */
private Vector3D position = Vector3D.ZERO;
/** Precision context used to construct boundaries. */
private final Precision.DoubleEquivalence precision;
/** Construct a new instance configured with the given precision context.
* @param precision precision context used to create boundaries
*/
private Builder(final Precision.DoubleEquivalence precision) {
this.precision = precision;
}
/** Set the center position of the created parallelepiped.
* @param pos center position of the created parallelepiped
* @return this instance
*/
public Builder setPosition(final Vector3D pos) {
this.position = pos;
return this;
}
/** Set the scaling for the created parallelepiped. The scale values determine
* the lengths of the respective sides in the created parallelepiped.
* @param scaleFactors scale factors
* @return this instance
*/
public Builder setScale(final Vector3D scaleFactors) {
this.scale = scaleFactors;
return this;
}
/** Set the scaling for the created parallelepiped. The scale values determine
* the lengths of the respective sides in the created parallelepiped.
* @param x x scale factor
* @param y y scale factor
* @param z z scale factor
* @return this instance
*/
public Builder setScale(final double x, final double y, final double z) {
return setScale(Vector3D.of(x, y, z));
}
/** Set the scaling for the created parallelepiped. The given scale factor is applied
* to the x, y, and z directions.
* @param scaleFactor scale factor for the x, y, and z directions
* @return this instance
*/
public Builder setScale(final double scaleFactor) {
return setScale(scaleFactor, scaleFactor, scaleFactor);
}
/** Set the rotation of the created parallelepiped.
* @param rot the rotation of the created parallelepiped
* @return this instance
*/
public Builder setRotation(final QuaternionRotation rot) {
this.rotation = rot;
return this;
}
/** Build a new parallelepiped instance with the values configured in this builder.
* @return a new parallelepiped instance
* @throws IllegalArgumentException if the length of any side of the parallelepiped is zero,
* as determined by the configured precision context
* @see Parallelepiped#fromTransformedUnitCube(Transform, Precision.DoubleEquivalence)
*/
public Parallelepiped build() {
final AffineTransformMatrix3D transform = AffineTransformMatrix3D.createScale(scale)
.rotate(rotation)
.translate(position);
return fromTransformedUnitCube(transform, precision);
}
}
}