import Base from './core/Base';
import Vector3 from './math/Vector3';
import Quaternion from './math/Quaternion';
import Matrix4 from './math/Matrix4';
import mat4 from './glmatrix/mat4';
import BoundingBox from './math/BoundingBox';
var nameId = 0;
/**
* @constructor clay.Node
* @extends clay.core.Base
*/
var Node = Base.extend(/** @lends clay.Node# */{
/**
* Scene node name
* @type {string}
*/
name: '',
/**
* Position relative to its parent node. aka translation.
* @type {clay.Vector3}
*/
position: null,
/**
* Rotation relative to its parent node. Represented by a quaternion
* @type {clay.Quaternion}
*/
rotation: null,
/**
* Scale relative to its parent node
* @type {clay.Vector3}
*/
scale: null,
/**
* Affine transform matrix relative to its root scene.
* @type {clay.Matrix4}
*/
worldTransform: null,
/**
* Affine transform matrix relative to its parent node.
* Composited with position, rotation and scale.
* @type {clay.Matrix4}
*/
localTransform: null,
/**
* If the local transform is update from SRT(scale, rotation, translation, which is position here) each frame
* @type {boolean}
*/
autoUpdateLocalTransform: true,
/**
* Parent of current scene node
* @type {?clay.Node}
* @private
*/
_parent: null,
/**
* The root scene mounted. Null if it is a isolated node
* @type {?clay.Scene}
* @private
*/
_scene: null,
/**
* @type {boolean}
* @private
*/
_needsUpdateWorldTransform: true,
/**
* @type {boolean}
* @private
*/
_inIterating: false,
// Depth for transparent list sorting
__depth: 0
}, function () {
if (!this.name) {
this.name = (this.type || 'NODE') + '_' + (nameId++);
}
if (!this.position) {
this.position = new Vector3();
}
if (!this.rotation) {
this.rotation = new Quaternion();
}
if (!this.scale) {
this.scale = new Vector3(1, 1, 1);
}
this.worldTransform = new Matrix4();
this.localTransform = new Matrix4();
this._children = [];
},
/**@lends clay.Node.prototype. */
{
/**
* @type {?clay.Vector3}
* @instance
*/
target: null,
/**
* If node and its chilren invisible
* @type {boolean}
* @instance
*/
invisible: false,
/**
* If Node is a skinned mesh
* @return {boolean}
*/
isSkinnedMesh: function () {
return false;
},
/**
* Return true if it is a renderable scene node, like Mesh and ParticleSystem
* @return {boolean}
*/
isRenderable: function () {
return false;
},
/**
* Set the name of the scene node
* @param {string} name
*/
setName: function (name) {
var scene = this._scene;
if (scene) {
var nodeRepository = scene._nodeRepository;
delete nodeRepository[this.name];
nodeRepository[name] = this;
}
this.name = name;
},
/**
* Add a child node
* @param {clay.Node} node
*/
add: function (node) {
var originalParent = node._parent;
if (originalParent === this) {
return;
}
if (originalParent) {
originalParent.remove(node);
}
node._parent = this;
this._children.push(node);
var scene = this._scene;
if (scene && scene !== node.scene) {
node.traverse(this._addSelfToScene, this);
}
// Mark children needs update transform
// In case child are remove and added again after parent moved
node._needsUpdateWorldTransform = true;
},
/**
* Remove the given child scene node
* @param {clay.Node} node
*/
remove: function (node) {
var children = this._children;
var idx = children.indexOf(node);
if (idx < 0) {
return;
}
children.splice(idx, 1);
node._parent = null;
if (this._scene) {
node.traverse(this._removeSelfFromScene, this);
}
},
/**
* Remove all children
*/
removeAll: function () {
var children = this._children;
for (var idx = 0; idx < children.length; idx++) {
children[idx]._parent = null;
if (this._scene) {
children[idx].traverse(this._removeSelfFromScene, this);
}
}
this._children = [];
},
/**
* Get the scene mounted
* @return {clay.Scene}
*/
getScene: function () {
return this._scene;
},
/**
* Get parent node
* @return {clay.Scene}
*/
getParent: function () {
return this._parent;
},
_removeSelfFromScene: function (descendant) {
descendant._scene.removeFromScene(descendant);
descendant._scene = null;
},
_addSelfToScene: function (descendant) {
this._scene.addToScene(descendant);
descendant._scene = this._scene;
},
/**
* Return true if it is ancestor of the given scene node
* @param {clay.Node} node
*/
isAncestor: function (node) {
var parent = node._parent;
while(parent) {
if (parent === this) {
return true;
}
parent = parent._parent;
}
return false;
},
/**
* Get a new created array of all children nodes
* @return {clay.Node[]}
*/
children: function () {
return this._children.slice();
},
/**
* Get child scene node at given index.
* @param {number} idx
* @return {clay.Node}
*/
childAt: function (idx) {
return this._children[idx];
},
/**
* Get first child with the given name
* @param {string} name
* @return {clay.Node}
*/
getChildByName: function (name) {
var children = this._children;
for (var i = 0; i < children.length; i++) {
if (children[i].name === name) {
return children[i];
}
}
},
/**
* Get first descendant have the given name
* @param {string} name
* @return {clay.Node}
*/
getDescendantByName: function (name) {
var children = this._children;
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child.name === name) {
return child;
} else {
var res = child.getDescendantByName(name);
if (res) {
return res;
}
}
}
},
/**
* Query descendant node by path
* @param {string} path
* @return {clay.Node}
* @example
* node.queryNode('root/parent/child');
*/
queryNode: function (path) {
if (!path) {
return;
}
// TODO Name have slash ?
var pathArr = path.split('/');
var current = this;
for (var i = 0; i < pathArr.length; i++) {
var name = pathArr[i];
// Skip empty
if (!name) {
continue;
}
var found = false;
var children = current._children;
for (var j = 0; j < children.length; j++) {
var child = children[j];
if (child.name === name) {
current = child;
found = true;
break;
}
}
// Early return if not found
if (!found) {
return;
}
}
return current;
},
/**
* Get query path, relative to rootNode(default is scene)
* @param {clay.Node} [rootNode]
* @return {string}
*/
getPath: function (rootNode) {
if (!this._parent) {
return '/';
}
var current = this._parent;
var path = this.name;
while (current._parent) {
path = current.name + '/' + path;
if (current._parent == rootNode) {
break;
}
current = current._parent;
}
if (!current._parent && rootNode) {
return null;
}
return path;
},
/**
* Depth first traverse all its descendant scene nodes.
*
* **WARN** Don't do `add`, `remove` operation in the callback during traverse.
* @param {Function} callback
* @param {Node} [context]
*/
traverse: function (callback, context) {
callback.call(context, this);
var _children = this._children;
for(var i = 0, len = _children.length; i < len; i++) {
_children[i].traverse(callback, context);
}
},
/**
* Traverse all children nodes.
*
* **WARN** DON'T do `add`, `remove` operation in the callback during iteration.
*
* @param {Function} callback
* @param {Node} [context]
*/
eachChild: function (callback, context) {
var _children = this._children;
for(var i = 0, len = _children.length; i < len; i++) {
var child = _children[i];
callback.call(context, child, i);
}
},
/**
* Set the local transform and decompose to SRT
* @param {clay.Matrix4} matrix
*/
setLocalTransform: function (matrix) {
mat4.copy(this.localTransform.array, matrix.array);
this.decomposeLocalTransform();
},
/**
* Decompose the local transform to SRT
*/
decomposeLocalTransform: function (keepScale) {
var scale = !keepScale ? this.scale: null;
this.localTransform.decomposeMatrix(scale, this.rotation, this.position);
},
/**
* Set the world transform and decompose to SRT
* @param {clay.Matrix4} matrix
*/
setWorldTransform: function (matrix) {
mat4.copy(this.worldTransform.array, matrix.array);
this.decomposeWorldTransform();
},
/**
* Decompose the world transform to SRT
* @function
*/
decomposeWorldTransform: (function () {
var tmp = mat4.create();
return function (keepScale) {
var localTransform = this.localTransform;
var worldTransform = this.worldTransform;
// Assume world transform is updated
if (this._parent) {
mat4.invert(tmp, this._parent.worldTransform.array);
mat4.multiply(localTransform.array, tmp, worldTransform.array);
} else {
mat4.copy(localTransform.array, worldTransform.array);
}
var scale = !keepScale ? this.scale: null;
localTransform.decomposeMatrix(scale, this.rotation, this.position);
};
})(),
transformNeedsUpdate: function () {
return this.position._dirty
|| this.rotation._dirty
|| this.scale._dirty;
},
/**
* Update local transform from SRT
* Notice that local transform will not be updated if _dirty mark of position, rotation, scale is all false
*/
updateLocalTransform: function () {
var position = this.position;
var rotation = this.rotation;
var scale = this.scale;
if (this.transformNeedsUpdate()) {
var m = this.localTransform.array;
// Transform order, scale->rotation->position
mat4.fromRotationTranslation(m, rotation.array, position.array);
mat4.scale(m, m, scale.array);
rotation._dirty = false;
scale._dirty = false;
position._dirty = false;
this._needsUpdateWorldTransform = true;
}
},
/**
* Update world transform, assume its parent world transform have been updated
* @private
*/
_updateWorldTransformTopDown: function () {
var localTransform = this.localTransform.array;
var worldTransform = this.worldTransform.array;
if (this._parent) {
mat4.multiplyAffine(
worldTransform,
this._parent.worldTransform.array,
localTransform
);
}
else {
mat4.copy(worldTransform, localTransform);
}
},
/**
* Update world transform before whole scene is updated.
*/
updateWorldTransform: function () {
// Find the root node which transform needs update;
var rootNodeIsDirty = this;
while (rootNodeIsDirty && rootNodeIsDirty.getParent()
&& rootNodeIsDirty.getParent().transformNeedsUpdate()
) {
rootNodeIsDirty = rootNodeIsDirty.getParent();
}
rootNodeIsDirty.update();
},
/**
* Update local transform and world transform recursively
* @param {boolean} forceUpdateWorld
*/
update: function (forceUpdateWorld) {
if (this.autoUpdateLocalTransform) {
this.updateLocalTransform();
}
else {
// Transform is manually setted
forceUpdateWorld = true;
}
if (forceUpdateWorld || this._needsUpdateWorldTransform) {
this._updateWorldTransformTopDown();
forceUpdateWorld = true;
this._needsUpdateWorldTransform = false;
}
var children = this._children;
for(var i = 0, len = children.length; i < len; i++) {
children[i].update(forceUpdateWorld);
}
},
/**
* Get bounding box of node
* @param {Function} [filter]
* @param {clay.BoundingBox} [out]
* @return {clay.BoundingBox}
*/
// TODO Skinning
getBoundingBox: (function () {
function defaultFilter (el) {
return !el.invisible && el.geometry;
}
var tmpBBox = new BoundingBox();
var tmpMat4 = new Matrix4();
var invWorldTransform = new Matrix4();
return function (filter, out) {
out = out || new BoundingBox();
filter = filter || defaultFilter;
if (this._parent) {
Matrix4.invert(invWorldTransform, this._parent.worldTransform);
}
else {
Matrix4.identity(invWorldTransform);
}
this.traverse(function (mesh) {
if (mesh.geometry && mesh.geometry.boundingBox) {
tmpBBox.copy(mesh.geometry.boundingBox);
Matrix4.multiply(tmpMat4, invWorldTransform, mesh.worldTransform);
tmpBBox.applyTransform(tmpMat4);
out.union(tmpBBox);
}
}, this, defaultFilter);
return out;
};
})(),
/**
* Get world position, extracted from world transform
* @param {clay.Vector3} [out]
* @return {clay.Vector3}
*/
getWorldPosition: function (out) {
// PENDING
if (this.transformNeedsUpdate()) {
this.updateWorldTransform();
}
var m = this.worldTransform.array;
if (out) {
var arr = out.array;
arr[0] = m[12];
arr[1] = m[13];
arr[2] = m[14];
return out;
}
else {
return new Vector3(m[12], m[13], m[14]);
}
},
/**
* Clone a new node
* @return {Node}
*/
clone: function () {
var node = new this.constructor();
var children = this._children;
node.setName(this.name);
node.position.copy(this.position);
node.rotation.copy(this.rotation);
node.scale.copy(this.scale);
for (var i = 0; i < children.length; i++) {
node.add(children[i].clone());
}
return node;
},
/**
* Rotate the node around a axis by angle degrees, axis passes through point
* @param {clay.Vector3} point Center point
* @param {clay.Vector3} axis Center axis
* @param {number} angle Rotation angle
* @see http://docs.unity3d.com/Documentation/ScriptReference/Transform.RotateAround.html
* @function
*/
rotateAround: (function () {
var v = new Vector3();
var RTMatrix = new Matrix4();
// TODO improve performance
return function (point, axis, angle) {
v.copy(this.position).subtract(point);
var localTransform = this.localTransform;
localTransform.identity();
// parent node
localTransform.translate(point);
localTransform.rotate(angle, axis);
RTMatrix.fromRotationTranslation(this.rotation, v);
localTransform.multiply(RTMatrix);
localTransform.scale(this.scale);
this.decomposeLocalTransform();
this._needsUpdateWorldTransform = true;
};
})(),
/**
* @param {clay.Vector3} target
* @param {clay.Vector3} [up]
* @see http://www.opengl.org/sdk/docs/man2/xhtml/gluLookAt.xml
* @function
*/
lookAt: (function () {
var m = new Matrix4();
return function (target, up) {
m.lookAt(this.position, target, up || this.localTransform.y).invert();
this.setLocalTransform(m);
this.target = target;
};
})()
});
export default Node;