blob: bb7534a2758757326dfb9a616ba9f86df5d686c1 [file] [log] [blame]
/*
* 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;
/**
* <code>Node</code> 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 SafeArrayList<Spatial> children = new SafeArrayList<Spatial>(Spatial.class);
/**
* Serialization only. Do not use.
*/
public Node() {
}
/**
* Constructor instantiates a new <code>Node</code> 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);
}
/**
*
* <code>getQuantity</code> 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;
}
/**
* <code>getTriangleCount</code> 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;
}
/**
* <code>getVertexCount</code> 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;
}
/**
* <code>attachChild</code> attaches a child to this node. This node
* becomes the child's parent. The current number of children maintained is
* returned.
* <br>
* 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();
}
/**
*
* <code>attachChildAt</code> attaches a child to this node at an index. This node
* becomes the child's parent. The current number of children maintained is
* returned.
* <br>
* 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();
}
/**
* <code>detachChild</code> 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;
}
/**
* <code>detachChild</code> 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;
}
/**
*
* <code>detachChildAt</code> 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;
}
/**
*
* <code>detachAllChildren</code> 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());
}
/**
* <code>getChildIndex</code> 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);
}
/**
*
* <code>getChild</code> 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);
}
/**
* <code>getChild</code> 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<Spatial> getChildren() {
return children;
}
@Override
public void setMaterial(Material mat){
for (int i = 0; i < children.size(); i++){
children.get(i).setMaterial(mat);
}
}
@Override
public void setLodLevel(int lod){
super.setLodLevel(lod);
for (Spatial child : children.getArray()) {
child.setLodLevel(lod);
}
}
public int collideWith(Collidable other, CollisionResults results){
int total = 0;
for (Spatial child : children.getArray()){
total += child.collideWith(other, results);
}
return total;
}
/**
* Returns flat list of Spatials implementing the specified class AND
* with name matching the specified pattern.
* </P> <P>
* Note that we are <i>matching</i> 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.
* </P> <P>
* By design, it is always safe to code loops like:<CODE><PRE>
* for (Spatial spatial : node.descendantMatches(AClass.class, "regex"))
* </PRE></CODE>
* </P> <P>
* "Descendants" does not include self, per the definition of the word.
* To test for descendants AND self, you must do a
* <code>node.matches(aClass, aRegex)</code> +
* <code>node.descendantMatches(aClass, aRegex)</code>.
* <P>
*
* @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 <T extends Spatial>List<T> descendantMatches(
Class<T> spatialSubclass, String nameRegex) {
List<T> newList = new ArrayList<T>();
if (getQuantity() < 1) return newList;
for (Spatial child : getChildren()) {
if (child.matches(spatialSubclass, nameRegex))
newList.add((T)child);
if (child instanceof Node)
newList.addAll(((Node) child).descendantMatches(
spatialSubclass, nameRegex));
}
return newList;
}
/**
* Convenience wrapper.
*
* @see #descendantMatches(java.lang.Class, java.lang.String)
*/
public <T extends Spatial>List<T> descendantMatches(
Class<T> spatialSubclass) {
return descendantMatches(spatialSubclass, null);
}
/**
* Convenience wrapper.
*
* @see #descendantMatches(java.lang.Class, java.lang.String)
*/
public <T extends Spatial>List<T> descendantMatches(String nameRegex) {
return descendantMatches(null, nameRegex);
}
@Override
public Node clone(boolean cloneMaterials){
Node nodeClone = (Node) super.clone(cloneMaterials);
// nodeClone.children = new ArrayList<Spatial>();
// for (Spatial child : children){
// Spatial childClone = child.clone();
// childClone.parent = nodeClone;
// nodeClone.children.add(childClone);
// }
return nodeClone;
}
@Override
public Spatial deepClone(){
Node nodeClone = (Node) super.clone();
nodeClone.children = new SafeArrayList<Spatial>(Spatial.class);
for (Spatial child : children){
Spatial childClone = child.deepClone();
childClone.parent = nodeClone;
nodeClone.children.add(childClone);
}
return nodeClone;
}
@Override
public void write(JmeExporter e) throws IOException {
super.write(e);
e.getCapsule(this).writeSavableArrayList(new ArrayList(children), "children", null);
}
@Override
public void read(JmeImporter e) throws IOException {
// XXX: Load children before loading itself!!
// This prevents empty children list if controls query
// it in Control.setSpatial().
children = new SafeArrayList( Spatial.class,
e.getCapsule(this).readSavableArrayList("children", null) );
// go through children and set parent to this node
if (children != null) {
for (Spatial child : children.getArray()) {
child.parent = this;
}
}
super.read(e);
}
@Override
public void setModelBound(BoundingVolume modelBound) {
if(children != null) {
for (Spatial child : children.getArray()) {
child.setModelBound(modelBound != null ? modelBound.clone(null) : null);
}
}
}
@Override
public void updateModelBound() {
if(children != null) {
for (Spatial child : children.getArray()) {
child.updateModelBound();
}
}
}
@Override
public void depthFirstTraversal(SceneGraphVisitor visitor) {
for (Spatial child : children.getArray()) {
child.depthFirstTraversal(visitor);
}
visitor.visit(this);
}
@Override
protected void breadthFirstTraversal(SceneGraphVisitor visitor, Queue<Spatial> queue) {
queue.addAll(children);
}
}