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}