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; 27 28 import com.sun.javafx.css.StyleManager; 29 import com.sun.javafx.scene.traversal.Direction; 30 import com.sun.javafx.scene.traversal.SubSceneTraversalEngine; 31 import com.sun.javafx.scene.traversal.TopMostTraversalEngine; 32 import javafx.application.ConditionalFeature; 33 import javafx.application.Platform; 34 import javafx.beans.NamedArg; 35 import javafx.beans.property.*; 36 import javafx.geometry.NodeOrientation; 37 import javafx.geometry.Point3D; 38 import javafx.scene.input.PickResult; 39 import javafx.scene.paint.Paint; 40 41 import java.util.ArrayList; 42 import java.util.List; 43 44 import com.sun.javafx.geom.BaseBounds; 45 import com.sun.javafx.geom.PickRay; 46 import com.sun.javafx.geom.transform.BaseTransform; 47 import com.sun.javafx.scene.CssFlags; 48 import com.sun.javafx.scene.DirtyBits; 49 import com.sun.javafx.scene.NodeHelper; 50 import com.sun.javafx.scene.SubSceneHelper; 51 import com.sun.javafx.scene.input.PickResultChooser; 52 import com.sun.javafx.sg.prism.NGCamera; 53 import com.sun.javafx.sg.prism.NGLightBase; 54 import com.sun.javafx.sg.prism.NGNode; 55 import com.sun.javafx.sg.prism.NGSubScene; 56 import com.sun.javafx.tk.Toolkit; 57 58 import com.sun.javafx.logging.PlatformLogger; 59 60 /** 61 * The {@code SubScene} class is the container for content in a scene graph. 62 * {@code SubScene} provides separation of different parts of a scene, each 63 * of which can be rendered with a different camera, depth buffer, or scene 64 * anti-aliasing. A {@code SubScene} is embedded into the main scene or another 65 * sub-scene. 66 * <p> 67 * An application may request depth buffer support or scene anti-aliasing 68 * support at the creation of a {@code SubScene}. A sub-scene with only 2D 69 * shapes and without any 3D transforms does not need a depth buffer nor scene 70 * anti-aliasing support. A sub-scene containing 3D shapes or 2D shapes with 3D 71 * transforms may use depth buffer support for proper depth sorted rendering; to 72 * avoid depth fighting (also known as Z fighting), disable depth testing on 2D 73 * shapes that have no 3D transforms. See 74 * {@link Node#depthTestProperty depthTest} for more information. A sub-scene 75 * with 3D shapes may enable scene anti-aliasing to improve its rendering 76 * quality. 77 * <p> 78 * The depthBuffer and antiAliasing flags are conditional features. With the 79 * respective default values of: false and {@code SceneAntialiasing.DISABLED}. 80 * See {@link javafx.application.ConditionalFeature#SCENE3D ConditionalFeature.SCENE3D} 81 * for more information. 82 * 83 * <p> 84 * Possible use cases are: 85 * <ul> 86 * <li> Mixing 2D and 3D content </li> 87 * <li> Overlay for UI controls </li> 88 * <li> Underlay for background </li> 89 * <li> Heads-up display </li> 90 * </ul> 91 * 92 * <p> 93 * A default headlight will be added to a {@code SubScene} that contains one or more 94 * {@code Shape3D} nodes, but no light nodes. This light source is a 95 * {@code Color.WHITE} {@code PointLight} placed at the camera position. 96 * </p> 97 * 98 * @since JavaFX 8.0 99 */ 100 public class SubScene extends Node { 101 static { 102 // This is used by classes in different packages to get access to 103 // private and package private methods. 104 SubSceneHelper.setSubSceneAccessor(new SubSceneHelper.SubSceneAccessor() { 105 @Override 106 public NGNode doCreatePeer(Node node) { 107 return ((SubScene) node).doCreatePeer(); 108 } 109 110 @Override 111 public void doUpdatePeer(Node node) { 112 ((SubScene) node).doUpdatePeer(); 113 } 114 115 @Override 116 public BaseBounds doComputeGeomBounds(Node node, 117 BaseBounds bounds, BaseTransform tx) { 118 return ((SubScene) node).doComputeGeomBounds(bounds, tx); 119 } 120 121 @Override 122 public boolean doComputeContains(Node node, double localX, double localY) { 123 return ((SubScene) node).doComputeContains(localX, localY); 124 } 125 126 @Override 127 public void doProcessCSS(Node node) { 128 ((SubScene) node).doProcessCSS(); 129 } 130 131 @Override 132 public void doPickNodeLocal(Node node, PickRay localPickRay, 133 PickResultChooser result) { 134 ((SubScene) node).doPickNodeLocal(localPickRay, result); 135 } 136 137 @Override 138 public boolean isDepthBuffer(SubScene subScene) { 139 return subScene.isDepthBufferInternal(); 140 }; 141 142 @Override 143 public Camera getEffectiveCamera(SubScene subScene) { 144 return subScene.getEffectiveCamera(); 145 } 146 147 }); 148 } 149 150 { 151 // To initialize the class helper at the begining each constructor of this class 152 SubSceneHelper.initHelper(this); 153 } 154 /** 155 * Creates a {@code SubScene} for a specific root Node with a specific size. 156 * 157 * @param root The root node of the scene graph 158 * @param width The width of the sub-scene 159 * @param height The height of the sub-scene 160 * 161 * @throws NullPointerException if root is null 162 */ 163 public SubScene(@NamedArg("root") Parent root, @NamedArg("width") double width, @NamedArg("height") double height) { 164 this(root, width, height, false, SceneAntialiasing.DISABLED); 165 } 166 167 /** 168 * Constructs a {@code SubScene} consisting of a root, with a dimension of width and 169 * height, specifies whether a depth buffer is created for this scene and 170 * specifies whether scene anti-aliasing is requested. 171 * <p> 172 * A sub-scene with only 2D shapes and without any 3D transforms does not 173 * need a depth buffer nor scene anti-aliasing support. A sub-scene 174 * containing 3D shapes or 2D shapes with 3D transforms may use depth buffer 175 * support for proper depth sorted rendering; to avoid depth fighting (also 176 * known as Z fighting), disable depth testing on 2D shapes that have no 3D 177 * transforms. See {@link Node#depthTestProperty depthTest} for more 178 * information. A sub-scene with 3D shapes may enable scene anti-aliasing to 179 * improve its rendering quality. 180 * 181 * @param root The root node of the scene graph 182 * @param width The width of the sub-scene 183 * @param height The height of the sub-scene 184 * @param depthBuffer The depth buffer flag 185 * @param antiAliasing The sub-scene anti-aliasing attribute. A value of 186 * {@code null} is treated as DISABLED. 187 * <p> 188 * The depthBuffer and antiAliasing flags are conditional features. With the 189 * respective default values of: false and {@code SceneAntialiasing.DISABLED}. 190 * See {@link javafx.application.ConditionalFeature#SCENE3D ConditionalFeature.SCENE3D} 191 * for more information. 192 * 193 * @throws NullPointerException if root is null 194 * 195 * @see javafx.scene.Node#setDepthTest(DepthTest) 196 */ 197 public SubScene(@NamedArg("root") Parent root, @NamedArg("width") double width, @NamedArg("height") double height, 198 @NamedArg("depthBuffer") boolean depthBuffer, @NamedArg("antiAliasing") SceneAntialiasing antiAliasing) 199 { 200 this.depthBuffer = depthBuffer; 201 this.antiAliasing = antiAliasing; 202 boolean isAntiAliasing = !(antiAliasing == null || antiAliasing == SceneAntialiasing.DISABLED); 203 setRoot(root); 204 setWidth(width); 205 setHeight(height); 206 207 if ((depthBuffer || isAntiAliasing) && !is3DSupported) { 208 String logname = SubScene.class.getName(); 209 PlatformLogger.getLogger(logname).warning("System can't support " 210 + "ConditionalFeature.SCENE3D"); 211 } 212 if (isAntiAliasing && !Toolkit.getToolkit().isMSAASupported()) { 213 String logname = SubScene.class.getName(); 214 PlatformLogger.getLogger(logname).warning("System can't support " 215 + "antiAliasing"); 216 } 217 } 218 219 private static boolean is3DSupported = 220 Platform.isSupported(ConditionalFeature.SCENE3D); 221 222 private final SceneAntialiasing antiAliasing; 223 224 /** 225 * Return the defined {@code SceneAntialiasing} for this {@code SubScene}. 226 * <p> 227 * Note: this is a conditional feature. See 228 * {@link javafx.application.ConditionalFeature#SCENE3D ConditionalFeature.SCENE3D} 229 * and {@link javafx.scene.SceneAntialiasing SceneAntialiasing} 230 * for more information. 231 * @return the SceneAntialiasing for this sub-scene 232 * @since JavaFX 8.0 233 */ 234 public final SceneAntialiasing getAntiAliasing() { 235 return antiAliasing; 236 } 237 238 private final boolean depthBuffer; 239 240 /** 241 * Retrieves the depth buffer attribute for this {@code SubScene}. 242 * @return the depth buffer attribute. 243 */ 244 public final boolean isDepthBuffer() { 245 return depthBuffer; 246 } 247 248 private boolean isDepthBufferInternal() { 249 return is3DSupported ? depthBuffer : false; 250 } 251 252 /** 253 * Defines the root {@code Node} of the {@code SubScene} scene graph. 254 * If a {@code Group} is used as the root, the 255 * contents of the scene graph will be clipped by the {@code SubScene}'s width and height. 256 * 257 * {@code SubScene} doesn't accept null root. 258 * 259 */ 260 private ObjectProperty<Parent> root; 261 262 public final void setRoot(Parent value) { 263 rootProperty().set(value); 264 } 265 266 public final Parent getRoot() { 267 return root == null ? null : root.get(); 268 } 269 270 public final ObjectProperty<Parent> rootProperty() { 271 if (root == null) { 272 root = new ObjectPropertyBase<Parent>() { 273 private Parent oldRoot; 274 275 private void forceUnbind() { 276 System.err.println("Unbinding illegal root."); 277 unbind(); 278 } 279 280 @Override 281 protected void invalidated() { 282 Parent _value = get(); 283 284 if (_value == null) { 285 if (isBound()) { forceUnbind(); } 286 throw new NullPointerException("Scene's root cannot be null"); 287 } 288 if (_value.getParent() != null) { 289 if (isBound()) { forceUnbind(); } 290 throw new IllegalArgumentException(_value + 291 "is already inside a scene-graph and cannot be set as root"); 292 } 293 if (_value.getClipParent() != null) { 294 if (isBound()) forceUnbind(); 295 throw new IllegalArgumentException(_value + 296 "is set as a clip on another node, so cannot be set as root"); 297 } 298 if ((_value.getScene() != null && 299 _value.getScene().getRoot() == _value) || 300 (_value.getSubScene() != null && 301 _value.getSubScene().getRoot() == _value && 302 _value.getSubScene() != SubScene.this)) 303 { 304 if (isBound()) { forceUnbind(); } 305 throw new IllegalArgumentException(_value + 306 "is already set as root of another scene or subScene"); 307 } 308 309 // disabled, isTreeVisible and isTreeShowing properties are inherited 310 _value.setTreeVisible(isTreeVisible()); 311 _value.setDisabled(isDisabled()); 312 _value.setTreeShowing(isTreeShowing()); 313 314 if (oldRoot != null) { 315 StyleManager.getInstance().forget(SubScene.this); 316 oldRoot.setScenes(null, null); 317 } 318 oldRoot = _value; 319 _value.getStyleClass().add(0, "root"); 320 _value.setScenes(getScene(), SubScene.this); 321 markDirty(SubSceneDirtyBits.ROOT_SG_DIRTY); 322 _value.resize(getWidth(), getHeight()); // maybe no-op if root is not resizable 323 _value.requestLayout(); 324 } 325 326 @Override 327 public Object getBean() { 328 return SubScene.this; 329 } 330 331 @Override 332 public String getName() { 333 return "root"; 334 } 335 }; 336 } 337 return root; 338 } 339 340 /** 341 * Specifies the type of camera use for rendering this {@code SubScene}. 342 * If {@code camera} is null, a parallel camera is used for rendering. 343 * It is illegal to set a camera that belongs to other {@code Scene} 344 * or {@code SubScene}. 345 * <p> 346 * Note: this is a conditional feature. See 347 * {@link javafx.application.ConditionalFeature#SCENE3D ConditionalFeature.SCENE3D} 348 * for more information. 349 * 350 * @defaultValue null 351 */ 352 private ObjectProperty<Camera> camera; 353 354 public final void setCamera(Camera value) { 355 cameraProperty().set(value); 356 } 357 358 public final Camera getCamera() { 359 return camera == null ? null : camera.get(); 360 } 361 362 public final ObjectProperty<Camera> cameraProperty() { 363 if (camera == null) { 364 camera = new ObjectPropertyBase<Camera>() { 365 Camera oldCamera = null; 366 367 @Override 368 protected void invalidated() { 369 Camera _value = get(); 370 if (_value != null) { 371 if (_value instanceof PerspectiveCamera 372 && !SubScene.is3DSupported) { 373 String logname = SubScene.class.getName(); 374 PlatformLogger.getLogger(logname).warning("System can't support " 375 + "ConditionalFeature.SCENE3D"); 376 } 377 // Illegal value if it belongs to any scene or other subscene 378 if ((_value.getScene() != null || _value.getSubScene() != null) 379 && (_value.getScene() != getScene() || _value.getSubScene() != SubScene.this)) { 380 throw new IllegalArgumentException(_value 381 + "is already part of other scene or subscene"); 382 } 383 // throws exception if the camera already has a different owner 384 _value.setOwnerSubScene(SubScene.this); 385 _value.setViewWidth(getWidth()); 386 _value.setViewHeight(getHeight()); 387 } 388 markDirty(SubSceneDirtyBits.CAMERA_DIRTY); 389 if (oldCamera != null && oldCamera != _value) { 390 oldCamera.setOwnerSubScene(null); 391 } 392 oldCamera = _value; 393 } 394 395 @Override 396 public Object getBean() { 397 return SubScene.this; 398 } 399 400 @Override 401 public String getName() { 402 return "camera"; 403 } 404 }; 405 } 406 return camera; 407 } 408 409 private Camera defaultCamera; 410 411 Camera getEffectiveCamera() { 412 final Camera cam = getCamera(); 413 if (cam == null 414 || (cam instanceof PerspectiveCamera && !is3DSupported)) { 415 if (defaultCamera == null) { 416 defaultCamera = new ParallelCamera(); 417 defaultCamera.setOwnerSubScene(this); 418 defaultCamera.setViewWidth(getWidth()); 419 defaultCamera.setViewHeight(getHeight()); 420 } 421 return defaultCamera; 422 } 423 424 return cam; 425 } 426 427 // Used by the camera 428 final void markContentDirty() { 429 markDirty(SubSceneDirtyBits.CONTENT_DIRTY); 430 } 431 432 /** 433 * Defines the width of this {@code SubScene} 434 * 435 * @defaultValue 0.0 436 */ 437 private DoubleProperty width; 438 439 public final void setWidth(double value) { 440 widthProperty().set(value); 441 } 442 443 public final double getWidth() { 444 return width == null ? 0.0 : width.get(); 445 } 446 447 public final DoubleProperty widthProperty() { 448 if (width == null) { 449 width = new DoublePropertyBase() { 450 451 @Override 452 public void invalidated() { 453 final Parent _root = getRoot(); 454 //TODO - use a better method to update mirroring 455 if (_root.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) { 456 NodeHelper.transformsChanged(_root); 457 } 458 if (_root.isResizable()) { 459 _root.resize(get() - _root.getLayoutX() - _root.getTranslateX(), _root.getLayoutBounds().getHeight()); 460 } 461 markDirty(SubSceneDirtyBits.SIZE_DIRTY); 462 NodeHelper.geomChanged(SubScene.this); 463 464 getEffectiveCamera().setViewWidth(get()); 465 } 466 467 @Override 468 public Object getBean() { 469 return SubScene.this; 470 } 471 472 @Override 473 public String getName() { 474 return "width"; 475 } 476 }; 477 } 478 return width; 479 } 480 481 /** 482 * Defines the height of this {@code SubScene} 483 * 484 * @defaultValue 0.0 485 */ 486 private DoubleProperty height; 487 488 public final void setHeight(double value) { 489 heightProperty().set(value); 490 } 491 492 public final double getHeight() { 493 return height == null ? 0.0 : height.get(); 494 } 495 496 public final DoubleProperty heightProperty() { 497 if (height == null) { 498 height = new DoublePropertyBase() { 499 500 @Override 501 public void invalidated() { 502 final Parent _root = getRoot(); 503 if (_root.isResizable()) { 504 _root.resize(_root.getLayoutBounds().getWidth(), get() - _root.getLayoutY() - _root.getTranslateY()); 505 } 506 markDirty(SubSceneDirtyBits.SIZE_DIRTY); 507 NodeHelper.geomChanged(SubScene.this); 508 509 getEffectiveCamera().setViewHeight(get()); 510 } 511 512 @Override 513 public Object getBean() { 514 return SubScene.this; 515 } 516 517 @Override 518 public String getName() { 519 return "height"; 520 } 521 }; 522 } 523 return height; 524 } 525 526 /** 527 * Defines the background fill of this {@code SubScene}. Both a {@code null} 528 * value meaning paint no background and a {@link javafx.scene.paint.Paint} 529 * with transparency are supported. The default value is null. 530 * 531 * @defaultValue null 532 */ 533 private ObjectProperty<Paint> fill; 534 535 public final void setFill(Paint value) { 536 fillProperty().set(value); 537 } 538 539 public final Paint getFill() { 540 return fill == null ? null : fill.get(); 541 } 542 543 public final ObjectProperty<Paint> fillProperty() { 544 if (fill == null) { 545 fill = new ObjectPropertyBase<Paint>(null) { 546 547 @Override 548 protected void invalidated() { 549 markDirty(SubSceneDirtyBits.FILL_DIRTY); 550 } 551 552 @Override 553 public Object getBean() { 554 return SubScene.this; 555 } 556 557 @Override 558 public String getName() { 559 return "fill"; 560 } 561 }; 562 } 563 return fill; 564 } 565 566 /* 567 * Note: This method MUST only be called via its accessor method. 568 */ 569 private void doUpdatePeer() { 570 // TODO deal with clip node 571 572 dirtyNodes = false; 573 if (isDirty()) { 574 NGSubScene peer = getPeer(); 575 final Camera cam = getEffectiveCamera(); 576 boolean contentChanged = false; 577 if (cam.getSubScene() == null && 578 isDirty(SubSceneDirtyBits.CONTENT_DIRTY)) { 579 // When camera is not a part of the graph, then its 580 // owner(subscene) must take care of syncing it. And when a 581 // property on the camera changes it will mark subscenes 582 // CONTENT_DIRTY. 583 cam.syncPeer(); 584 } 585 if (isDirty(SubSceneDirtyBits.FILL_DIRTY)) { 586 Object platformPaint = getFill() == null ? null : 587 Toolkit.getPaintAccessor().getPlatformPaint(getFill()); 588 peer.setFillPaint(platformPaint); 589 contentChanged = true; 590 } 591 if (isDirty(SubSceneDirtyBits.SIZE_DIRTY)) { 592 // Note change in size is a geom change and is handled by peer 593 peer.setWidth((float)getWidth()); 594 peer.setHeight((float)getHeight()); 595 } 596 if (isDirty(SubSceneDirtyBits.CAMERA_DIRTY)) { 597 peer.setCamera((NGCamera) cam.getPeer()); 598 contentChanged = true; 599 } 600 if (isDirty(SubSceneDirtyBits.ROOT_SG_DIRTY)) { 601 peer.setRoot(getRoot().getPeer()); 602 contentChanged = true; 603 } 604 contentChanged |= syncLights(); 605 if (contentChanged || isDirty(SubSceneDirtyBits.CONTENT_DIRTY)) { 606 peer.markContentDirty(); 607 } 608 609 clearDirtyBits(); 610 } 611 612 } 613 614 @Override 615 void nodeResolvedOrientationChanged() { 616 getRoot().parentResolvedOrientationInvalidated(); 617 } 618 619 /*********************************************************************** 620 * CSS * 621 **********************************************************************/ 622 /* 623 * Note: This method MUST only be called via its accessor method. 624 */ 625 private void doProcessCSS() { 626 // Nothing to do... 627 if (cssFlag == CssFlags.CLEAN) { return; } 628 629 if (getRoot().cssFlag == CssFlags.CLEAN) { 630 getRoot().cssFlag = cssFlag; 631 } 632 SubSceneHelper.superProcessCSS(this); 633 getRoot().processCSS(); 634 } 635 636 @Override 637 void processCSS() { 638 Parent root = getRoot(); 639 if (root.isDirty(DirtyBits.NODE_CSS)) { 640 root.clearDirty(DirtyBits.NODE_CSS); 641 if (cssFlag == CssFlags.CLEAN) { cssFlag = CssFlags.UPDATE; } 642 } 643 super.processCSS(); 644 } 645 646 private ObjectProperty<String> userAgentStylesheet = null; 647 /** 648 * @return the userAgentStylesheet property. 649 * @see #getUserAgentStylesheet() 650 * @see #setUserAgentStylesheet(String) 651 * @since JavaFX 8u20 652 */ 653 public final ObjectProperty<String> userAgentStylesheetProperty() { 654 if (userAgentStylesheet == null) { 655 userAgentStylesheet = new SimpleObjectProperty<String>(SubScene.this, "userAgentStylesheet", null) { 656 @Override protected void invalidated() { 657 StyleManager.getInstance().forget(SubScene.this); 658 reapplyCSS(); 659 } 660 }; 661 } 662 return userAgentStylesheet; 663 } 664 665 /** 666 * Get the URL of the user-agent stylesheet that will be used by this SubScene. If the URL has not been set, 667 * the platform-default user-agent stylesheet will be used. 668 * <p> 669 * For additional information about using CSS with the scene graph, 670 * see the <a href="doc-files/cssref.html">CSS Reference Guide</a>. 671 * </p> 672 * @return The URL of the user-agent stylesheet that will be used by this SubScene, 673 * or null if has not been set. 674 * @since JavaFX 8u20 675 */ 676 public final String getUserAgentStylesheet() { 677 return userAgentStylesheet == null ? null : userAgentStylesheet.get(); 678 } 679 680 /** 681 * Set the URL of the user-agent stylesheet that will be used by this SubScene in place of the 682 * the platform-default user-agent stylesheet. If the URL does not resolve to a valid location, 683 * the platform-default user-agent stylesheet will be used. 684 * <p> 685 * For additional information about using CSS with the scene graph, 686 * see the <a href="doc-files/cssref.html">CSS Reference Guide</a>. 687 * </p> 688 * @param url The URL is a hierarchical URI of the form [scheme:][//authority][path]. If the URL 689 * does not have a [scheme:] component, the URL is considered to be the [path] component only. 690 * Any leading '/' character of the [path] is ignored and the [path] is treated as a path relative to 691 * the root of the application's classpath. 692 * @since JavaFX 8u20 693 */ 694 public final void setUserAgentStylesheet(String url) { 695 userAgentStylesheetProperty().set(url); 696 } 697 698 @Override void updateBounds() { 699 super.updateBounds(); 700 getRoot().updateBounds(); 701 } 702 703 /* 704 * Note: This method MUST only be called via its accessor method. 705 */ 706 private NGNode doCreatePeer() { 707 if (!is3DSupported) { 708 return new NGSubScene(false, false); 709 } 710 boolean aa = !(antiAliasing == null || antiAliasing == SceneAntialiasing.DISABLED); 711 return new NGSubScene(depthBuffer, aa && Toolkit.getToolkit().isMSAASupported()); 712 } 713 714 /* 715 * Note: This method MUST only be called via its accessor method. 716 */ 717 private BaseBounds doComputeGeomBounds(BaseBounds bounds, BaseTransform tx) { 718 int w = (int)Math.ceil(width.get()); 719 int h = (int)Math.ceil(height.get()); 720 bounds = bounds.deriveWithNewBounds(0.0f, 0.0f, 0.0f, 721 w, h, 0.0f); 722 bounds = tx.transform(bounds, bounds); 723 return bounds; 724 } 725 726 /*********************************************************************** 727 * Dirty Bits * 728 **********************************************************************/ 729 boolean dirtyLayout = false; 730 void setDirtyLayout(Parent p) { 731 if (!dirtyLayout && p != null && p.getSubScene() == this && 732 this.getScene() != null) { 733 dirtyLayout = true; 734 markDirtyLayoutBranch(); 735 markDirty(SubSceneDirtyBits.CONTENT_DIRTY); 736 } 737 } 738 739 private boolean dirtyNodes = false; 740 void setDirty(Node n) { 741 if (!dirtyNodes && n != null && n.getSubScene() == this && 742 this.getScene() != null) { 743 dirtyNodes = true; 744 markDirty(SubSceneDirtyBits.CONTENT_DIRTY); 745 } 746 } 747 748 void layoutPass() { 749 if (dirtyLayout) { 750 Parent r = getRoot(); 751 if (r != null) { 752 r.layout(); 753 } 754 dirtyLayout = false; 755 } 756 } 757 758 private TopMostTraversalEngine traversalEngine = new SubSceneTraversalEngine(this); 759 760 boolean traverse(Node node, Direction dir) { 761 return traversalEngine.trav(node, dir) != null; 762 } 763 764 private enum SubSceneDirtyBits { 765 SIZE_DIRTY, 766 FILL_DIRTY, 767 ROOT_SG_DIRTY, 768 CAMERA_DIRTY, 769 LIGHTS_DIRTY, 770 CONTENT_DIRTY; 771 772 private int mask; 773 774 private SubSceneDirtyBits() { mask = 1 << ordinal(); } 775 776 public final int getMask() { return mask; } 777 } 778 779 private int dirtyBits = ~0; 780 781 private void clearDirtyBits() { dirtyBits = 0; } 782 783 private boolean isDirty() { return dirtyBits != 0; } 784 785 // Should not be called directly, instead use markDirty 786 private void setDirty(SubSceneDirtyBits dirtyBit) { 787 this.dirtyBits |= dirtyBit.getMask(); 788 } 789 790 private boolean isDirty(SubSceneDirtyBits dirtyBit) { 791 return ((this.dirtyBits & dirtyBit.getMask()) != 0); 792 } 793 794 private void markDirty(SubSceneDirtyBits dirtyBit) { 795 if (!isDirty()) { 796 // Force SubScene to redraw 797 NodeHelper.markDirty(this, DirtyBits.NODE_CONTENTS); 798 } 799 setDirty(dirtyBit); 800 } 801 802 /*********************************************************************** 803 * Picking * 804 **********************************************************************/ 805 806 /* 807 * Note: This method MUST only be called via its accessor method. 808 */ 809 private boolean doComputeContains(double localX, double localY) { 810 if (subSceneComputeContains(localX, localY)) { 811 return true; 812 } else { 813 return NodeHelper.computeContains(getRoot(), localX, localY); 814 } 815 } 816 817 /** 818 * Determines whether {@code SubScene} contains the given point. 819 * It does not consider the contained nodes, only {@code SubScene}'s 820 * size and fills. 821 * @param localX horizontal coordinate in the local space of the {@code SubScene} node 822 * @param localY vertical coordinate in the local space of the {@code SubScene} node 823 * @return true if the point is inside {@code SubScene}'s area covered by its fill 824 */ 825 private boolean subSceneComputeContains(double localX, double localY) { 826 if (localX < 0 || localY < 0 || localX > getWidth() || localY > getHeight()) { 827 return false; 828 } 829 return getFill() != null; 830 } 831 832 /* 833 * Generates a pick ray based on local coordinates and camera. Then finds a 834 * top-most child node that intersects the pick ray. 835 */ 836 private PickResult pickRootSG(double localX, double localY) { 837 final double viewWidth = getWidth(); 838 final double viewHeight = getHeight(); 839 if (localX < 0 || localY < 0 || localX > viewWidth || localY > viewHeight) { 840 return null; 841 } 842 final PickResultChooser result = new PickResultChooser(); 843 final PickRay pickRay = getEffectiveCamera().computePickRay(localX, localY, new PickRay()); 844 pickRay.getDirectionNoClone().normalize(); 845 getRoot().pickNode(pickRay, result); 846 return result.toPickResult(); 847 } 848 849 /** 850 * Finds a top-most child node that contains the given local coordinates. 851 * 852 * Returns the picked node, null if no such node was found. 853 * 854 * Note: This method MUST only be called via its accessor method. 855 */ 856 private void doPickNodeLocal(PickRay localPickRay, PickResultChooser result) { 857 final double boundsDistance = intersectsBounds(localPickRay); 858 if (!Double.isNaN(boundsDistance) && result.isCloser(boundsDistance)) { 859 final Point3D intersectPt = PickResultChooser.computePoint( 860 localPickRay, boundsDistance); 861 final PickResult subSceneResult = 862 pickRootSG(intersectPt.getX(), intersectPt.getY()); 863 if (subSceneResult != null) { 864 result.offerSubScenePickResult(this, subSceneResult, boundsDistance); 865 } else if (isPickOnBounds() || 866 subSceneComputeContains(intersectPt.getX(), intersectPt.getY())) { 867 result.offer(this, boundsDistance, intersectPt); 868 } 869 } 870 } 871 872 private List<LightBase> lights = new ArrayList<>(); 873 874 // @param light must not be null 875 final void addLight(LightBase light) { 876 if (!lights.contains(light)) { 877 markDirty(SubSceneDirtyBits.LIGHTS_DIRTY); 878 lights.add(light); 879 } 880 } 881 882 final void removeLight(LightBase light) { 883 if (lights.remove(light)) { 884 markDirty(SubSceneDirtyBits.LIGHTS_DIRTY); 885 } 886 } 887 888 /** 889 * PG Light synchronizer. 890 */ 891 private boolean syncLights() { 892 boolean lightOwnerChanged = false; 893 if (!isDirty(SubSceneDirtyBits.LIGHTS_DIRTY)) { 894 return lightOwnerChanged; 895 } 896 NGSubScene pgSubScene = getPeer(); 897 NGLightBase peerLights[] = pgSubScene.getLights(); 898 if (!lights.isEmpty() || (peerLights != null)) { 899 if (lights.isEmpty()) { 900 pgSubScene.setLights(null); 901 } else { 902 if (peerLights == null || peerLights.length < lights.size()) { 903 peerLights = new NGLightBase[lights.size()]; 904 } 905 int i = 0; 906 for (; i < lights.size(); i++) { 907 peerLights[i] = lights.get(i).getPeer(); 908 } 909 // Clear the rest of the list 910 while (i < peerLights.length && peerLights[i] != null) { 911 peerLights[i++] = null; 912 } 913 pgSubScene.setLights(peerLights); 914 } 915 lightOwnerChanged = true; 916 } 917 return lightOwnerChanged; 918 } 919 920 }