/*
* Copyright (c) 2009-2010 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.scene;
import com.jme3.bounding.BoundingVolume;
import com.jme3.collision.Collidable;
import com.jme3.collision.CollisionResults;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.Savable;
import com.jme3.material.Material;
import com.jme3.util.SafeArrayList;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Node
defines an internal node of a scene graph. The internal
* node maintains a collection of children and handles merging said children
* into a single bound to allow for very fast culling of multiple nodes. Node
* allows for any number of children to be attached.
*
* @author Mark Powell
* @author Gregg Patton
* @author Joshua Slack
*/
public class Node extends Spatial implements Savable {
private static final Logger logger = Logger.getLogger(Node.class.getName());
/**
* This node's children.
*/
protected SafeArrayListNode
with a default empty
* list for containing children.
*
* @param name
* the name of the scene element. This is required for
* identification and comparision purposes.
*/
public Node(String name) {
super(name);
}
/**
*
* getQuantity
returns the number of children this node
* maintains.
*
* @return the number of children this node maintains.
*/
public int getQuantity() {
return children.size();
}
@Override
protected void setTransformRefresh(){
super.setTransformRefresh();
for (Spatial child : children.getArray()){
if ((child.refreshFlags & RF_TRANSFORM) != 0)
continue;
child.setTransformRefresh();
}
}
@Override
protected void setLightListRefresh(){
super.setLightListRefresh();
for (Spatial child : children.getArray()){
if ((child.refreshFlags & RF_LIGHTLIST) != 0)
continue;
child.setLightListRefresh();
}
}
@Override
protected void updateWorldBound(){
super.updateWorldBound();
// for a node, the world bound is a combination of all it's children
// bounds
BoundingVolume resultBound = null;
for (Spatial child : children.getArray()) {
// child bound is assumed to be updated
assert (child.refreshFlags & RF_BOUND) == 0;
if (resultBound != null) {
// merge current world bound with child world bound
resultBound.mergeLocal(child.getWorldBound());
} else {
// set world bound to first non-null child world bound
if (child.getWorldBound() != null) {
resultBound = child.getWorldBound().clone(this.worldBound);
}
}
}
this.worldBound = resultBound;
}
@Override
public void updateLogicalState(float tpf){
super.updateLogicalState(tpf);
if (children.isEmpty()) {
return;
}
for (Spatial child : children.getArray()) {
child.updateLogicalState(tpf);
}
}
@Override
public void updateGeometricState(){
if ((refreshFlags & RF_LIGHTLIST) != 0){
updateWorldLightList();
}
if ((refreshFlags & RF_TRANSFORM) != 0){
// combine with parent transforms- same for all spatial
// subclasses.
updateWorldTransforms();
}
if (!children.isEmpty()) {
// the important part- make sure child geometric state is refreshed
// first before updating own world bound. This saves
// a round-trip later on.
// NOTE 9/19/09
// Although it does save a round trip,
for (Spatial child : children.getArray()) {
child.updateGeometricState();
}
}
if ((refreshFlags & RF_BOUND) != 0){
updateWorldBound();
}
assert refreshFlags == 0;
}
/**
* getTriangleCount
returns the number of triangles contained
* in all sub-branches of this node that contain geometry.
*
* @return the triangle count of this branch.
*/
@Override
public int getTriangleCount() {
int count = 0;
if(children != null) {
for(int i = 0; i < children.size(); i++) {
count += children.get(i).getTriangleCount();
}
}
return count;
}
/**
* getVertexCount
returns the number of vertices contained
* in all sub-branches of this node that contain geometry.
*
* @return the vertex count of this branch.
*/
@Override
public int getVertexCount() {
int count = 0;
if(children != null) {
for(int i = 0; i < children.size(); i++) {
count += children.get(i).getVertexCount();
}
}
return count;
}
/**
* attachChild
attaches a child to this node. This node
* becomes the child's parent. The current number of children maintained is
* returned.
*
* If the child already had a parent it is detached from that former parent.
*
* @param child
* the child to attach to this node.
* @return the number of children maintained by this node.
* @throws NullPointerException If child is null.
*/
public int attachChild(Spatial child) {
if (child == null)
throw new NullPointerException();
if (child.getParent() != this && child != this) {
if (child.getParent() != null) {
child.getParent().detachChild(child);
}
child.setParent(this);
children.add(child);
// XXX: Not entirely correct? Forces bound update up the
// tree stemming from the attached child. Also forces
// transform update down the tree-
child.setTransformRefresh();
child.setLightListRefresh();
if (logger.isLoggable(Level.INFO)) {
logger.log(Level.INFO,"Child ({0}) attached to this node ({1})",
new Object[]{child.getName(), getName()});
}
}
return children.size();
}
/**
*
* attachChildAt
attaches a child to this node at an index. This node
* becomes the child's parent. The current number of children maintained is
* returned.
*
* If the child already had a parent it is detached from that former parent.
*
* @param child
* the child to attach to this node.
* @return the number of children maintained by this node.
* @throws NullPointerException if child is null.
*/
public int attachChildAt(Spatial child, int index) {
if (child == null)
throw new NullPointerException();
if (child.getParent() != this && child != this) {
if (child.getParent() != null) {
child.getParent().detachChild(child);
}
child.setParent(this);
children.add(index, child);
child.setTransformRefresh();
child.setLightListRefresh();
if (logger.isLoggable(Level.INFO)) {
logger.log(Level.INFO,"Child ({0}) attached to this node ({1})",
new Object[]{child.getName(), getName()});
}
}
return children.size();
}
/**
* detachChild
removes a given child from the node's list.
* This child will no longer be maintained.
*
* @param child
* the child to remove.
* @return the index the child was at. -1 if the child was not in the list.
*/
public int detachChild(Spatial child) {
if (child == null)
throw new NullPointerException();
if (child.getParent() == this) {
int index = children.indexOf(child);
if (index != -1) {
detachChildAt(index);
}
return index;
}
return -1;
}
/**
* detachChild
removes a given child from the node's list.
* This child will no longe be maintained. Only the first child with a
* matching name is removed.
*
* @param childName
* the child to remove.
* @return the index the child was at. -1 if the child was not in the list.
*/
public int detachChildNamed(String childName) {
if (childName == null)
throw new NullPointerException();
for (int x = 0, max = children.size(); x < max; x++) {
Spatial child = children.get(x);
if (childName.equals(child.getName())) {
detachChildAt( x );
return x;
}
}
return -1;
}
/**
*
* detachChildAt
removes a child at a given index. That child
* is returned for saving purposes.
*
* @param index
* the index of the child to be removed.
* @return the child at the supplied index.
*/
public Spatial detachChildAt(int index) {
Spatial child = children.remove(index);
if ( child != null ) {
child.setParent( null );
logger.log(Level.INFO, "{0}: Child removed.", this.toString());
// since a child with a bound was detached;
// our own bound will probably change.
setBoundRefresh();
// our world transform no longer influences the child.
// XXX: Not neccessary? Since child will have transform updated
// when attached anyway.
child.setTransformRefresh();
// lights are also inherited from parent
child.setLightListRefresh();
}
return child;
}
/**
*
* detachAllChildren
removes all children attached to this
* node.
*/
public void detachAllChildren() {
for ( int i = children.size() - 1; i >= 0; i-- ) {
detachChildAt(i);
}
logger.log(Level.INFO, "{0}: All children removed.", this.toString());
}
/**
* getChildIndex
returns the index of the given spatial
* in this node's list of children.
* @param sp
* The spatial to look up
* @return
* The index of the spatial in the node's children, or -1
* if the spatial is not attached to this node
*/
public int getChildIndex(Spatial sp) {
return children.indexOf(sp);
}
/**
* More efficient than e.g detaching and attaching as no updates are needed.
*
* @param index1 The index of the first child to swap
* @param index2 The index of the second child to swap
*/
public void swapChildren(int index1, int index2) {
Spatial c2 = children.get(index2);
Spatial c1 = children.remove(index1);
children.add(index1, c2);
children.remove(index2);
children.add(index2, c1);
}
/**
*
* getChild
returns a child at a given index.
*
* @param i
* the index to retrieve the child from.
* @return the child at a specified index.
*/
public Spatial getChild(int i) {
return children.get(i);
}
/**
* getChild
returns the first child found with exactly the
* given name (case sensitive.)
*
* @param name
* the name of the child to retrieve. If null, we'll return null.
* @return the child if found, or null.
*/
public Spatial getChild(String name) {
if (name == null)
return null;
for (Spatial child : children.getArray()) {
if (name.equals(child.getName())) {
return child;
} else if(child instanceof Node) {
Spatial out = ((Node)child).getChild(name);
if(out != null) {
return out;
}
}
}
return null;
}
/**
* determines if the provided Spatial is contained in the children list of
* this node.
*
* @param spat
* the child object to look for.
* @return true if the object is contained, false otherwise.
*/
public boolean hasChild(Spatial spat) {
if (children.contains(spat))
return true;
for (Spatial child : children.getArray()) {
if (child instanceof Node && ((Node) child).hasChild(spat))
return true;
}
return false;
}
/**
* Returns all children to this node. Note that modifying that given
* list is not allowed.
*
* @return a list containing all children to this node
*/
public List
* Note that we are matching the pattern, therefore the pattern * must match the entire pattern (i.e. it behaves as if it is sandwiched * between "^" and "$"). * You can set regex modes, like case insensitivity, by using the (?X) * or (?X:Y) constructs. *
* By design, it is always safe to code loops like:
*
* for (Spatial spatial : node.descendantMatches(AClass.class, "regex"))
*
* "Descendants" does not include self, per the definition of the word.
* To test for descendants AND self, you must do a
* node.matches(aClass, aRegex)
+
* node.descendantMatches(aClass, aRegex)
.
*
*
* @param spatialSubclass Subclass which matching Spatials must implement.
* Null causes all Spatials to qualify.
* @param nameRegex Regular expression to match Spatial name against.
* Null causes all Names to qualify.
* @return Non-null, but possibly 0-element, list of matching Spatials (also Instances extending Spatials).
*
* @see java.util.regex.Pattern
* @see Spatial#matches(java.lang.Class, java.lang.String)
*/
@SuppressWarnings("unchecked")
public