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.CylinderHelper;
  36 import com.sun.javafx.scene.shape.MeshHelper;
  37 import com.sun.javafx.sg.prism.NGCylinder;
  38 import com.sun.javafx.sg.prism.NGNode;
  39 import javafx.beans.property.DoubleProperty;
  40 import javafx.beans.property.SimpleDoubleProperty;
  41 import javafx.geometry.Point2D;
  42 import javafx.geometry.Point3D;
  43 import javafx.scene.Node;
  44 import javafx.scene.input.PickResult;
  45 import javafx.scene.transform.Rotate;
  46 
  47 /**
  48  * The {@code Cylinder} class defines a 3 dimensional cylinder with the specified size.
  49  * A {@code Cylinder} is a 3D geometry primitive created with a given radius and height.
  50  * It is centered at the origin.
  51  *
  52  * @since JavaFX 8.0
  53  */
  54 public class Cylinder extends Shape3D {
  55     static {
  56          // This is used by classes in different packages to get access to
  57          // private and package private methods.
  58         CylinderHelper.setCylinderAccessor(new CylinderHelper.CylinderAccessor() {
  59             @Override
  60             public NGNode doCreatePeer(Node node) {
  61                 return ((Cylinder) node).doCreatePeer();
  62             }
  63 
  64             @Override
  65             public void doUpdatePeer(Node node) {
  66                 ((Cylinder) node).doUpdatePeer();
  67             }
  68 
  69             @Override
  70             public BaseBounds doComputeGeomBounds(Node node,
  71             BaseBounds bounds, BaseTransform tx) {
  72                 return ((Cylinder) node).doComputeGeomBounds(bounds, tx);
  73             }
  74 
  75             @Override
  76             public boolean doComputeContains(Node node, double localX, double localY) {
  77                 return ((Cylinder) node).doComputeContains(localX, localY);
  78             }
  79 
  80             @Override
  81             public boolean doComputeIntersects(Node node, PickRay pickRay,
  82             PickResultChooser pickResult) {
  83                 return ((Cylinder) node).doComputeIntersects(pickRay, pickResult);
  84             }
  85         });
  86     }
  87     static final int DEFAULT_DIVISIONS = 64;
  88     static final double DEFAULT_RADIUS = 1;
  89     static final double DEFAULT_HEIGHT = 2;
  90 
  91     private int divisions = DEFAULT_DIVISIONS;
  92     private TriangleMesh mesh;
  93 
  94     {
  95         // To initialize the class helper at the begining each constructor of this class
  96         CylinderHelper.initHelper(this);
  97     }
  98     /**
  99      * Creates a new instance of {@code Cylinder} of radius of 1.0 and height of 2.0.
 100      * Resolution defaults to 15 divisions along X and Z axis.
 101      */
 102     public Cylinder() {
 103         this(DEFAULT_RADIUS, DEFAULT_HEIGHT, DEFAULT_DIVISIONS);
 104     }
 105 
 106     /**
 107      * Creates a new instance of {@code Cylinder} of a given radius and height.
 108      * Resolution defaults to 15 divisions along X and Z axis.
 109      *
 110      * @param radius Radius
 111      * @param height Height
 112      */
 113     public Cylinder (double radius, double height) {
 114         this(radius, height, DEFAULT_DIVISIONS);
 115     }
 116 
 117     /**
 118      * Creates a new instance of {@code Cylinder} of a given radius, height, and
 119      * divisions. Resolution defaults to 15 divisions along X and Z axis.
 120      *
 121      * Note that divisions should be at least 3. Any value less than that will be
 122      * clamped to 3.
 123      *
 124      * @param radius Radius
 125      * @param height Height
 126      * @param divisions Divisions
 127      */
 128     public Cylinder (double radius, double height, int divisions) {
 129         this.divisions = divisions < 3 ? 3 : divisions;
 130         setRadius(radius);
 131         setHeight(height);
 132     }
 133 
 134     /**
 135      * Defines the height or the Y dimension of the Cylinder.
 136      *
 137      * @defaultValue 2.0
 138      */
 139     private DoubleProperty height;
 140 
 141     public final void setHeight(double value) {
 142         heightProperty().set(value);
 143     }
 144 
 145     public final double getHeight() {
 146         return height == null ? 2 : height.get();
 147     }
 148 
 149     public final DoubleProperty heightProperty() {
 150         if (height == null) {
 151             height = new SimpleDoubleProperty(Cylinder.this, "height", DEFAULT_HEIGHT) {
 152                 @Override
 153                 public void invalidated() {
 154                     NodeHelper.markDirty(Cylinder.this, DirtyBits.MESH_GEOM);
 155                     manager.invalidateCylinderMesh(key);
 156                     key = 0;
 157                     NodeHelper.geomChanged(Cylinder.this);
 158                 }
 159             };
 160         }
 161         return height;
 162     }
 163 
 164     /**
 165      * Defines the radius in the Z plane of the Cylinder.
 166      *
 167      * @defaultValue 1.0
 168      */
 169     private DoubleProperty radius;
 170 
 171     public final void setRadius(double value) {
 172         radiusProperty().set(value);
 173     }
 174 
 175     public final double getRadius() {
 176         return radius == null ? 1 : radius.get();
 177     }
 178 
 179     public final DoubleProperty radiusProperty() {
 180         if (radius == null) {
 181             radius = new SimpleDoubleProperty(Cylinder.this, "radius", DEFAULT_RADIUS) {
 182                 @Override
 183                 public void invalidated() {
 184                     NodeHelper.markDirty(Cylinder.this, DirtyBits.MESH_GEOM);
 185                     manager.invalidateCylinderMesh(key);
 186                     key = 0;
 187                     NodeHelper.geomChanged(Cylinder.this);
 188                 }
 189             };
 190         }
 191         return radius;
 192     }
 193 
 194     /**
 195      * Retrieves the divisions attribute use to generate this cylinder.
 196      *
 197      * @return the divisions attribute.
 198      */
 199     public int getDivisions() {
 200         return divisions;
 201     }
 202 
 203     /*
 204      * Note: This method MUST only be called via its accessor method.
 205      */
 206     private void doUpdatePeer() {
 207         if (NodeHelper.isDirty(this, DirtyBits.MESH_GEOM)) {
 208             final NGCylinder peer = NodeHelper.getPeer(this);
 209             final float h = (float) getHeight();
 210             final float r = (float) getRadius();
 211             if (h < 0 || r < 0) {
 212                 peer.updateMesh(null);
 213             } else {
 214                 if (key == 0) {
 215                     key = generateKey(h, r, divisions);
 216                 }
 217                 mesh = manager.getCylinderMesh(h, r, divisions, key);
 218                 mesh.updatePG();
 219                 peer.updateMesh(mesh.getPGTriangleMesh());
 220             }
 221         }
 222     }
 223 
 224     /*
 225      * Note: This method MUST only be called via its accessor method.
 226      */
 227     private NGNode doCreatePeer() {
 228         return new NGCylinder();
 229     }
 230 
 231     /*
 232      * Note: This method MUST only be called via its accessor method.
 233      */
 234     private BaseBounds doComputeGeomBounds(BaseBounds bounds, BaseTransform tx) {
 235         final float h = (float) getHeight();
 236         final float r = (float) getRadius();
 237 
 238         if (r < 0 || h < 0) {
 239             return bounds.makeEmpty();
 240         }
 241 
 242         final float hh = h * 0.5f;
 243 
 244         bounds = bounds.deriveWithNewBounds(-r, -hh, -r, r, hh, r);
 245         bounds = tx.transform(bounds, bounds);
 246         return bounds;
 247     }
 248 
 249     /*
 250      * Note: This method MUST only be called via its accessor method.
 251      */
 252     private boolean doComputeContains(double localX, double localY) {
 253         double w = getRadius();
 254         double hh = getHeight()*.5f;
 255         return -w <= localX && localX <= w &&
 256                 -hh <= localY && localY <= hh;
 257     }
 258 
 259     /*
 260      * Note: This method MUST only be called via its accessor method.
 261      */
 262     private boolean doComputeIntersects(PickRay pickRay, PickResultChooser pickResult) {
 263 
 264         final boolean exactPicking = divisions < DEFAULT_DIVISIONS && mesh != null;
 265 
 266         final double r = getRadius();
 267         final Vec3d dir = pickRay.getDirectionNoClone();
 268         final double dirX = dir.x;
 269         final double dirY = dir.y;
 270         final double dirZ = dir.z;
 271         final Vec3d origin = pickRay.getOriginNoClone();
 272         final double originX = origin.x;
 273         final double originY = origin.y;
 274         final double originZ = origin.z;
 275         final double h = getHeight();
 276         final double halfHeight = h / 2.0;
 277         final CullFace cullFace = getCullFace();
 278 
 279         // Check the open cylinder first
 280 
 281         // Coeficients of a quadratic equation desribing intersection with an infinite cylinder
 282         final double a = dirX * dirX + dirZ * dirZ;
 283         final double b = 2 * (dirX * originX + dirZ * originZ);
 284         final double c = originX * originX + originZ * originZ - r * r;
 285 
 286         final double discriminant = b * b - 4 * a * c;
 287 
 288         double t0, t1, t = Double.POSITIVE_INFINITY;
 289         final double minDistance = pickRay.getNearClip();
 290         final double maxDistance = pickRay.getFarClip();
 291 
 292         if (discriminant >= 0 && (dirX != 0.0 || dirZ != 0.0)) {
 293             // the line hits the infinite cylinder
 294 
 295             final double distSqrt = Math.sqrt(discriminant);
 296             final double q = (b < 0) ? (-b - distSqrt) / 2.0 : (-b + distSqrt) / 2.0;
 297 
 298             t0 = q / a;
 299             t1 = c / q;
 300 
 301             if (t0 > t1) {
 302                 double temp = t0;
 303                 t0 = t1;
 304                 t1 = temp;
 305             }
 306 
 307             // let's see if the hit is between clipping planes and within the cylinder's height
 308             final double y0 = originY + t0 * dirY;
 309             if (t0 < minDistance || y0 < -halfHeight || y0 > halfHeight || cullFace == CullFace.FRONT) {
 310                 final double y1 = originY + t1 * dirY;
 311                 if (t1 >= minDistance && t1 <= maxDistance && y1 >= -halfHeight && y1 <= halfHeight) {
 312                     if (cullFace != CullFace.BACK || exactPicking) {
 313                         // t0 is outside or behind but t1 hits.
 314 
 315                         // We need to do the exact picking even if the back wall
 316                         // is culled because the front facing triangles may
 317                         // still be in front of us
 318                         t = t1;
 319                     }
 320                 } // else no hit (but we need to check the caps)
 321             } else if (t0 <= maxDistance) {
 322                 // t0 hits the height between clipping planes
 323                 t = t0;
 324             } // else no hit (but we need to check the caps)
 325         }
 326 
 327         // Now check the caps
 328 
 329         // if we already know we are going to do the exact picking,
 330         // there is no need to check the caps
 331 
 332         boolean topCap = false, bottomCap = false;
 333         if (t == Double.POSITIVE_INFINITY || !exactPicking) {
 334             final double tBottom = (-halfHeight - originY) / dirY;
 335             final double tTop = (halfHeight - originY) / dirY;
 336             boolean isT0Bottom = false;
 337 
 338             if (tBottom < tTop) {
 339                 t0 = tBottom;
 340                 t1 = tTop;
 341                 isT0Bottom = true;
 342             } else {
 343                 t0 = tTop;
 344                 t1 = tBottom;
 345             }
 346 
 347             if (t0 >= minDistance && t0 <= maxDistance && t0 < t && cullFace != CullFace.FRONT) {
 348                 final double tX = originX + dirX * t0;
 349                 final double tZ = originZ + dirZ * t0;
 350                 if (tX * tX + tZ * tZ <= r * r) {
 351                     bottomCap = isT0Bottom; topCap = !isT0Bottom;
 352                     t = t0;
 353                 }
 354             }
 355 
 356             if (t1 >= minDistance && t1 <= maxDistance && t1 < t && (cullFace != CullFace.BACK || exactPicking)) {
 357                 final double tX = originX + dirX * t1;
 358                 final double tZ = originZ + dirZ * t1;
 359                 if (tX * tX + tZ * tZ <= r * r) {
 360                     topCap = isT0Bottom; bottomCap = !isT0Bottom;
 361                     t = t1;
 362                 }
 363             }
 364         }
 365 
 366         if (Double.isInfinite(t) || Double.isNaN(t)) {
 367             // no hit
 368             return false;
 369         }
 370 
 371         if (exactPicking) {
 372             return MeshHelper.computeIntersects(mesh, pickRay, pickResult, this, cullFace, false);
 373         }
 374 
 375         if (pickResult != null && pickResult.isCloser(t)) {
 376             final Point3D point = PickResultChooser.computePoint(pickRay, t);
 377 
 378             Point2D txCoords;
 379             if (topCap) {
 380                 txCoords = new Point2D(
 381                         0.5 + point.getX() / (2 * r),
 382                         0.5 + point.getZ() / (2 * r));
 383             } else if (bottomCap) {
 384                 txCoords = new Point2D(
 385                         0.5 + point.getX() / (2 * r),
 386                         0.5 - point.getZ() / (2 * r));
 387             } else {
 388                 final Point3D proj = new Point3D(point.getX(), 0, point.getZ());
 389                 final Point3D cross = proj.crossProduct(Rotate.Z_AXIS);
 390                 double angle = proj.angle(Rotate.Z_AXIS);
 391                 if (cross.getY() > 0) {
 392                     angle = 360 - angle;
 393                 }
 394                 txCoords = new Point2D(1 - angle / 360, 0.5 + point.getY() / h);
 395             }
 396 
 397             pickResult.offer(this, t, PickResult.FACE_UNDEFINED, point, txCoords);
 398         }
 399         return true;
 400     }
 401 
 402     static TriangleMesh createMesh(int div, float h, float r) {
 403 
 404         // NOTE: still create mesh for degenerated cylinder
 405         final int nPonits = div * 2 + 2;
 406         final int tcCount = (div + 1) * 4 + 1; // 2 cap tex
 407         final int faceCount = div * 4;
 408 
 409         float textureDelta = 1.f / 256;
 410 
 411         float dA = 1.f / div;
 412         h *= .5f;
 413 
 414         float points[] = new float[nPonits * 3];
 415         float tPoints[] = new float[tcCount * 2];
 416         int faces[] = new int[faceCount * 6];
 417         int smoothing[] = new int[faceCount];
 418 
 419         int pPos = 0, tPos = 0;
 420 
 421         for (int i = 0; i < div; ++i) {
 422             double a = dA * i * 2 * Math.PI;
 423 
 424             points[pPos + 0] = (float) (Math.sin(a) * r);
 425             points[pPos + 2] = (float) (Math.cos(a) * r);
 426             points[pPos + 1] = h;
 427             tPoints[tPos + 0] = 1 - dA * i;
 428             tPoints[tPos + 1] = 1 - textureDelta;
 429             pPos += 3; tPos += 2;
 430         }
 431 
 432         // top edge
 433         tPoints[tPos + 0] = 0;
 434         tPoints[tPos + 1] = 1 - textureDelta;
 435         tPos += 2;
 436 
 437         for (int i = 0; i < div; ++i) {
 438             double a = dA * i * 2 * Math.PI;
 439             points[pPos + 0] = (float) (Math.sin(a) * r);
 440             points[pPos + 2] = (float) (Math.cos(a) * r);
 441             points[pPos + 1] = -h;
 442             tPoints[tPos + 0] = 1 - dA * i;
 443             tPoints[tPos + 1] = textureDelta;
 444             pPos += 3; tPos += 2;
 445         }
 446 
 447         // bottom edge
 448         tPoints[tPos + 0] = 0;
 449         tPoints[tPos + 1] = textureDelta;
 450         tPos += 2;
 451 
 452         // add cap central points
 453         points[pPos + 0] = 0;
 454         points[pPos + 1] = h;
 455         points[pPos + 2] = 0;
 456         points[pPos + 3] = 0;
 457         points[pPos + 4] = -h;
 458         points[pPos + 5] = 0;
 459         pPos += 6;
 460 
 461         // add cap central points
 462         // bottom cap
 463         for (int i = 0; i <= div; ++i) {
 464             double a = (i < div) ? (dA * i * 2) * Math.PI: 0;
 465             tPoints[tPos + 0] = (float) (Math.sin(a) * 0.5f) + 0.5f;
 466             tPoints[tPos + 1] = (float) (Math.cos(a) * 0.5f) + 0.5f;
 467             tPos += 2;
 468         }
 469 
 470         // top cap
 471         for (int i = 0; i <= div; ++i) {
 472             double a = (i < div) ? (dA * i * 2) * Math.PI: 0;
 473             tPoints[tPos + 0] = 0.5f + (float) (Math.sin(a) * 0.5f);
 474             tPoints[tPos + 1] = 0.5f - (float) (Math.cos(a) * 0.5f);
 475             tPos += 2;
 476         }
 477 
 478         tPoints[tPos + 0] = .5f;
 479         tPoints[tPos + 1] = .5f;
 480         tPos += 2;
 481 
 482         int fIndex = 0;
 483 
 484         // build body faces
 485         for (int p0 = 0; p0 < div; ++p0) {
 486             int p1 = p0 + 1;
 487             int p2 = p0 + div;
 488             int p3 = p1 + div;
 489 
 490             // add p0, p1, p2
 491             faces[fIndex+0] = p0;
 492             faces[fIndex+1] = p0;
 493             faces[fIndex+2] = p2;
 494             faces[fIndex+3] = p2 + 1;
 495             faces[fIndex+4] = p1 == div ? 0 : p1;
 496             faces[fIndex+5] = p1;
 497             fIndex += 6;
 498 
 499             // add p3, p2, p1
 500             // *faces++ = SmFace(p3,p1,p2, p3,p1,p2, 1);
 501             faces[fIndex+0] = p3 % div == 0 ? p3 - div : p3;
 502             faces[fIndex+1] = p3 + 1;
 503             faces[fIndex+2] = p1 == div ? 0 : p1;
 504             faces[fIndex+3] = p1;
 505             faces[fIndex+4] = p2;
 506             faces[fIndex+5] = p2 + 1;
 507             fIndex += 6;
 508 
 509         }
 510         // build cap faces
 511         int tStart = (div + 1) * 2;
 512         int t1 = (div + 1) * 4;
 513         int p1 = div * 2;
 514 
 515         // bottom cap
 516         for (int p0 = 0; p0 < div; ++p0) {
 517             int p2 = p0 + 1;
 518             int t0 = tStart + p0;
 519             int t2 = t0 + 1;
 520 
 521             // add p0, p1, p2
 522             faces[fIndex+0] = p0;
 523             faces[fIndex+1] = t0;
 524             faces[fIndex+2] = p2 == div ? 0 : p2;
 525             faces[fIndex+3] = t2;
 526             faces[fIndex+4] = p1;
 527             faces[fIndex+5] = t1;
 528             fIndex += 6;
 529         }
 530 
 531         p1 = div * 2 + 1;
 532         tStart = (div + 1) * 3;
 533 
 534         // top cap
 535         for (int p0 = 0; p0 < div; ++p0) {
 536             int p2 = p0 + 1 + div;
 537             int t0 = tStart + p0;
 538             int t2 = t0 + 1;
 539 
 540             //*faces++ = SmFace(p0+div+1,p1,p2, t0,t1,t2, 2);
 541             faces[fIndex+0] = p0 + div;
 542             faces[fIndex+1] = t0;
 543             faces[fIndex+2] = p1;
 544             faces[fIndex+3] = t1;
 545             faces[fIndex+4] = p2 % div == 0 ? p2 - div : p2;
 546             faces[fIndex+5] = t2;
 547             fIndex += 6;
 548         }
 549 
 550         for (int i = 0; i < div * 2; ++i) {
 551             smoothing[i] = 1;
 552         }
 553         for (int i = div * 2; i < div * 4; ++i) {
 554             smoothing[i] = 2;
 555         }
 556 
 557         TriangleMesh m = new TriangleMesh(true);
 558         m.getPoints().setAll(points);
 559         m.getTexCoords().setAll(tPoints);
 560         m.getFaces().setAll(faces);
 561         m.getFaceSmoothingGroups().setAll(smoothing);
 562 
 563         return m;
 564     }
 565 
 566     private static int generateKey(float h, float r, int div) {
 567         int hash = 7;
 568         hash = 47 * hash + Float.floatToIntBits(h);
 569         hash = 47 * hash + Float.floatToIntBits(r);
 570         hash = 47 * hash + div;
 571         return hash;
 572     }
 573 }