Parallelepiped.java

  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. import java.text.MessageFormat;
  19. import java.util.Arrays;
  20. import java.util.List;
  21. import java.util.stream.Collectors;

  22. import org.apache.commons.geometry.core.Transform;
  23. import org.apache.commons.geometry.euclidean.threed.AffineTransformMatrix3D;
  24. import org.apache.commons.geometry.euclidean.threed.ConvexVolume;
  25. import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
  26. import org.apache.commons.geometry.euclidean.threed.Planes;
  27. import org.apache.commons.geometry.euclidean.threed.Vector3D;
  28. import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
  29. import org.apache.commons.numbers.core.Precision;

  30. /** Class representing parallelepipeds, i.e. 3 dimensional figures formed by six
  31.  * parallelograms. For example, cubes and rectangular prisms are parallelepipeds.
  32.  * @see <a href="https://en.wikipedia.org/wiki/Parallelepiped">Parallelepiped</a>
  33.  */
  34. public final class Parallelepiped extends ConvexVolume {

  35.     /** Vertices defining a cube with sides of length 1 centered at the origin. */
  36.     private static final List<Vector3D> UNIT_CUBE_VERTICES = Arrays.asList(
  37.                 Vector3D.of(-0.5, -0.5, -0.5),
  38.                 Vector3D.of(0.5, -0.5, -0.5),
  39.                 Vector3D.of(0.5, 0.5, -0.5),
  40.                 Vector3D.of(-0.5, 0.5, -0.5),

  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.     /** Simple constructor. Callers are responsible for ensuring that the given boundaries
  47.      * represent a parallelepiped. No validation is performed.
  48.      * @param boundaries the boundaries of the parallelepiped; this must be a list
  49.      *      with 6 elements
  50.      */
  51.     private Parallelepiped(final List<PlaneConvexSubset> boundaries) {
  52.         super(boundaries);
  53.     }

  54.     /** Construct a new instance representing a unit cube centered at the origin. The vertices of this
  55.      * cube are:
  56.      * <pre>
  57.      * [
  58.      *      (-0.5, -0.5, -0.5),
  59.      *      (0.5, -0.5, -0.5),
  60.      *      (0.5, 0.5, -0.5),
  61.      *      (-0.5, 0.5, -0.5),
  62.      *
  63.      *      (-0.5, -0.5, 0.5),
  64.      *      (0.5, -0.5, 0.5),
  65.      *      (0.5, 0.5, 0.5),
  66.      *      (-0.5, 0.5, 0.5)
  67.      * ]
  68.      * </pre>
  69.      * @param precision precision context used to construct boundaries
  70.      * @return a new instance representing a unit cube centered at the origin
  71.      */
  72.     public static Parallelepiped unitCube(final Precision.DoubleEquivalence precision) {
  73.         return fromTransformedUnitCube(AffineTransformMatrix3D.identity(), precision);
  74.     }

  75.     /** Return a new instance representing an axis-aligned parallelepiped, ie, a rectangular prism.
  76.      * The points {@code a} and {@code b} are taken to represent opposite corner points in the prism and may be
  77.      * specified in any order.
  78.      * @param a first corner point in the prism (opposite of {@code b})
  79.      * @param b second corner point in the prism (opposite of {@code a})
  80.      * @param precision precision context used to construct boundaries
  81.      * @return a new instance representing an axis-aligned rectangular prism
  82.      * @throws IllegalArgumentException if the width, height, or depth of the defined prism is zero
  83.      *      as evaluated by the precision context.
  84.      */
  85.     public static Parallelepiped axisAligned(final Vector3D a, final Vector3D b,
  86.             final Precision.DoubleEquivalence precision) {

  87.         final double minX = Math.min(a.getX(), b.getX());
  88.         final double maxX = Math.max(a.getX(), b.getX());

  89.         final double minY = Math.min(a.getY(), b.getY());
  90.         final double maxY = Math.max(a.getY(), b.getY());

  91.         final double minZ = Math.min(a.getZ(), b.getZ());
  92.         final double maxZ = Math.max(a.getZ(), b.getZ());

  93.         final double xDelta = maxX - minX;
  94.         final double yDelta = maxY - minY;
  95.         final double zDelta = maxZ - minZ;

  96.         final Vector3D scale = Vector3D.of(xDelta, yDelta, zDelta);
  97.         final Vector3D position = Vector3D.of(
  98.                     (0.5 * xDelta) + minX,
  99.                     (0.5 * yDelta) + minY,
  100.                     (0.5 * zDelta) + minZ
  101.                 );

  102.         return builder(precision)
  103.                 .setScale(scale)
  104.                 .setPosition(position)
  105.                 .build();
  106.     }

  107.     /** Construct a new instance by transforming a unit cube centered at the origin. The vertices of
  108.      * this input cube are:
  109.      * <pre>
  110.      * [
  111.      *      (-0.5, -0.5, -0.5),
  112.      *      (0.5, -0.5, -0.5),
  113.      *      (0.5, 0.5, -0.5),
  114.      *      (-0.5, 0.5, -0.5),
  115.      *
  116.      *      (-0.5, -0.5, 0.5),
  117.      *      (0.5, -0.5, 0.5),
  118.      *      (0.5, 0.5, 0.5),
  119.      *      (-0.5, 0.5, 0.5)
  120.      * ]
  121.      * </pre>
  122.      * @param transform transform to apply to the vertices of the unit cube
  123.      * @param precision precision context used to construct boundaries
  124.      * @return a new instance created by transforming the vertices of a unit cube centered at the origin
  125.      * @throws IllegalArgumentException if the width, height, or depth of the defined shape is zero
  126.      *      as evaluated by the precision context.
  127.      */
  128.     public static Parallelepiped fromTransformedUnitCube(final Transform<Vector3D> transform,
  129.             final Precision.DoubleEquivalence precision) {

  130.         final List<Vector3D> vertices = UNIT_CUBE_VERTICES.stream()
  131.                 .map(transform)
  132.                 .collect(Collectors.toList());
  133.         final boolean reverse = !transform.preservesOrientation();

  134.         // check lengths in each dimension
  135.         ensureNonZeroSideLength(vertices.get(0), vertices.get(1), precision);
  136.         ensureNonZeroSideLength(vertices.get(1), vertices.get(2), precision);
  137.         ensureNonZeroSideLength(vertices.get(0), vertices.get(4), precision);

  138.         final List<PlaneConvexSubset> boundaries = Arrays.asList(
  139.                     // planes orthogonal to x
  140.                     createFace(0, 4, 7, 3, vertices, reverse, precision),
  141.                     createFace(1, 2, 6, 5, vertices, reverse, precision),

  142.                     // planes orthogonal to y
  143.                     createFace(0, 1, 5, 4, vertices, reverse, precision),
  144.                     createFace(3, 7, 6, 2, vertices, reverse, precision),

  145.                     // planes orthogonal to z
  146.                     createFace(0, 3, 2, 1, vertices, reverse, precision),
  147.                     createFace(4, 5, 6, 7, vertices, reverse, precision)
  148.                 );

  149.         return new Parallelepiped(boundaries);
  150.     }

  151.     /** Return a new {@link Builder} instance to use for constructing parallelepipeds.
  152.      * @param precision precision context used to create boundaries
  153.      * @return a new {@link Builder} instance
  154.      */
  155.     public static Builder builder(final Precision.DoubleEquivalence precision) {
  156.         return new Builder(precision);
  157.     }

  158.     /** Create a single face of a parallelepiped using the indices of elements in the given vertex list.
  159.      * @param a first vertex index
  160.      * @param b second vertex index
  161.      * @param c third vertex index
  162.      * @param d fourth vertex index
  163.      * @param vertices list of vertices for the parallelepiped
  164.      * @param reverse if true, reverse the orientation of the face
  165.      * @param precision precision context used to create the face
  166.      * @return a parallelepiped face created from the indexed vertices
  167.      */
  168.     private static PlaneConvexSubset createFace(final int a, final int b, final int c, final int d,
  169.             final List<? extends Vector3D> vertices, final boolean reverse,
  170.             final Precision.DoubleEquivalence precision) {

  171.         final Vector3D pa = vertices.get(a);
  172.         final Vector3D pb = vertices.get(b);
  173.         final Vector3D pc = vertices.get(c);
  174.         final Vector3D pd = vertices.get(d);

  175.         final List<Vector3D> loop = reverse ?
  176.                 Arrays.asList(pd, pc, pb, pa) :
  177.                 Arrays.asList(pa, pb, pc, pd);

  178.         return Planes.convexPolygonFromVertices(loop, precision);
  179.     }

  180.     /** Ensure that the given points defining one side of a parallelepiped face are separated by a non-zero
  181.      * distance, as determined by the precision context.
  182.      * @param a first vertex
  183.      * @param b second vertex
  184.      * @param precision precision used to evaluate the distance between the two points
  185.      * @throws IllegalArgumentException if the given points are equivalent according to the precision context
  186.      */
  187.     private static void ensureNonZeroSideLength(final Vector3D a, final Vector3D b,
  188.             final Precision.DoubleEquivalence precision) {
  189.         if (precision.eqZero(a.distance(b))) {
  190.             throw new IllegalArgumentException(MessageFormat.format(
  191.                     "Parallelepiped has zero size: vertices {0} and {1} are equivalent", a, b));
  192.         }
  193.     }

  194.     /** Class designed to aid construction of {@link Parallelepiped} instances. Parallelepipeds are constructed
  195.      * by transforming the vertices of a unit cube centered at the origin with a transform built from
  196.      * the values configured here. The transformations applied are <em>scaling</em>, <em>rotation</em>,
  197.      * and <em>translation</em>, in that order. When applied in this order, the scale factors determine
  198.      * the width, height, and depth of the parallelepiped; the rotation determines the orientation; and the
  199.      * translation determines the position of the center point.
  200.      */
  201.     public static final class Builder {

  202.         /** Amount to scale the parallelepiped. */
  203.         private Vector3D scale = Vector3D.of(1, 1, 1);

  204.         /** The rotation of the parallelepiped. */
  205.         private QuaternionRotation rotation = QuaternionRotation.identity();

  206.         /** Amount to translate the parallelepiped. */
  207.         private Vector3D position = Vector3D.ZERO;

  208.         /** Precision context used to construct boundaries. */
  209.         private final Precision.DoubleEquivalence precision;

  210.         /** Construct a new instance configured with the given precision context.
  211.          * @param precision precision context used to create boundaries
  212.          */
  213.         private Builder(final Precision.DoubleEquivalence precision) {
  214.             this.precision = precision;
  215.         }

  216.         /** Set the center position of the created parallelepiped.
  217.          * @param pos center position of the created parallelepiped
  218.          * @return this instance
  219.          */
  220.         public Builder setPosition(final Vector3D pos) {
  221.             this.position = pos;
  222.             return this;
  223.         }

  224.         /** Set the scaling for the created parallelepiped. The scale values determine
  225.          * the lengths of the respective sides in the created parallelepiped.
  226.          * @param scaleFactors scale factors
  227.          * @return this instance
  228.          */
  229.         public Builder setScale(final Vector3D scaleFactors) {
  230.             this.scale = scaleFactors;
  231.             return this;
  232.         }

  233.         /** Set the scaling for the created parallelepiped. The scale values determine
  234.          * the lengths of the respective sides in the created parallelepiped.
  235.          * @param x x scale factor
  236.          * @param y y scale factor
  237.          * @param z z scale factor
  238.          * @return this instance
  239.          */
  240.         public Builder setScale(final double x, final double y, final double z) {
  241.             return setScale(Vector3D.of(x, y, z));
  242.         }

  243.         /** Set the scaling for the created parallelepiped. The given scale factor is applied
  244.          * to the x, y, and z directions.
  245.          * @param scaleFactor scale factor for the x, y, and z directions
  246.          * @return this instance
  247.          */
  248.         public Builder setScale(final double scaleFactor) {
  249.             return setScale(scaleFactor, scaleFactor, scaleFactor);
  250.         }

  251.         /** Set the rotation of the created parallelepiped.
  252.          * @param rot the rotation of the created parallelepiped
  253.          * @return this instance
  254.          */
  255.         public Builder setRotation(final QuaternionRotation rot) {
  256.             this.rotation = rot;
  257.             return this;
  258.         }

  259.         /** Build a new parallelepiped instance with the values configured in this builder.
  260.          * @return a new parallelepiped instance
  261.          * @throws IllegalArgumentException if the length of any side of the parallelepiped is zero,
  262.          *      as determined by the configured precision context
  263.          * @see Parallelepiped#fromTransformedUnitCube(Transform, Precision.DoubleEquivalence)
  264.          */
  265.         public Parallelepiped build() {
  266.             final AffineTransformMatrix3D transform = AffineTransformMatrix3D.createScale(scale)
  267.                     .rotate(rotation)
  268.                     .translate(position);

  269.             return fromTransformedUnitCube(transform, precision);
  270.         }
  271.     }
  272. }