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 }