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 }