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 }