1 /*
   2  * Copyright (c) 2013, 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 javafx.application.ConditionalFeature;
  29 import javafx.application.Platform;
  30 import javafx.beans.NamedArg;
  31 import javafx.beans.property.DoubleProperty;
  32 import javafx.beans.property.DoublePropertyBase;
  33 import javafx.beans.property.ObjectProperty;
  34 import javafx.beans.property.ObjectPropertyBase;
  35 import javafx.beans.value.WritableValue;
  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.SubSceneHelper;
  52 import com.sun.javafx.scene.input.PickResultChooser;
  53 import com.sun.javafx.scene.traversal.TraversalEngine;
  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  *
  65  * @since JavaFX 8.0
  66  */
  67 public class SubScene extends Node {
  68 
  69     /**
  70      * Creates a SubScene for a specific root Node with a specific size.
  71      *
  72      * @param root The root node of the scene graph
  73      * @param width The width of the scene
  74      * @param height The height of the scene
  75      *
  76      * @throws IllegalStateException if this constructor is called on a thread
  77      * other than the JavaFX Application Thread.
  78      * @throws NullPointerException if root is null
  79      */
  80     public SubScene(@NamedArg("root") Parent root, @NamedArg("width") double width, @NamedArg("height") double height) {
  81         this(root, width, height, false, SceneAntialiasing.DISABLED);
  82     }
  83 
  84     /**
  85      * Constructs a SubScene consisting of a root, with a dimension of width and
  86      * height, specifies whether a depth buffer is created for this scene and
  87      * specifies whether scene anti-aliasing is requested.
  88      *
  89      * @param root The root node of the scene graph
  90      * @param width The width of the scene
  91      * @param height The height of the scene
  92      * @param depthBuffer The depth buffer flag
  93      * @param antiAliasing The sub-scene anti-aliasing attribute. A value of
  94      * {@code null} is treated as DISABLED.
  95      * <p>
  96      * The depthBuffer and antiAliasing flags are conditional features. With the
  97      * respective default values of: false and {@code SceneAntialiasing.DISABLED}.
  98      * See {@link javafx.application.ConditionalFeature#SCENE3D ConditionalFeature.SCENE3D}
  99      * for more information.
 100      *
 101      * @throws IllegalStateException if this constructor is called on a thread
 102      * other than the JavaFX Application Thread.
 103      * @throws NullPointerException if root is null
 104      *
 105      * @see javafx.scene.Node#setDepthTest(DepthTest)
 106      */
 107     public SubScene(@NamedArg("root") Parent root, @NamedArg("width") double width, @NamedArg("height") double height,
 108             @NamedArg("depthBuffer") boolean depthBuffer, @NamedArg("antiAliasing") SceneAntialiasing antiAliasing)
 109     {
 110         this.depthBuffer = depthBuffer;
 111         this.antiAliasing = antiAliasing;
 112         boolean isAntiAliasing = !(antiAliasing == null || antiAliasing == SceneAntialiasing.DISABLED);
 113         setRoot(root);
 114         setWidth(width);
 115         setHeight(height);
 116 
 117         if ((depthBuffer || isAntiAliasing) && !is3DSupported) {
 118             String logname = SubScene.class.getName();
 119             PlatformLogger.getLogger(logname).warning("System can't support "
 120                     + "ConditionalFeature.SCENE3D");
 121         }
 122         if (isAntiAliasing && !Toolkit.getToolkit().isAntiAliasingSupported()) {
 123             String logname = SubScene.class.getName();
 124             PlatformLogger.getLogger(logname).warning("System can't support "
 125                     + "antiAliasing");
 126         }
 127     }
 128 
 129     private static boolean is3DSupported =
 130             Platform.isSupported(ConditionalFeature.SCENE3D);
 131 
 132     private final SceneAntialiasing antiAliasing;
 133 
 134     /**
 135      * Return the defined {@code SceneAntialiasing} for this {@code SubScene}.
 136      * <p>
 137      * Note: this is a conditional feature. See
 138      * {@link javafx.application.ConditionalFeature#SCENE3D ConditionalFeature.SCENE3D}
 139      * and {@link javafx.scene.SceneAntialiasing SceneAntialiasing}
 140      * for more information.
 141      * @since JavaFX 8.0
 142      */
 143     public final SceneAntialiasing getAntiAliasing() {
 144         return antiAliasing;
 145     }
 146 
 147     private final boolean depthBuffer;
 148 
 149     /**
 150      * Retrieves the depth buffer attribute for this SubScene.
 151      * @return the depth buffer attribute.
 152      */
 153     public final boolean isDepthBuffer() {
 154         return depthBuffer;
 155     }
 156 
 157     private boolean isDepthBufferInternal() {
 158         return is3DSupported ? depthBuffer : false;
 159     }
 160 
 161     /**
 162      * Defines the root {@code Node} of the SubScene scene graph.
 163      * If a {@code Group} is used as the root, the
 164      * contents of the scene graph will be clipped by the SubScene's width and height.
 165      *
 166      * SubScene doesn't accept null root.
 167      *
 168      */
 169     private ObjectProperty<Parent> root;
 170 
 171     public final void setRoot(Parent value) {
 172         rootProperty().set(value);
 173     }
 174 
 175     public final Parent getRoot() {
 176         return root == null ? null : root.get();
 177     }
 178 
 179     public final ObjectProperty<Parent> rootProperty() {
 180         if (root == null) {
 181             root = new ObjectPropertyBase<Parent>() {
 182                 private Parent oldRoot;
 183 
 184                 private void forceUnbind() {
 185                     System.err.println("Unbinding illegal root.");
 186                     unbind();
 187                 }
 188 
 189                 @Override
 190                 protected void invalidated() {
 191                     Parent _value = get();
 192 
 193                     if (_value == null) {
 194                         if (isBound()) { forceUnbind(); }
 195                         throw new NullPointerException("Scene's root cannot be null");
 196                     }
 197                     if (_value.getParent() != null) {
 198                         if (isBound()) { forceUnbind(); }
 199                         throw new IllegalArgumentException(_value +
 200                                 "is already inside a scene-graph and cannot be set as root");
 201                     }
 202                     if (_value.getClipParent() != null) {
 203                         if (isBound()) forceUnbind();
 204                         throw new IllegalArgumentException(_value +
 205                                 "is set as a clip on another node, so cannot be set as root");
 206                     }
 207                     if ((_value.getScene() != null &&
 208                             _value.getScene().getRoot() == _value) ||
 209                             (_value.getSubScene() != null &&
 210                             _value.getSubScene().getRoot() == _value &&
 211                             _value.getSubScene() != SubScene.this))
 212                     {
 213                         if (isBound()) { forceUnbind(); }
 214                         throw new IllegalArgumentException(_value +
 215                                 "is already set as root of another scene or subScene");
 216                     }
 217 
 218                     // disabled and isTreeVisible properties are inherrited
 219                     _value.setTreeVisible(impl_isTreeVisible());
 220                     _value.setDisabled(isDisabled());
 221 
 222                     if (oldRoot != null) {
 223                         oldRoot.setScenes(null, null);
 224                         oldRoot.setImpl_traversalEngine(null);
 225                     }
 226                     oldRoot = _value;
 227                     if (_value.getImpl_traversalEngine() == null) {
 228                         _value.setImpl_traversalEngine(new TraversalEngine(_value, true));
 229                     }
 230                     _value.getStyleClass().add(0, "root");
 231                     _value.setScenes(getScene(), SubScene.this);
 232                     markDirty(SubSceneDirtyBits.ROOT_SG_DIRTY);
 233                     _value.resize(getWidth(), getHeight()); // maybe no-op if root is not resizable
 234                     _value.requestLayout();
 235                 }
 236 
 237                 @Override
 238                 public Object getBean() {
 239                     return SubScene.this;
 240                 }
 241 
 242                 @Override
 243                 public String getName() {
 244                     return "root";
 245                 }
 246             };
 247         }
 248         return root;
 249     }
 250 
 251     /**
 252      * Specifies the type of camera use for rendering this {@code SubScene}.
 253      * If {@code camera} is null, a parallel camera is used for rendering.
 254      * It is illegal to set a camera that belongs to other {@code Scene}
 255      * or {@code SubScene}.
 256      * <p>
 257      * Note: this is a conditional feature. See
 258      * {@link javafx.application.ConditionalFeature#SCENE3D ConditionalFeature.SCENE3D}
 259      * for more information.
 260      *
 261      * @defaultValue null
 262      */
 263     private ObjectProperty<Camera> camera;
 264 
 265     public final void setCamera(Camera value) {
 266         cameraProperty().set(value);
 267     }
 268 
 269     public final Camera getCamera() {
 270         return camera == null ? null : camera.get();
 271     }
 272 
 273     public final ObjectProperty<Camera> cameraProperty() {
 274         if (camera == null) {
 275             camera = new ObjectPropertyBase<Camera>() {
 276                 Camera oldCamera = null;
 277 
 278                 @Override
 279                 protected void invalidated() {
 280                     Camera _value = get();
 281                     if (_value != null) {
 282                         if (_value instanceof PerspectiveCamera
 283                                 && !SubScene.is3DSupported) {
 284                             String logname = SubScene.class.getName();
 285                             PlatformLogger.getLogger(logname).warning("System can't support "
 286                                     + "ConditionalFeature.SCENE3D");
 287                         }
 288                         // Illegal value if it belongs to any scene or other subscene
 289                         if ((_value.getScene() != null || _value.getSubScene() != null)
 290                                 && (_value.getScene() != getScene() || _value.getSubScene() != SubScene.this)) {
 291                             throw new IllegalArgumentException(_value
 292                                     + "is already part of other scene or subscene");
 293                         }
 294                         // throws exception if the camera already has a different owner
 295                         _value.setOwnerSubScene(SubScene.this);
 296                         _value.setViewWidth(getWidth());
 297                         _value.setViewHeight(getHeight());
 298                     }
 299                     markDirty(SubSceneDirtyBits.CAMERA_DIRTY);
 300                     if (oldCamera != null && oldCamera != _value) {
 301                         oldCamera.setOwnerSubScene(null);
 302                     }
 303                     oldCamera = _value;
 304                 }
 305 
 306                 @Override
 307                 public Object getBean() {
 308                     return SubScene.this;
 309                 }
 310 
 311                 @Override
 312                 public String getName() {
 313                     return "camera";
 314                 }
 315             };
 316         }
 317         return camera;
 318     }
 319 
 320     private Camera defaultCamera;
 321 
 322     Camera getEffectiveCamera() {
 323         final Camera cam = getCamera();
 324         if (cam == null
 325                 || (cam instanceof PerspectiveCamera && !is3DSupported)) {
 326             if (defaultCamera == null) {
 327                 defaultCamera = new ParallelCamera();
 328                 defaultCamera.setOwnerSubScene(this);
 329                 defaultCamera.setViewWidth(getWidth());
 330                 defaultCamera.setViewHeight(getHeight());
 331             }
 332             return defaultCamera;
 333         }
 334 
 335         return cam;
 336     }
 337 
 338     // Used by the camera
 339     final void markContentDirty() {
 340         markDirty(SubSceneDirtyBits.CONTENT_DIRTY);
 341     }
 342 
 343     /**
 344      * Defines the width of this {@code SubScene}
 345      *
 346      * @defaultvalue 0.0
 347      */
 348     private DoubleProperty width;
 349 
 350     public final void setWidth(double value) {
 351         widthProperty().set(value);
 352     }
 353 
 354     public final double getWidth() {
 355         return width == null ? 0.0 : width.get();
 356     }
 357 
 358     public final DoubleProperty widthProperty() {
 359         if (width == null) {
 360             width = new DoublePropertyBase() {
 361 
 362                 @Override
 363                 public void invalidated() {
 364                     final Parent _root = getRoot();
 365                     //TODO - use a better method to update mirroring
 366                     if (_root.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) {
 367                         _root.impl_transformsChanged();
 368                     }
 369                     if (_root.isResizable()) {
 370                         _root.resize(get() - _root.getLayoutX() - _root.getTranslateX(), _root.getLayoutBounds().getHeight());
 371                     }
 372                     markDirty(SubSceneDirtyBits.SIZE_DIRTY);
 373                     SubScene.this.impl_geomChanged();
 374 
 375                     getEffectiveCamera().setViewWidth(get());
 376                 }
 377 
 378                 @Override
 379                 public Object getBean() {
 380                     return SubScene.this;
 381                 }
 382 
 383                 @Override
 384                 public String getName() {
 385                     return "width";
 386                 }
 387             };
 388         }
 389         return width;
 390     }
 391 
 392     /**
 393      * Defines the height of this {@code SubScene}
 394      *
 395      * @defaultvalue 0.0
 396      */
 397     private DoubleProperty height;
 398 
 399     public final void setHeight(double value) {
 400         heightProperty().set(value);
 401     }
 402 
 403     public final double getHeight() {
 404         return height == null ? 0.0 : height.get();
 405     }
 406 
 407     public final DoubleProperty heightProperty() {
 408         if (height == null) {
 409             height = new DoublePropertyBase() {
 410 
 411                 @Override
 412                 public void invalidated() {
 413                     final Parent _root = getRoot();
 414                     if (_root.isResizable()) {
 415                         _root.resize(_root.getLayoutBounds().getWidth(), get() - _root.getLayoutY() - _root.getTranslateY());
 416                     }
 417                     markDirty(SubSceneDirtyBits.SIZE_DIRTY);
 418                     SubScene.this.impl_geomChanged();
 419 
 420                     getEffectiveCamera().setViewHeight(get());
 421                 }
 422 
 423                 @Override
 424                 public Object getBean() {
 425                     return SubScene.this;
 426                 }
 427 
 428                 @Override
 429                 public String getName() {
 430                     return "height";
 431                 }
 432             };
 433         }
 434         return height;
 435     }
 436 
 437     /**
 438      * Defines the background fill of this {@code SubScene}. Both a {@code null}
 439      * value meaning paint no background and a {@link javafx.scene.paint.Paint}
 440      * with transparency are supported. The default value is null.
 441      *
 442      * @defaultValue null
 443      */
 444     private ObjectProperty<Paint> fill;
 445 
 446     public final void setFill(Paint value) {
 447         fillProperty().set(value);
 448     }
 449 
 450     public final Paint getFill() {
 451         return fill == null ? null : fill.get();
 452     }
 453 
 454     public final ObjectProperty<Paint> fillProperty() {
 455         if (fill == null) {
 456             fill = new ObjectPropertyBase<Paint>(null) {
 457 
 458                 @Override
 459                 protected void invalidated() {
 460                     markDirty(SubSceneDirtyBits.FILL_DIRTY);
 461                 }
 462 
 463                 @Override
 464                 public Object getBean() {
 465                     return SubScene.this;
 466                 }
 467 
 468                 @Override
 469                 public String getName() {
 470                     return "fill";
 471                 }
 472             };
 473         }
 474         return fill;
 475     }
 476 
 477     /**
 478      * @treatAsPrivate implementation detail
 479      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 480      */
 481     @Deprecated @Override
 482     public void impl_updatePeer() {
 483         super.impl_updatePeer();
 484 
 485         // TODO deal with clip node
 486 
 487         dirtyNodes = false;
 488         if (isDirty()) {
 489             NGSubScene peer = impl_getPeer();
 490             final Camera cam = getEffectiveCamera();
 491             boolean contentChanged = false;
 492             if (cam.getSubScene() == null &&
 493                     isDirty(SubSceneDirtyBits.CONTENT_DIRTY)) {
 494                 // When camera is not a part of the graph, then its
 495                 // owner(subscene) must take care of syncing it. And when a
 496                 // property on the camera changes it will mark subscenes
 497                 // CONTENT_DIRTY.
 498                 cam.impl_syncPeer();
 499             }
 500             if (isDirty(SubSceneDirtyBits.FILL_DIRTY)) {
 501                 Object platformPaint = getFill() == null ? null :
 502                         Toolkit.getPaintAccessor().getPlatformPaint(getFill());
 503                 peer.setFillPaint(platformPaint);
 504                 contentChanged = true;
 505             }
 506             if (isDirty(SubSceneDirtyBits.SIZE_DIRTY)) {
 507                 // Note change in size is a geom change and is handled by peer
 508                 peer.setWidth((float)getWidth());
 509                 peer.setHeight((float)getHeight());
 510             }
 511             if (isDirty(SubSceneDirtyBits.CAMERA_DIRTY)) {
 512                 peer.setCamera((NGCamera) cam.impl_getPeer());
 513                 contentChanged = true;
 514             }
 515             if (isDirty(SubSceneDirtyBits.ROOT_SG_DIRTY)) {
 516                 peer.setRoot(getRoot().impl_getPeer());
 517                 contentChanged = true;
 518             }
 519             contentChanged |= syncLights();
 520             if (contentChanged || isDirty(SubSceneDirtyBits.CONTENT_DIRTY)) {
 521                 peer.markContentDirty();
 522             }
 523 
 524             clearDirtyBits();
 525         }
 526 
 527     }
 528 
 529     @Override
 530     void nodeResolvedOrientationChanged() {
 531         getRoot().parentResolvedOrientationInvalidated();
 532     }
 533 
 534     /***********************************************************************
 535      *                         CSS                                         *
 536      **********************************************************************/
 537     /**
 538      * @treatAsPrivate implementation detail
 539      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 540      */
 541     @Deprecated @Override
 542     protected void impl_processCSS(WritableValue<Boolean> cacheHint) {
 543         // Nothing to do...
 544         if (cssFlag == CssFlags.CLEAN) { return; }
 545 
 546         if (getRoot().cssFlag == CssFlags.CLEAN) {
 547             getRoot().cssFlag = cssFlag;
 548         }
 549         super.impl_processCSS(cacheHint);
 550         getRoot().processCSS(cacheHint);
 551     }
 552 
 553     @Override
 554     void processCSS(WritableValue<Boolean> cacheHint) {
 555         Parent root = getRoot();
 556         if (root.impl_isDirty(DirtyBits.NODE_CSS)) {
 557             root.impl_clearDirty(DirtyBits.NODE_CSS);
 558             if (cssFlag == CssFlags.CLEAN) { cssFlag = CssFlags.UPDATE; }
 559         }
 560         super.processCSS(cacheHint);
 561     }
 562 
 563     @Override void updateBounds() {
 564         super.updateBounds();
 565         getRoot().updateBounds();
 566     }
 567 
 568     /**
 569      * @treatAsPrivate implementation detail
 570      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 571      */
 572     @Deprecated    @Override
 573     protected NGNode impl_createPeer() {
 574         if (!is3DSupported) {
 575             return new NGSubScene(false, false);
 576         }
 577         boolean aa = !(antiAliasing == null || antiAliasing == SceneAntialiasing.DISABLED);
 578         return new NGSubScene(depthBuffer, aa && Toolkit.getToolkit().isAntiAliasingSupported());
 579     }
 580 
 581     /**
 582      * @treatAsPrivate implementation detail
 583      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 584      */
 585     @Deprecated    @Override
 586     public BaseBounds impl_computeGeomBounds(BaseBounds bounds, BaseTransform tx) {
 587         int w = (int)Math.ceil(width.get());
 588         int h = (int)Math.ceil(height.get());
 589         bounds = bounds.deriveWithNewBounds(0.0f, 0.0f, 0.0f,
 590                                             w, h, 0.0f);
 591         bounds = tx.transform(bounds, bounds);
 592         return bounds;
 593     }
 594 
 595     /***********************************************************************
 596      *                         Dirty Bits                                  *
 597      **********************************************************************/
 598     boolean dirtyLayout = false;
 599     void setDirtyLayout(Parent p) {
 600         if (!dirtyLayout && p != null && p.getSubScene() == this &&
 601                 this.getScene() != null) {
 602             dirtyLayout = true;
 603             markDirtyLayoutBranch();
 604             markDirty(SubSceneDirtyBits.CONTENT_DIRTY);
 605         }
 606     }
 607 
 608     private boolean dirtyNodes = false;
 609     void setDirty(Node n) {
 610         if (!dirtyNodes && n != null && n.getSubScene() == this &&
 611                 this.getScene() != null) {
 612             dirtyNodes = true;
 613             markDirty(SubSceneDirtyBits.CONTENT_DIRTY);
 614         }
 615     }
 616 
 617     void layoutPass() {
 618         if (dirtyLayout) {
 619             Parent r = getRoot();
 620             if (r != null) {
 621                 r.layout();
 622             }
 623             dirtyLayout = false;
 624         }
 625     }
 626 
 627     private enum SubSceneDirtyBits {
 628         SIZE_DIRTY,
 629         FILL_DIRTY,
 630         ROOT_SG_DIRTY,
 631         CAMERA_DIRTY,
 632         LIGHTS_DIRTY,
 633         CONTENT_DIRTY;
 634 
 635         private int mask;
 636 
 637         private SubSceneDirtyBits() { mask = 1 << ordinal(); }
 638 
 639         public final int getMask() { return mask; }
 640     }
 641 
 642     private int dirtyBits = ~0;
 643 
 644     private void clearDirtyBits() { dirtyBits = 0; }
 645 
 646     private boolean isDirty() { return dirtyBits != 0; }
 647 
 648     // Should not be called directly, instead use markDirty
 649     private void setDirty(SubSceneDirtyBits dirtyBit) {
 650         this.dirtyBits |= dirtyBit.getMask();
 651     }
 652 
 653     private boolean isDirty(SubSceneDirtyBits dirtyBit) {
 654         return ((this.dirtyBits & dirtyBit.getMask()) != 0);
 655     }
 656 
 657     private void markDirty(SubSceneDirtyBits dirtyBit) {
 658         if (!isDirty()) {
 659             // Force SubScene to redraw
 660             impl_markDirty(DirtyBits.NODE_CONTENTS);
 661         }
 662         setDirty(dirtyBit);
 663     }
 664 
 665     /***********************************************************************
 666      *                           Picking                                   *
 667      **********************************************************************/
 668 
 669     /**
 670      * @treatAsPrivate implementation detail
 671      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 672      */
 673     @Deprecated    @Override
 674     protected boolean impl_computeContains(double localX, double localY) {
 675         if (subSceneComputeContains(localX, localY)) {
 676             return true;
 677         } else {
 678             return getRoot().impl_computeContains(localX, localY);
 679         }
 680     }
 681 
 682     /**
 683      * Determines whether subScene contains the given point.
 684      * It does not consider the contained nodes, only subScene's
 685      * size and fills.
 686      * @param localX horizontal coordinate in the local space of the subScene node
 687      * @param localY vertical coordinate in the local space of the subScene node
 688      * @return true if the point is inside subScene's area covered by its fill
 689      */
 690     private boolean subSceneComputeContains(double localX, double localY) {
 691         if (localX < 0 || localY < 0 || localX > getWidth() || localY > getHeight()) {
 692             return false;
 693         }
 694         return getFill() != null;
 695     }
 696 
 697     /*
 698      * Generates a pick ray based on local coordinates and camera. Then finds a
 699      * top-most child node that intersects the pick ray.
 700      */
 701     private PickResult pickRootSG(double localX, double localY) {
 702         final double viewWidth = getWidth();
 703         final double viewHeight = getHeight();
 704         if (localX < 0 || localY < 0 || localX > viewWidth || localY > viewHeight) {
 705             return null;
 706         }
 707         final PickResultChooser result = new PickResultChooser();
 708         final PickRay pickRay = getEffectiveCamera().computePickRay(localX, localY, new PickRay());
 709         pickRay.getDirectionNoClone().normalize();
 710         getRoot().impl_pickNode(pickRay, result);
 711         return result.toPickResult();
 712     }
 713 
 714     /**
 715      * Finds a top-most child node that contains the given local coordinates.
 716      *
 717      * Returns the picked node, null if no such node was found.
 718      * @treatAsPrivate implementation detail
 719      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 720      */
 721     @Deprecated @Override
 722     protected void impl_pickNodeLocal(PickRay localPickRay, PickResultChooser result) {
 723         final double boundsDistance = impl_intersectsBounds(localPickRay);
 724         if (!Double.isNaN(boundsDistance) && result.isCloser(boundsDistance)) {
 725             final Point3D intersectPt = PickResultChooser.computePoint(
 726                     localPickRay, boundsDistance);
 727             final PickResult subSceneResult =
 728                     pickRootSG(intersectPt.getX(), intersectPt.getY());
 729             if (subSceneResult != null) {
 730                 result.offerSubScenePickResult(this, subSceneResult, boundsDistance);
 731             } else if (isPickOnBounds() ||
 732                     subSceneComputeContains(intersectPt.getX(), intersectPt.getY())) {
 733                 result.offer(this, boundsDistance, intersectPt);
 734             }
 735         }
 736     }
 737 
 738     /**
 739      * @treatAsPrivate implementation detail
 740      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
 741      */
 742     @Deprecated    @Override
 743     public Object impl_processMXNode(MXNodeAlgorithm alg, MXNodeAlgorithmContext ctx) {
 744         throw new UnsupportedOperationException("Not supported yet.");
 745     }
 746 
 747 
 748     private List<LightBase> lights = new ArrayList<>();
 749 
 750     // @param light must not be null
 751     final void addLight(LightBase light) {
 752         if (!lights.contains(light)) {
 753             markDirty(SubSceneDirtyBits.LIGHTS_DIRTY);
 754             lights.add(light);
 755         }
 756     }
 757 
 758     final void removeLight(LightBase light) {
 759         if (lights.remove(light)) {
 760             markDirty(SubSceneDirtyBits.LIGHTS_DIRTY);
 761         }
 762     }
 763 
 764     /**
 765      * PG Light synchronizer.
 766      */
 767     private boolean syncLights() {
 768         boolean lightOwnerChanged = false;
 769         if (!isDirty(SubSceneDirtyBits.LIGHTS_DIRTY)) {
 770             return lightOwnerChanged;
 771         }
 772         NGSubScene pgSubScene = impl_getPeer();
 773         NGLightBase peerLights[] = pgSubScene.getLights();
 774         if (!lights.isEmpty() || (peerLights != null)) {
 775             if (lights.isEmpty()) {
 776                 pgSubScene.setLights(null);
 777             } else {
 778                 if (peerLights == null || peerLights.length < lights.size()) {
 779                     peerLights = new NGLightBase[lights.size()];
 780                 }
 781                 int i = 0;
 782                 for (; i < lights.size(); i++) {
 783                     peerLights[i] = lights.get(i).impl_getPeer();
 784                 }
 785                 // Clear the rest of the list
 786                 while (i < peerLights.length && peerLights[i] != null) {
 787                     peerLights[i++] = null;
 788                 }
 789                 pgSubScene.setLights(peerLights);
 790             }
 791             lightOwnerChanged = true;
 792         }
 793         return lightOwnerChanged;
 794     }
 795 
 796     static {
 797         // This is used by classes in different packages to get access to
 798         // private and package private methods.
 799         SubSceneHelper.setSubSceneAccessor(new SubSceneHelper.SubSceneAccessor() {
 800 
 801             @Override
 802             public boolean isDepthBuffer(SubScene subScene) {
 803                 return subScene.isDepthBufferInternal();
 804             };
 805 
 806             @Override
 807             public Camera getEffectiveCamera(SubScene subScene) {
 808                 return subScene.getEffectiveCamera();
 809             }
 810         });
 811     }
 812 }