1 /*
   2  * Copyright (c) 2013, 2018, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene;
  27 
  28 import com.sun.javafx.collections.TrackableObservableList;
  29 import com.sun.javafx.geom.BaseBounds;
  30 import com.sun.javafx.geom.BoxBounds;
  31 import com.sun.javafx.geom.transform.Affine3D;
  32 import com.sun.javafx.geom.transform.BaseTransform;
  33 import com.sun.javafx.scene.DirtyBits;
  34 import com.sun.javafx.scene.LightBaseHelper;
  35 import com.sun.javafx.scene.NodeHelper;
  36 import com.sun.javafx.scene.transform.TransformHelper;
  37 import com.sun.javafx.sg.prism.NGLightBase;
  38 import com.sun.javafx.tk.Toolkit;
  39 import javafx.application.ConditionalFeature;
  40 import javafx.application.Platform;
  41 import javafx.beans.InvalidationListener;
  42 import javafx.beans.Observable;
  43 import javafx.beans.property.BooleanProperty;
  44 import javafx.beans.property.ObjectProperty;
  45 import javafx.beans.property.SimpleBooleanProperty;
  46 import javafx.beans.property.SimpleObjectProperty;
  47 import javafx.collections.ListChangeListener.Change;
  48 import javafx.collections.ObservableList;
  49 import javafx.scene.paint.Color;
  50 import javafx.scene.shape.Shape3D;
  51 import com.sun.javafx.logging.PlatformLogger;
  52 
  53 /**
  54  * The {@code LightBase} class provides definitions of common properties for
  55  * objects that represent a form of Light source.  These properties
  56  * include:
  57  * <ul>
  58  * <li>The color that defines color of the light source.
  59  * </ul>
  60  *
  61  * Note that this is a conditional feature. See
  62  * {@link javafx.application.ConditionalFeature#SCENE3D ConditionalFeature.SCENE3D}
  63  * for more information.
  64  *
  65  * <p>
  66  * An application should not extend the LightBase class directly. Doing so may lead to
  67  * an UnsupportedOperationException being thrown.
  68  * </p>
  69  *
  70  * @since JavaFX 8.0
  71  */
  72 public abstract class LightBase extends Node {
  73     static {
  74          // This is used by classes in different packages to get access to
  75          // private and package private methods.
  76         LightBaseHelper.setLightBaseAccessor(new LightBaseHelper.LightBaseAccessor() {
  77             @Override
  78             public void doMarkDirty(Node node, DirtyBits dirtyBit) {
  79                 ((LightBase) node).doMarkDirty(dirtyBit);
  80             }
  81 
  82             @Override
  83             public void doUpdatePeer(Node node) {
  84                 ((LightBase) node).doUpdatePeer();
  85             }
  86 
  87             @Override
  88             public BaseBounds doComputeGeomBounds(Node node,
  89                     BaseBounds bounds, BaseTransform tx) {
  90                 return ((LightBase) node).doComputeGeomBounds(bounds, tx);
  91             }
  92 
  93             @Override
  94             public boolean doComputeContains(Node node, double localX, double localY) {
  95                 return ((LightBase) node).doComputeContains(localX, localY);
  96             }
  97         });
  98     }
  99 
 100     private Affine3D localToSceneTx = new Affine3D();
 101 
 102     {
 103         // To initialize the class helper at the begining each constructor of this class
 104         LightBaseHelper.initHelper(this);
 105     }
 106 
 107     /**
 108      * Creates a new instance of {@code LightBase} class with a default Color.WHITE light source.
 109      */
 110     protected LightBase() {
 111         this(Color.WHITE);
 112     }
 113 
 114     /**
 115      * Creates a new instance of {@code LightBase} class using the specified color.
 116      *
 117      * @param color the color of the light source
 118      */
 119     protected LightBase(Color color) {
 120         if (!Platform.isSupported(ConditionalFeature.SCENE3D)) {
 121             String logname = LightBase.class.getName();
 122             PlatformLogger.getLogger(logname).warning("System can't support "
 123                                                       + "ConditionalFeature.SCENE3D");
 124         }
 125 
 126         setColor(color);
 127         this.localToSceneTransformProperty().addListener(observable ->
 128                 NodeHelper.markDirty(this, DirtyBits.NODE_LIGHT_TRANSFORM));
 129     }
 130 
 131     /**
 132      * Specifies the color of light source.
 133      *
 134      * @defaultValue null
 135      */
 136     private ObjectProperty<Color> color;
 137 
 138     public final void setColor(Color value) {
 139         colorProperty().set(value);
 140     }
 141 
 142     public final Color getColor() {
 143         return color == null ? null : color.get();
 144     }
 145 
 146     public final ObjectProperty<Color> colorProperty() {
 147         if (color == null) {
 148             color = new SimpleObjectProperty<Color>(LightBase.this, "color") {
 149                 @Override
 150                 protected void invalidated() {
 151                     NodeHelper.markDirty(LightBase.this, DirtyBits.NODE_LIGHT);
 152                 }
 153             };
 154         }
 155         return color;
 156     }
 157 
 158     /**
 159      * Defines the light on or off.
 160      *
 161      * @defaultValue true
 162      */
 163     private BooleanProperty lightOn;
 164 
 165     public final void setLightOn(boolean value) {
 166         lightOnProperty().set(value);
 167     }
 168 
 169     public final boolean isLightOn() {
 170         return lightOn == null ? true : lightOn.get();
 171     }
 172 
 173     public final BooleanProperty lightOnProperty() {
 174         if (lightOn == null) {
 175             lightOn = new SimpleBooleanProperty(LightBase.this, "lightOn", true) {
 176                 @Override
 177                 protected void invalidated() {
 178                     NodeHelper.markDirty(LightBase.this, DirtyBits.NODE_LIGHT);
 179                 }
 180             };
 181         }
 182         return lightOn;
 183     }
 184 
 185     private ObservableList<Node> scope;
 186 
 187     /**
 188      * Gets the list of nodes that specifies the
 189      * hierarchical scope of this Light. If the scope list is empty,
 190      * the Light node has universe scope: all nodes under it's scene
 191      * are affected by it. If the scope list is non-empty, only those
 192      * 3D Shape nodes in the scope list and under the Group nodes in the
 193      * scope list are affected by this Light node.
 194      * @return the list of nodes that specifies the hierarchical scope of this
 195      * Light
 196      */
 197     public ObservableList<Node> getScope() {
 198         if (scope == null) {
 199             scope = new TrackableObservableList<Node>() {
 200 
 201                 @Override
 202                 protected void onChanged(Change<Node> c) {
 203                     NodeHelper.markDirty(LightBase.this, DirtyBits.NODE_LIGHT_SCOPE);
 204                     while (c.next()) {
 205                         for (Node node : c.getRemoved()) {
 206                             // Update the removed nodes
 207                             if (node instanceof Parent || node instanceof Shape3D) {
 208                                 markChildrenDirty(node);
 209                             }
 210                         }
 211                         for (Node node : c.getAddedSubList()) {
 212                             if (node instanceof Parent || node instanceof Shape3D) {
 213                                 markChildrenDirty(node);
 214                             }
 215                         }
 216                     }
 217                 }
 218             };
 219         }
 220         return scope;
 221     }
 222 
 223     @Override
 224     void scenesChanged(final Scene newScene, final SubScene newSubScene,
 225                        final Scene oldScene, final SubScene oldSubScene) {
 226         // This light is owned by the Scene/SubScene, and thus must change
 227         // accordingly. Note lights can owned by either a Scene or SubScene,
 228         // but not both.
 229         if (oldSubScene != null) {
 230             oldSubScene.removeLight(this);
 231         } else if (oldScene != null) {
 232             oldScene.removeLight(this);
 233         }
 234         if (newSubScene != null) {
 235             newSubScene.addLight(this);
 236         } else if (newScene != null) {
 237             newScene.addLight(this);
 238         }
 239     }
 240 
 241     private void markOwnerDirty() {
 242         // if the light is part of the scene/subScene, we will need to notify
 243         // the owner to mark the entire scene/subScene dirty.
 244         SubScene subScene = getSubScene();
 245         if (subScene != null) {
 246             subScene.markContentDirty();
 247         } else {
 248             Scene scene = getScene();
 249             if (scene != null) {
 250                 scene.setNeedsRepaint();
 251             }
 252         }
 253     }
 254 
 255     private void markChildrenDirty(Node node) {
 256         if (node instanceof Shape3D) {
 257             // Dirty using a lightweight DirtyBits.NODE_DRAWMODE bit
 258             NodeHelper.markDirty(((Shape3D) node), DirtyBits.NODE_DRAWMODE);
 259         } else if (node instanceof Parent) {
 260             for (Node child : ((Parent) node).getChildren()) {
 261                 markChildrenDirty(child);
 262             }
 263         }
 264     }
 265 
 266     /*
 267      * Note: This method MUST only be called via its accessor method.
 268      */
 269     private void doMarkDirty(DirtyBits dirtyBit) {
 270         if ((scope == null) || getScope().isEmpty()) {
 271             // This light affect the entire scene/subScene
 272             markOwnerDirty();
 273         } else if (dirtyBit != DirtyBits.NODE_LIGHT_SCOPE) {
 274             // Skip NODE_LIGHT_SCOPE dirty since it is processed on scope change.
 275             ObservableList<Node> tmpScope = getScope();
 276             for (int i = 0, max = tmpScope.size(); i < max; i++) {
 277                 markChildrenDirty(tmpScope.get(i));
 278             }
 279         }
 280     }
 281 
 282     /*
 283      * Note: This method MUST only be called via its accessor method.
 284      */
 285     private void doUpdatePeer() {
 286         NGLightBase peer = getPeer();
 287         if (isDirty(DirtyBits.NODE_LIGHT)) {
 288             peer.setColor((getColor() == null) ?
 289                     Toolkit.getPaintAccessor().getPlatformPaint(Color.WHITE)
 290                     : Toolkit.getPaintAccessor().getPlatformPaint(getColor()));
 291             peer.setLightOn(isLightOn());
 292         }
 293 
 294         if (isDirty(DirtyBits.NODE_LIGHT_SCOPE)) {
 295             if (scope != null) {
 296                 ObservableList<Node> tmpScope = getScope();
 297                 if (tmpScope.isEmpty()) {
 298                     peer.setScope(null);
 299                 } else {
 300                     Object ngList[] = new Object[tmpScope.size()];
 301                     for (int i = 0; i < tmpScope.size(); i++) {
 302                         Node n = tmpScope.get(i);
 303                         ngList[i] = n.getPeer();
 304                     }
 305                     peer.setScope(ngList);
 306                 }
 307             }
 308         }
 309 
 310         if (isDirty(DirtyBits.NODE_LIGHT_TRANSFORM)) {
 311             localToSceneTx.setToIdentity();
 312             TransformHelper.apply(getLocalToSceneTransform(), localToSceneTx);
 313             // TODO: 3D - For now, we are treating the scene as world. This may need to change
 314             // for the fixed eye position case.
 315             peer.setWorldTransform(localToSceneTx);
 316         }
 317     }
 318 
 319     /*
 320      * Note: This method MUST only be called via its accessor method.
 321      */
 322     private BaseBounds doComputeGeomBounds(BaseBounds bounds, BaseTransform tx) {
 323         // TODO: 3D - Check is this the right default
 324         return new BoxBounds();
 325     }
 326 
 327     /*
 328      * Note: This method MUST only be called via its accessor method.
 329      */
 330     private boolean doComputeContains(double localX, double localY) {
 331         // TODO: 3D - Check is this the right default
 332         return false;
 333     }
 334 
 335 }