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}