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; 18 19 import java.util.Objects; 20 21 import org.apache.commons.geometry.core.Transform; 22 import org.apache.commons.geometry.core.partitioning.AbstractHyperplane; 23 import org.apache.commons.geometry.core.partitioning.Hyperplane; 24 import org.apache.commons.geometry.euclidean.threed.line.Line3D; 25 import org.apache.commons.geometry.euclidean.threed.line.Lines3D; 26 import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation; 27 import org.apache.commons.geometry.euclidean.twod.ConvexArea; 28 import org.apache.commons.numbers.core.Precision; 29 30 /** Class representing a plane in 3 dimensional Euclidean space. Each plane is defined by a 31 * {@link #getNormal() normal} and an {@link #getOriginOffset() origin offset}. If \(\vec{n}\) is the plane normal, 32 * \(d\) is the origin offset, and \(p\) and \(q\) are any points in the plane, then the following are true: 33 * <ul> 34 * <li>\(\lVert \vec{n} \rVert\) = 1</li> 35 * <li>\(\vec{n} \cdot (p - q) = 0\)</li> 36 * <li>\(d = - (\vec{n} \cdot q)\)</li> 37 * </ul> 38 * In other words, the normal is a unit vector such that the dot product of the normal and the difference of 39 * any two points in the plane is always equal to \(0\). Similarly, the {@code origin offset} is equal to the 40 * negation of the dot product of the normal and any point in the plane. The projection of the origin onto the 41 * plane (given by {@link #getOrigin()}), is computed as \(-d \vec{n}\). 42 * 43 * <p>Instances of this class are guaranteed to be immutable.</p> 44 * @see Planes 45 */ 46 public class Plane extends AbstractHyperplane<Vector3D> { 47 48 /** Plane normal. */ 49 private final Vector3D.Unit normal; 50 51 /** Offset of the origin with respect to the plane. */ 52 private final double originOffset; 53 54 /** Construct a plane from its component parts. 55 * @param normal unit normal vector 56 * @param originOffset offset of the origin with respect to the plane 57 * @param precision precision context used to compare floating point values 58 */ 59 Plane(final Vector3D.Unit normal, final double originOffset, 60 final Precision.DoubleEquivalence precision) { 61 62 super(precision); 63 64 this.normal = normal; 65 this.originOffset = originOffset; 66 } 67 68 /** Get the orthogonal projection of the 3D-space origin in the plane. 69 * @return the origin point of the plane frame (point closest to the 3D-space 70 * origin) 71 */ 72 public Vector3D getOrigin() { 73 return normal.multiply(-originOffset); 74 } 75 76 /** Get the offset of the spatial origin ({@code 0, 0, 0}) with respect to the plane. 77 * @return the offset of the origin with respect to the plane. 78 */ 79 public double getOriginOffset() { 80 return originOffset; 81 } 82 83 /** Get the plane normal vector. 84 * @return plane normal vector 85 */ 86 public Vector3D.Unit getNormal() { 87 return normal; 88 } 89 90 /** Return an {@link EmbeddingPlane} instance suitable for embedding 2D geometric objects 91 * into this plane. Returned instances are guaranteed to be equal between invocations. 92 * @return a plane instance suitable for embedding 2D subspaces 93 */ 94 public EmbeddingPlane getEmbedding() { 95 final Vector3D.Unit u = normal.orthogonal(); 96 final Vector3D.Unit v = normal.cross(u).normalize(); 97 98 return new EmbeddingPlane(u, v, normal, originOffset, getPrecision()); 99 } 100 101 /** {@inheritDoc} */ 102 @Override 103 public double offset(final Vector3D point) { 104 return point.dot(normal) + originOffset; 105 } 106 107 /** Get the offset (oriented distance) of the given line with respect to the plane. The value 108 * closest to zero is returned, which will always be zero if the line is not parallel to the plane. 109 * @param line line to calculate the offset of 110 * @return the offset of the line with respect to the plane or 0.0 if the line 111 * is not parallel to the plane. 112 */ 113 public double offset(final Line3D line) { 114 if (!isParallel(line)) { 115 return 0.0; 116 } 117 return offset(line.getOrigin()); 118 } 119 120 /** Get the offset (oriented distance) of the given plane with respect to this instance. The value 121 * closest to zero is returned, which will always be zero if the planes are not parallel. 122 * @param plane plane to calculate the offset of 123 * @return the offset of the plane with respect to this instance or 0.0 if the planes 124 * are not parallel. 125 */ 126 public double offset(final Plane plane) { 127 if (!isParallel(plane)) { 128 return 0.0; 129 } 130 return originOffset + (similarOrientation(plane) ? -plane.originOffset : plane.originOffset); 131 } 132 133 /** Check if the instance contains a point. 134 * @param p point to check 135 * @return true if p belongs to the plane 136 */ 137 @Override 138 public boolean contains(final Vector3D p) { 139 return getPrecision().eqZero(offset(p)); 140 } 141 142 /** Check if the instance contains a line. 143 * @param line line to check 144 * @return true if line is contained in this plane 145 */ 146 public boolean contains(final Line3D line) { 147 return isParallel(line) && contains(line.getOrigin()); 148 } 149 150 /** Check if the instance contains another plane. Planes are considered similar if they contain 151 * the same points. This does not mean they are equal since they can have opposite normals. 152 * @param plane plane to which the instance is compared 153 * @return true if the planes are similar 154 */ 155 public boolean contains(final Plane plane) { 156 final double angle = normal.angle(plane.normal); 157 final Precision.DoubleEquivalence precision = getPrecision(); 158 159 return ((precision.eqZero(angle)) && precision.eq(originOffset, plane.originOffset)) || 160 ((precision.eq(angle, Math.PI)) && precision.eq(originOffset, -plane.originOffset)); 161 } 162 163 /** {@inheritDoc} */ 164 @Override 165 public Vector3D project(final Vector3D point) { 166 return getOrigin().add(point.reject(normal)); 167 } 168 169 /** Project a 3D line onto the plane. 170 * @param line the line to project 171 * @return the projection of the given line onto the plane. 172 */ 173 public Line3D project(final Line3D line) { 174 final Vector3D direction = line.getDirection(); 175 final Vector3D projection = normal.multiply(direction.dot(normal) * (1 / normal.normSq())); 176 177 final Vector3D projectedLineDirection = direction.subtract(projection); 178 final Vector3D p1 = project(line.getOrigin()); 179 final Vector3D p2 = p1.add(projectedLineDirection); 180 181 return Lines3D.fromPoints(p1, p2, getPrecision()); 182 } 183 184 /** {@inheritDoc} */ 185 @Override 186 public PlaneConvexSubset span() { 187 return Planes.subsetFromConvexArea(getEmbedding(), ConvexArea.full()); 188 } 189 190 /** Check if the line is parallel to the instance. 191 * @param line line to check. 192 * @return true if the line is parallel to the instance, false otherwise. 193 */ 194 public boolean isParallel(final Line3D line) { 195 final double dot = normal.dot(line.getDirection()); 196 197 return getPrecision().eqZero(dot); 198 } 199 200 /** Check if the plane is parallel to the instance. 201 * @param plane plane to check. 202 * @return true if the plane is parallel to the instance, false otherwise. 203 */ 204 public boolean isParallel(final Plane plane) { 205 return getPrecision().eqZero(normal.cross(plane.normal).norm()); 206 } 207 208 /** {@inheritDoc} */ 209 @Override 210 public boolean similarOrientation(final Hyperplane<Vector3D> other) { 211 return (((Plane) other).normal).dot(normal) > 0; 212 } 213 214 /** Get the intersection of a line with this plane. 215 * @param line line intersecting the instance 216 * @return intersection point between between the line and the instance (null if 217 * the line is parallel to the instance) 218 */ 219 public Vector3D intersection(final Line3D line) { 220 final Vector3D direction = line.getDirection(); 221 final double dot = normal.dot(direction); 222 223 if (getPrecision().eqZero(dot)) { 224 return null; 225 } 226 227 final Vector3D point = line.pointAt(0); 228 final double k = -(originOffset + normal.dot(point)) / dot; 229 230 return Vector3D.Sum.of(point) 231 .addScaled(k, direction) 232 .get(); 233 } 234 235 /** Get the line formed by the intersection of this instance with the given plane. 236 * The returned line lies in both planes and points in the direction of 237 * the cross product <code>n<sub>1</sub> x n<sub>2</sub></code>, where <code>n<sub>1</sub></code> 238 * is the normal of the current instance and <code>n<sub>2</sub></code> is the normal 239 * of the argument. 240 * 241 * <p>Null is returned if the planes are parallel.</p> 242 * 243 * @param other other plane 244 * @return line at the intersection of the instance and the other plane, or null 245 * if no such line exists 246 */ 247 public Line3D intersection(final Plane other) { 248 final Vector3D direction = normal.cross(other.normal); 249 250 if (getPrecision().eqZero(direction.norm())) { 251 return null; 252 } 253 254 final Vector3D point = intersection(this, other, Planes.fromNormal(direction, getPrecision())); 255 256 return Lines3D.fromPointAndDirection(point, direction, getPrecision()); 257 } 258 259 /** Build a new reversed version of this plane, with opposite orientation. 260 * @return a new reversed plane 261 */ 262 @Override 263 public Plane reverse() { 264 return new Plane(normal.negate(), -originOffset, getPrecision()); 265 } 266 267 /** {@inheritDoc} 268 * 269 * <p>Instances are transformed by selecting 3 representative points from the 270 * plane, transforming them, and constructing a new plane from the transformed points. 271 * Since the normal is not transformed directly, but rather is constructed new from the 272 * transformed points, the relative orientations of points in the plane are preserved, 273 * even for transforms that do not 274 * {@link Transform#preservesOrientation() preserve orientation}. The example below shows 275 * a plane being transformed by a non-orientation-preserving transform. The normal of the 276 * transformed plane retains its counterclockwise relationship to the points in the plane, 277 * in contrast with the normal that is transformed directly by the transform. 278 * </p> 279 * <pre> 280 * // construct a plane from 3 points; the normal will be selected such that the 281 * // points are ordered counterclockwise when looking down the plane normal. 282 * Vector3D p1 = Vector3D.of(0, 0, 0); 283 * Vector3D p2 = Vector3D.of(+1, 0, 0); 284 * Vector3D p3 = Vector3D.of(0, +1, 0); 285 * 286 * Plane plane = Planes.fromPoints(p1, p2, p3, precision); // normal is (0, 0, +1) 287 * 288 * // create a transform that negates all x-values; this transform does not 289 * // preserve orientation, i.e. it will convert a right-handed system into a left-handed 290 * // system and vice versa 291 * AffineTransformMatrix3D transform = AffineTransformMatrix3D.createScale(-1, 1, 1); 292 * 293 * // transform the plane 294 * Plane transformedPlane = plane.transform(transform); 295 * 296 * // the plane normal is oriented such that transformed points are still ordered 297 * // counterclockwise when looking down the plane normal; since the point (1, 0, 0) has 298 * // now become (-1, 0, 0), the normal has flipped to (0, 0, -1) 299 * transformedPlane.getNormal(); 300 * 301 * // directly transform the original plane normal; the normal is unchanged by the transform 302 * // since the target space of the transform is left-handed 303 * AffineTransformMatrix3D normalTransform = transform.normalTransform(); 304 * Vector3D directlyTransformedNormal = normalTransform.apply(plane.getNormal()); // (0, 0, +1) 305 * </pre> 306 */ 307 @Override 308 public Plane transform(final Transform<Vector3D> transform) { 309 // create 3 representation points lying on the plane, transform them, 310 // and use the transformed points to create a new plane 311 312 final Vector3D u = normal.orthogonal(); 313 final Vector3D v = normal.cross(u); 314 315 final Vector3D p1 = getOrigin(); 316 final Vector3D p2 = p1.add(u); 317 final Vector3D p3 = p1.add(v); 318 319 final Vector3D t1 = transform.apply(p1); 320 final Vector3D t2 = transform.apply(p2); 321 final Vector3D t3 = transform.apply(p3); 322 323 return Planes.fromPoints(t1, t2, t3, getPrecision()); 324 } 325 326 /** Translate the plane by the specified amount. 327 * @param translation translation to apply 328 * @return a new plane 329 */ 330 public Plane translate(final Vector3D translation) { 331 final Vector3D tOrigin = getOrigin().add(translation); 332 333 return Planes.fromPointAndNormal(tOrigin, normal, getPrecision()); 334 } 335 336 /** Rotate the plane around the specified point. 337 * @param center rotation center 338 * @param rotation 3-dimensional rotation 339 * @return a new plane 340 */ 341 public Plane rotate(final Vector3D center, final QuaternionRotation rotation) { 342 final Vector3D delta = getOrigin().subtract(center); 343 final Vector3D tOrigin = center.add(rotation.apply(delta)); 344 345 // we can directly apply the rotation to the normal since it will transform 346 // it properly (there is no translation or scaling involved) 347 final Vector3D.Unit tNormal = rotation.apply(normal).normalize(); 348 349 return Planes.fromPointAndNormal(tOrigin, tNormal, getPrecision()); 350 } 351 352 /** Return true if this instance should be considered equivalent to the argument, using the 353 * given precision context for comparison. Instances are considered equivalent if they contain 354 * the same points, which is determined by comparing the plane {@code origins} and {@code normals}. 355 * @param other the point to compare with 356 * @param precision precision context to use for the comparison 357 * @return true if this instance should be considered equivalent to the argument 358 * @see Vector3D#eq(Vector3D, Precision.DoubleEquivalence) 359 */ 360 public boolean eq(final Plane other, final Precision.DoubleEquivalence precision) { 361 return getOrigin().eq(other.getOrigin(), precision) && 362 normal.eq(other.normal, precision); 363 } 364 365 /** {@inheritDoc} */ 366 @Override 367 public int hashCode() { 368 return Objects.hash(normal, originOffset, getPrecision()); 369 } 370 371 /** {@inheritDoc} */ 372 @Override 373 public boolean equals(final Object obj) { 374 if (this == obj) { 375 return true; 376 } else if (obj == null || obj.getClass() != this.getClass()) { 377 return false; 378 } 379 380 final Plane other = (Plane) obj; 381 382 return Objects.equals(this.normal, other.normal) && 383 Double.compare(this.originOffset, other.originOffset) == 0 && 384 Objects.equals(this.getPrecision(), other.getPrecision()); 385 } 386 387 /** {@inheritDoc} */ 388 @Override 389 public String toString() { 390 final StringBuilder sb = new StringBuilder(); 391 sb.append(getClass().getSimpleName()) 392 .append("[origin= ") 393 .append(getOrigin()) 394 .append(", normal= ") 395 .append(normal) 396 .append(']'); 397 398 return sb.toString(); 399 } 400 401 /** Get the intersection point of three planes. Returns null if no unique intersection point 402 * exists (ie, there are no intersection points or an infinite number). 403 * @param plane1 first plane1 404 * @param plane2 second plane2 405 * @param plane3 third plane2 406 * @return intersection point of the three planes or null if no unique intersection point exists 407 */ 408 public static Vector3D intersection(final Plane plane1, final Plane plane2, final Plane plane3) { 409 410 // coefficients of the three planes linear equations 411 final double a1 = plane1.normal.getX(); 412 final double b1 = plane1.normal.getY(); 413 final double c1 = plane1.normal.getZ(); 414 final double d1 = plane1.originOffset; 415 416 final double a2 = plane2.normal.getX(); 417 final double b2 = plane2.normal.getY(); 418 final double c2 = plane2.normal.getZ(); 419 final double d2 = plane2.originOffset; 420 421 final double a3 = plane3.normal.getX(); 422 final double b3 = plane3.normal.getY(); 423 final double c3 = plane3.normal.getZ(); 424 final double d3 = plane3.originOffset; 425 426 // direct Cramer resolution of the linear system 427 // (this is still feasible for a 3x3 system) 428 final double a23 = (b2 * c3) - (b3 * c2); 429 final double b23 = (c2 * a3) - (c3 * a2); 430 final double c23 = (a2 * b3) - (a3 * b2); 431 final double determinant = (a1 * a23) + (b1 * b23) + (c1 * c23); 432 433 // use the precision context of the first plane to determine equality 434 if (plane1.getPrecision().eqZero(determinant)) { 435 return null; 436 } 437 438 final double r = 1.0 / determinant; 439 return Vector3D.of((-a23 * d1 - (c1 * b3 - c3 * b1) * d2 - (c2 * b1 - c1 * b2) * d3) * r, 440 (-b23 * d1 - (c3 * a1 - c1 * a3) * d2 - (c1 * a2 - c2 * a1) * d3) * r, 441 (-c23 * d1 - (b1 * a3 - b3 * a1) * d2 - (b2 * a1 - b1 * a2) * d3) * r); 442 } 443 }