1 /* 2 * Copyright (c) 2013, 2018, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.scene.shape; 27 28 import com.sun.javafx.geom.BaseBounds; 29 import com.sun.javafx.geom.PickRay; 30 import com.sun.javafx.geom.Vec3d; 31 import com.sun.javafx.geom.transform.BaseTransform; 32 import com.sun.javafx.scene.DirtyBits; 33 import com.sun.javafx.scene.NodeHelper; 34 import com.sun.javafx.scene.input.PickResultChooser; 35 import com.sun.javafx.scene.shape.BoxHelper; 36 import com.sun.javafx.sg.prism.NGBox; 37 import com.sun.javafx.sg.prism.NGNode; 38 import javafx.beans.property.DoubleProperty; 39 import javafx.beans.property.SimpleDoubleProperty; 40 import javafx.geometry.Point2D; 41 import javafx.geometry.Point3D; 42 import javafx.scene.Node; 43 import javafx.scene.input.PickResult; 44 45 /** 46 * The {@code Box} class defines a 3 dimensional box with the specified size. 47 * A {@code Box} is a 3D geometry primitive created with a given depth, width, 48 * and height. It is centered at the origin. 49 * 50 * @since JavaFX 8.0 51 */ 52 public class Box extends Shape3D { 53 static { 54 // This is used by classes in different packages to get access to 55 // private and package private methods. 56 BoxHelper.setBoxAccessor(new BoxHelper.BoxAccessor() { 57 @Override 58 public NGNode doCreatePeer(Node node) { 59 return ((Box) node).doCreatePeer(); 60 } 61 62 @Override 63 public void doUpdatePeer(Node node) { 64 ((Box) node).doUpdatePeer(); 65 } 66 67 @Override 68 public BaseBounds doComputeGeomBounds(Node node, 69 BaseBounds bounds, BaseTransform tx) { 70 return ((Box) node).doComputeGeomBounds(bounds, tx); 71 } 72 73 @Override 74 public boolean doComputeContains(Node node, double localX, double localY) { 75 return ((Box) node).doComputeContains(localX, localY); 76 } 77 78 @Override 79 public boolean doComputeIntersects(Node node, PickRay pickRay, 80 PickResultChooser pickResult) { 81 return ((Box) node).doComputeIntersects(pickRay, pickResult); 82 } 83 }); 84 } 85 86 private TriangleMesh mesh; 87 88 /** 89 * Creates a new instance of {@code Box} of dimension 2 by 2 by 2. 90 */ 91 92 public static final double DEFAULT_SIZE = 2; 93 94 { 95 // To initialize the class helper at the beginning of each constructor of this class 96 BoxHelper.initHelper(this); 97 } 98 99 public Box() { 100 this(DEFAULT_SIZE, DEFAULT_SIZE, DEFAULT_SIZE); 101 } 102 103 /** 104 * Creates a new instance of {@code Box} of dimension width by height 105 * by depth. 106 * @param width the width of this box 107 * @param height the height of this box 108 * @param depth the depth of this box 109 */ 110 public Box(double width, double height, double depth) { 111 setWidth(width); 112 setHeight(height); 113 setDepth(depth); 114 } 115 116 /** 117 * Defines the depth or the Z dimension of the Box. 118 * 119 * @defaultValue 2.0 120 */ 121 private DoubleProperty depth; 122 123 public final void setDepth(double value) { 124 depthProperty().set(value); 125 } 126 127 public final double getDepth() { 128 return depth == null ? 2 : depth.get(); 129 } 130 131 public final DoubleProperty depthProperty() { 132 if (depth == null) { 133 depth = new SimpleDoubleProperty(Box.this, "depth", DEFAULT_SIZE) { 134 @Override 135 public void invalidated() { 136 NodeHelper.markDirty(Box.this, DirtyBits.MESH_GEOM); 137 manager.invalidateBoxMesh(key); 138 key = null; 139 NodeHelper.geomChanged(Box.this); 140 } 141 }; 142 } 143 return depth; 144 } 145 146 /** 147 * Defines the height or the Y dimension of the Box. 148 * 149 * @defaultValue 2.0 150 */ 151 private DoubleProperty height; 152 153 public final void setHeight(double value) { 154 heightProperty().set(value); 155 } 156 157 public final double getHeight() { 158 return height == null ? 2 : height.get(); 159 } 160 161 public final DoubleProperty heightProperty() { 162 if (height == null) { 163 height = new SimpleDoubleProperty(Box.this, "height", DEFAULT_SIZE) { 164 @Override 165 public void invalidated() { 166 NodeHelper.markDirty(Box.this, DirtyBits.MESH_GEOM); 167 manager.invalidateBoxMesh(key); 168 key = null; 169 NodeHelper.geomChanged(Box.this); 170 } 171 }; 172 } 173 return height; 174 } 175 176 /** 177 * Defines the width or the X dimension of the Box. 178 * 179 * @defaultValue 2.0 180 */ 181 private DoubleProperty width; 182 183 public final void setWidth(double value) { 184 widthProperty().set(value); 185 } 186 187 public final double getWidth() { 188 return width == null ? 2 : width.get(); 189 } 190 191 public final DoubleProperty widthProperty() { 192 if (width == null) { 193 width = new SimpleDoubleProperty(Box.this, "width", DEFAULT_SIZE) { 194 @Override 195 public void invalidated() { 196 NodeHelper.markDirty(Box.this, DirtyBits.MESH_GEOM); 197 manager.invalidateBoxMesh(key); 198 key = null; 199 NodeHelper.geomChanged(Box.this); 200 } 201 }; 202 } 203 return width; 204 } 205 206 /* 207 * Note: This method MUST only be called via its accessor method. 208 */ 209 private NGNode doCreatePeer() { 210 return new NGBox(); 211 } 212 213 /* 214 * Note: This method MUST only be called via its accessor method. 215 */ 216 private void doUpdatePeer() { 217 if (NodeHelper.isDirty(this, DirtyBits.MESH_GEOM)) { 218 NGBox peer = NodeHelper.getPeer(this); 219 final float w = (float) getWidth(); 220 final float h = (float) getHeight(); 221 final float d = (float) getDepth(); 222 if (w < 0 || h < 0 || d < 0) { 223 peer.updateMesh(null); 224 } else { 225 if (key == null) { 226 key = new BoxKey(w, h, d); 227 } 228 mesh = manager.getBoxMesh(w, h, d, key); 229 mesh.updatePG(); 230 peer.updateMesh(mesh.getPGTriangleMesh()); 231 } 232 } 233 } 234 235 /* 236 * Note: This method MUST only be called via its accessor method. 237 */ 238 private BaseBounds doComputeGeomBounds(BaseBounds bounds, BaseTransform tx) { 239 final float w = (float) getWidth(); 240 final float h = (float) getHeight(); 241 final float d = (float) getDepth(); 242 243 if (w < 0 || h < 0 || d < 0) { 244 return bounds.makeEmpty(); 245 } 246 247 final float hw = w * 0.5f; 248 final float hh = h * 0.5f; 249 final float hd = d * 0.5f; 250 251 bounds = bounds.deriveWithNewBounds(-hw, -hh, -hd, hw, hh, hd); 252 bounds = tx.transform(bounds, bounds); 253 return bounds; 254 } 255 256 /* 257 * Note: This method MUST only be called via its accessor method. 258 */ 259 private boolean doComputeContains(double localX, double localY) { 260 double w = getWidth(); 261 double h = getHeight(); 262 return -w <= localX && localX <= w && 263 -h <= localY && localY <= h; 264 } 265 266 /* 267 * Note: This method MUST only be called via its accessor method. 268 */ 269 private boolean doComputeIntersects(PickRay pickRay, PickResultChooser pickResult) { 270 271 final double w = getWidth(); 272 final double h = getHeight(); 273 final double d = getDepth(); 274 final double hWidth = w / 2.0; 275 final double hHeight = h / 2.0; 276 final double hDepth = d / 2.0; 277 final Vec3d dir = pickRay.getDirectionNoClone(); 278 final double invDirX = dir.x == 0.0 ? Double.POSITIVE_INFINITY : (1.0 / dir.x); 279 final double invDirY = dir.y == 0.0 ? Double.POSITIVE_INFINITY : (1.0 / dir.y); 280 final double invDirZ = dir.z == 0.0 ? Double.POSITIVE_INFINITY : (1.0 / dir.z); 281 final Vec3d origin = pickRay.getOriginNoClone(); 282 final double originX = origin.x; 283 final double originY = origin.y; 284 final double originZ = origin.z; 285 final boolean signX = invDirX < 0.0; 286 final boolean signY = invDirY < 0.0; 287 final boolean signZ = invDirZ < 0.0; 288 289 double t0 = Double.NEGATIVE_INFINITY; 290 double t1 = Double.POSITIVE_INFINITY; 291 char side0 = '0'; 292 char side1 = '0'; 293 294 if (Double.isInfinite(invDirX)) { 295 if (-hWidth <= originX && hWidth >= originX) { 296 // move on, we are inside for the whole length 297 } else { 298 return false; 299 } 300 } else { 301 t0 = ((signX ? hWidth : -hWidth) - originX) * invDirX; 302 t1 = ((signX ? -hWidth : hWidth) - originX) * invDirX; 303 side0 = signX ? 'X' : 'x'; 304 side1 = signX ? 'x' : 'X'; 305 } 306 307 if (Double.isInfinite(invDirY)) { 308 if (-hHeight <= originY && hHeight >= originY) { 309 // move on, we are inside for the whole length 310 } else { 311 return false; 312 } 313 } else { 314 final double ty0 = ((signY ? hHeight : -hHeight) - originY) * invDirY; 315 final double ty1 = ((signY ? -hHeight : hHeight) - originY) * invDirY; 316 317 if ((t0 > ty1) || (ty0 > t1)) { 318 return false; 319 } 320 if (ty0 > t0) { 321 side0 = signY ? 'Y' : 'y'; 322 t0 = ty0; 323 } 324 if (ty1 < t1) { 325 side1 = signY ? 'y' : 'Y'; 326 t1 = ty1; 327 } 328 } 329 330 if (Double.isInfinite(invDirZ)) { 331 if (-hDepth <= originZ && hDepth >= originZ) { 332 // move on, we are inside for the whole length 333 } else { 334 return false; 335 } 336 } else { 337 double tz0 = ((signZ ? hDepth : -hDepth) - originZ) * invDirZ; 338 double tz1 = ((signZ ? -hDepth : hDepth) - originZ) * invDirZ; 339 340 if ((t0 > tz1) || (tz0 > t1)) { 341 return false; 342 } 343 if (tz0 > t0) { 344 side0 = signZ ? 'Z' : 'z'; 345 t0 = tz0; 346 } 347 if (tz1 < t1) { 348 side1 = signZ ? 'z' : 'Z'; 349 t1 = tz1; 350 } 351 } 352 353 char side = side0; 354 double t = t0; 355 final CullFace cullFace = getCullFace(); 356 final double minDistance = pickRay.getNearClip(); 357 final double maxDistance = pickRay.getFarClip(); 358 359 if (t0 > maxDistance) { 360 return false; 361 } 362 if (t0 < minDistance || cullFace == CullFace.FRONT) { 363 if (t1 >= minDistance && t1 <= maxDistance && cullFace != CullFace.BACK) { 364 side = side1; 365 t = t1; 366 } else { 367 return false; 368 } 369 } 370 371 if (Double.isInfinite(t) || Double.isNaN(t)) { 372 // We've got a nonsense pick ray or box size. 373 return false; 374 } 375 376 if (pickResult != null && pickResult.isCloser(t)) { 377 Point3D point = PickResultChooser.computePoint(pickRay, t); 378 379 Point2D txtCoords = null; 380 381 switch (side) { 382 case 'x': // left 383 txtCoords = new Point2D( 384 0.5 - point.getZ() / d, 385 0.5 + point.getY() / h); 386 break; 387 case 'X': // right 388 txtCoords = new Point2D( 389 0.5 + point.getZ() / d, 390 0.5 + point.getY() / h); 391 break; 392 case 'y': // top 393 txtCoords = new Point2D( 394 0.5 + point.getX() / w, 395 0.5 - point.getZ() / d); 396 break; 397 case 'Y': // bottom 398 txtCoords = new Point2D( 399 0.5 + point.getX() / w, 400 0.5 + point.getZ() / d); 401 break; 402 case 'z': // front 403 txtCoords = new Point2D( 404 0.5 + point.getX() / w, 405 0.5 + point.getY() / h); 406 break; 407 case 'Z': // back 408 txtCoords = new Point2D( 409 0.5 - point.getX() / w, 410 0.5 + point.getY() / h); 411 break; 412 default: 413 // No hit with any of the planes. We must have had a zero 414 // pick ray direction vector. Should never happen. 415 return false; 416 } 417 418 pickResult.offer(this, t, PickResult.FACE_UNDEFINED, point, txtCoords); 419 } 420 421 return true; 422 } 423 424 static TriangleMesh createMesh(float w, float h, float d) { 425 426 // NOTE: still create mesh for degenerated box 427 float hw = w / 2f; 428 float hh = h / 2f; 429 float hd = d / 2f; 430 431 float points[] = { 432 -hw, -hh, -hd, 433 hw, -hh, -hd, 434 hw, hh, -hd, 435 -hw, hh, -hd, 436 -hw, -hh, hd, 437 hw, -hh, hd, 438 hw, hh, hd, 439 -hw, hh, hd}; 440 441 float texCoords[] = {0, 0, 1, 0, 1, 1, 0, 1}; 442 443 // Specifies hard edges. 444 int faceSmoothingGroups[] = { 445 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 446 }; 447 448 int faces[] = { 449 0, 0, 2, 2, 1, 1, 450 2, 2, 0, 0, 3, 3, 451 1, 0, 6, 2, 5, 1, 452 6, 2, 1, 0, 2, 3, 453 5, 0, 7, 2, 4, 1, 454 7, 2, 5, 0, 6, 3, 455 4, 0, 3, 2, 0, 1, 456 3, 2, 4, 0, 7, 3, 457 3, 0, 6, 2, 2, 1, 458 6, 2, 3, 0, 7, 3, 459 4, 0, 1, 2, 5, 1, 460 1, 2, 4, 0, 0, 3, 461 }; 462 463 TriangleMesh mesh = new TriangleMesh(true); 464 mesh.getPoints().setAll(points); 465 mesh.getTexCoords().setAll(texCoords); 466 mesh.getFaces().setAll(faces); 467 mesh.getFaceSmoothingGroups().setAll(faceSmoothingGroups); 468 469 return mesh; 470 } 471 472 private static class BoxKey extends Key { 473 474 final double width, height, depth; 475 476 private BoxKey(double width, double height, double depth) { 477 this.width = width; 478 this.height = height; 479 this.depth = depth; 480 } 481 482 @Override 483 public int hashCode() { 484 long bits = 7L; 485 bits = 31L * bits + Double.doubleToLongBits(depth); 486 bits = 31L * bits + Double.doubleToLongBits(height); 487 bits = 31L * bits + Double.doubleToLongBits(width); 488 return Long.hashCode(bits); 489 } 490 491 @Override 492 public boolean equals(Object obj) { 493 if (this == obj) { 494 return true; 495 } 496 if (obj == null) { 497 return false; 498 } 499 if (!(obj instanceof BoxKey)) { 500 return false; 501 } 502 BoxKey other = (BoxKey) obj; 503 if (Double.compare(depth, other.depth) != 0) { 504 return false; 505 } 506 if (Double.compare(height, other.height) != 0) { 507 return false; 508 } 509 if (Double.compare(width, other.width) != 0) { 510 return false; 511 } 512 return true; 513 } 514 } 515 }