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