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 }