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 }