1 /* 2 * Copyright (c) 2013, 2016, 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 begining 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 */ 107 public Box(double width, double height, double depth) { 108 setWidth(width); 109 setHeight(height); 110 setDepth(depth); 111 } 112 113 /** 114 * Defines the depth or the Z dimension of the Box. 115 * 116 * @defaultValue 2.0 117 */ 118 private DoubleProperty depth; 119 120 public final void setDepth(double value) { 121 depthProperty().set(value); 122 } 123 124 public final double getDepth() { 125 return depth == null ? 2 : depth.get(); 126 } 127 128 public final DoubleProperty depthProperty() { 129 if (depth == null) { 130 depth = new SimpleDoubleProperty(Box.this, "depth", DEFAULT_SIZE) { 131 @Override 132 public void invalidated() { 133 NodeHelper.markDirty(Box.this, DirtyBits.MESH_GEOM); 134 manager.invalidateBoxMesh(key); 135 key = 0; 136 NodeHelper.geomChanged(Box.this); 137 } 138 }; 139 } 140 return depth; 141 } 142 143 /** 144 * Defines the height or the Y dimension of the Box. 145 * 146 * @defaultValue 2.0 147 */ 148 private DoubleProperty height; 149 150 public final void setHeight(double value) { 151 heightProperty().set(value); 152 } 153 154 public final double getHeight() { 155 return height == null ? 2 : height.get(); 156 } 157 158 public final DoubleProperty heightProperty() { 159 if (height == null) { 160 height = new SimpleDoubleProperty(Box.this, "height", DEFAULT_SIZE) { 161 @Override 162 public void invalidated() { 163 NodeHelper.markDirty(Box.this, DirtyBits.MESH_GEOM); 164 manager.invalidateBoxMesh(key); 165 key = 0; 166 NodeHelper.geomChanged(Box.this); 167 } 168 }; 169 } 170 return height; 171 } 172 173 /** 174 * Defines the width or the X dimension of the Box. 175 * 176 * @defaultValue 2.0 177 */ 178 private DoubleProperty width; 179 180 public final void setWidth(double value) { 181 widthProperty().set(value); 182 } 183 184 public final double getWidth() { 185 return width == null ? 2 : width.get(); 186 } 187 188 public final DoubleProperty widthProperty() { 189 if (width == null) { 190 width = new SimpleDoubleProperty(Box.this, "width", DEFAULT_SIZE) { 191 @Override 192 public void invalidated() { 193 NodeHelper.markDirty(Box.this, DirtyBits.MESH_GEOM); 194 manager.invalidateBoxMesh(key); 195 key = 0; 196 NodeHelper.geomChanged(Box.this); 197 } 198 }; 199 } 200 return width; 201 } 202 203 /* 204 * Note: This method MUST only be called via its accessor method. 205 */ 206 private NGNode doCreatePeer() { 207 return new NGBox(); 208 } 209 210 /* 211 * Note: This method MUST only be called via its accessor method. 212 */ 213 private void doUpdatePeer() { 214 if (NodeHelper.isDirty(this, DirtyBits.MESH_GEOM)) { 215 NGBox peer = NodeHelper.getPeer(this); 216 final float w = (float) getWidth(); 217 final float h = (float) getHeight(); 218 final float d = (float) getDepth(); 219 if (w < 0 || h < 0 || d < 0) { 220 peer.updateMesh(null); 221 } else { 222 if (key == 0) { 223 key = generateKey(w, h, d); 224 } 225 mesh = manager.getBoxMesh(w, h, d, key); 226 mesh.updatePG(); 227 peer.updateMesh(mesh.getPGTriangleMesh()); 228 } 229 } 230 } 231 232 /* 233 * Note: This method MUST only be called via its accessor method. 234 */ 235 private BaseBounds doComputeGeomBounds(BaseBounds bounds, BaseTransform tx) { 236 final float w = (float) getWidth(); 237 final float h = (float) getHeight(); 238 final float d = (float) getDepth(); 239 240 if (w < 0 || h < 0 || d < 0) { 241 return bounds.makeEmpty(); 242 } 243 244 final float hw = w * 0.5f; 245 final float hh = h * 0.5f; 246 final float hd = d * 0.5f; 247 248 bounds = bounds.deriveWithNewBounds(-hw, -hh, -hd, hw, hh, hd); 249 bounds = tx.transform(bounds, bounds); 250 return bounds; 251 } 252 253 /* 254 * Note: This method MUST only be called via its accessor method. 255 */ 256 private boolean doComputeContains(double localX, double localY) { 257 double w = getWidth(); 258 double h = getHeight(); 259 return -w <= localX && localX <= w && 260 -h <= localY && localY <= h; 261 } 262 263 /* 264 * Note: This method MUST only be called via its accessor method. 265 */ 266 private boolean doComputeIntersects(PickRay pickRay, PickResultChooser pickResult) { 267 268 final double w = getWidth(); 269 final double h = getHeight(); 270 final double d = getDepth(); 271 final double hWidth = w / 2.0; 272 final double hHeight = h / 2.0; 273 final double hDepth = d / 2.0; 274 final Vec3d dir = pickRay.getDirectionNoClone(); 275 final double invDirX = dir.x == 0.0 ? Double.POSITIVE_INFINITY : (1.0 / dir.x); 276 final double invDirY = dir.y == 0.0 ? Double.POSITIVE_INFINITY : (1.0 / dir.y); 277 final double invDirZ = dir.z == 0.0 ? Double.POSITIVE_INFINITY : (1.0 / dir.z); 278 final Vec3d origin = pickRay.getOriginNoClone(); 279 final double originX = origin.x; 280 final double originY = origin.y; 281 final double originZ = origin.z; 282 final boolean signX = invDirX < 0.0; 283 final boolean signY = invDirY < 0.0; 284 final boolean signZ = invDirZ < 0.0; 285 286 double t0 = Double.NEGATIVE_INFINITY; 287 double t1 = Double.POSITIVE_INFINITY; 288 char side0 = '0'; 289 char side1 = '0'; 290 291 if (Double.isInfinite(invDirX)) { 292 if (-hWidth <= originX && hWidth >= originX) { 293 // move on, we are inside for the whole length 294 } else { 295 return false; 296 } 297 } else { 298 t0 = ((signX ? hWidth : -hWidth) - originX) * invDirX; 299 t1 = ((signX ? -hWidth : hWidth) - originX) * invDirX; 300 side0 = signX ? 'X' : 'x'; 301 side1 = signX ? 'x' : 'X'; 302 } 303 304 if (Double.isInfinite(invDirY)) { 305 if (-hHeight <= originY && hHeight >= originY) { 306 // move on, we are inside for the whole length 307 } else { 308 return false; 309 } 310 } else { 311 final double ty0 = ((signY ? hHeight : -hHeight) - originY) * invDirY; 312 final double ty1 = ((signY ? -hHeight : hHeight) - originY) * invDirY; 313 314 if ((t0 > ty1) || (ty0 > t1)) { 315 return false; 316 } 317 if (ty0 > t0) { 318 side0 = signY ? 'Y' : 'y'; 319 t0 = ty0; 320 } 321 if (ty1 < t1) { 322 side1 = signY ? 'y' : 'Y'; 323 t1 = ty1; 324 } 325 } 326 327 if (Double.isInfinite(invDirZ)) { 328 if (-hDepth <= originZ && hDepth >= originZ) { 329 // move on, we are inside for the whole length 330 } else { 331 return false; 332 } 333 } else { 334 double tz0 = ((signZ ? hDepth : -hDepth) - originZ) * invDirZ; 335 double tz1 = ((signZ ? -hDepth : hDepth) - originZ) * invDirZ; 336 337 if ((t0 > tz1) || (tz0 > t1)) { 338 return false; 339 } 340 if (tz0 > t0) { 341 side0 = signZ ? 'Z' : 'z'; 342 t0 = tz0; 343 } 344 if (tz1 < t1) { 345 side1 = signZ ? 'z' : 'Z'; 346 t1 = tz1; 347 } 348 } 349 350 char side = side0; 351 double t = t0; 352 final CullFace cullFace = getCullFace(); 353 final double minDistance = pickRay.getNearClip(); 354 final double maxDistance = pickRay.getFarClip(); 355 356 if (t0 > maxDistance) { 357 return false; 358 } 359 if (t0 < minDistance || cullFace == CullFace.FRONT) { 360 if (t1 >= minDistance && t1 <= maxDistance && cullFace != CullFace.BACK) { 361 side = side1; 362 t = t1; 363 } else { 364 return false; 365 } 366 } 367 368 if (Double.isInfinite(t) || Double.isNaN(t)) { 369 // We've got a nonsense pick ray or box size. 370 return false; 371 } 372 373 if (pickResult != null && pickResult.isCloser(t)) { 374 Point3D point = PickResultChooser.computePoint(pickRay, t); 375 376 Point2D txtCoords = null; 377 378 switch (side) { 379 case 'x': // left 380 txtCoords = new Point2D( 381 0.5 - point.getZ() / d, 382 0.5 + point.getY() / h); 383 break; 384 case 'X': // right 385 txtCoords = new Point2D( 386 0.5 + point.getZ() / d, 387 0.5 + point.getY() / h); 388 break; 389 case 'y': // top 390 txtCoords = new Point2D( 391 0.5 + point.getX() / w, 392 0.5 - point.getZ() / d); 393 break; 394 case 'Y': // bottom 395 txtCoords = new Point2D( 396 0.5 + point.getX() / w, 397 0.5 + point.getZ() / d); 398 break; 399 case 'z': // front 400 txtCoords = new Point2D( 401 0.5 + point.getX() / w, 402 0.5 + point.getY() / h); 403 break; 404 case 'Z': // back 405 txtCoords = new Point2D( 406 0.5 - point.getX() / w, 407 0.5 + point.getY() / h); 408 break; 409 default: 410 // No hit with any of the planes. We must have had a zero 411 // pick ray direction vector. Should never happen. 412 return false; 413 } 414 415 pickResult.offer(this, t, PickResult.FACE_UNDEFINED, point, txtCoords); 416 } 417 418 return true; 419 } 420 421 static TriangleMesh createMesh(float w, float h, float d) { 422 423 // NOTE: still create mesh for degenerated box 424 float hw = w / 2f; 425 float hh = h / 2f; 426 float hd = d / 2f; 427 428 float points[] = { 429 -hw, -hh, -hd, 430 hw, -hh, -hd, 431 hw, hh, -hd, 432 -hw, hh, -hd, 433 -hw, -hh, hd, 434 hw, -hh, hd, 435 hw, hh, hd, 436 -hw, hh, hd}; 437 438 float texCoords[] = {0, 0, 1, 0, 1, 1, 0, 1}; 439 440 // Specifies hard edges. 441 int faceSmoothingGroups[] = { 442 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 443 }; 444 445 int faces[] = { 446 0, 0, 2, 2, 1, 1, 447 2, 2, 0, 0, 3, 3, 448 1, 0, 6, 2, 5, 1, 449 6, 2, 1, 0, 2, 3, 450 5, 0, 7, 2, 4, 1, 451 7, 2, 5, 0, 6, 3, 452 4, 0, 3, 2, 0, 1, 453 3, 2, 4, 0, 7, 3, 454 3, 0, 6, 2, 2, 1, 455 6, 2, 3, 0, 7, 3, 456 4, 0, 1, 2, 5, 1, 457 1, 2, 4, 0, 0, 3, 458 }; 459 460 TriangleMesh mesh = new TriangleMesh(true); 461 mesh.getPoints().setAll(points); 462 mesh.getTexCoords().setAll(texCoords); 463 mesh.getFaces().setAll(faces); 464 mesh.getFaceSmoothingGroups().setAll(faceSmoothingGroups); 465 466 return mesh; 467 } 468 469 private static int generateKey(float w, float h, float d) { 470 int hash = 3; 471 hash = 97 * hash + Float.floatToIntBits(w); 472 hash = 97 * hash + Float.floatToIntBits(h); 473 hash = 97 * hash + Float.floatToIntBits(d); 474 return hash; 475 } 476 }