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