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