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