cannon
Version:
A lightweight 3D physics engine written in JavaScript.
1,612 lines (1,456 loc) • 810 kB
JavaScript
/*
* SceneJS WebGL Scene Graph Library for JavaScript
* http://scenejs.org/
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://scenejs.org/license
* Copyright 2010, Lindsay Kay
*
* Includes WebGLTrace
* Various functions for helping debug WebGL apps.
* http://github.com/jackpal/webgltrace
* Copyright (c) 2009 The Chromium Authors. All rights reserved.
*
* Includes WebGL-Debug
* Various functions for helping debug WebGL apps.
* http://khronos.org/webgl/wiki/Debugging
* Copyright (c) 2009 The Chromium Authors. All rights reserved.
*/
/** Generic map of IDs to items - can generate own IDs or accept given IDs.
* Given IDs should be strings in order to not clash with internally generated IDs, which are numbers.
*/
var SceneJS_Map = function() {
this.items = [];
this.lastUniqueId = 0;
this.addItem = function() {
var item;
if (arguments.length == 2) {
var id = arguments[0];
item = arguments[1];
if (this.items[id]) { // Won't happen if given ID is string
throw SceneJS_errorModule.fatalError(SceneJS.errors.ID_CLASH, "ID clash: '" + id + "'");
}
this.items[id] = item;
return id;
} else {
while (true) {
item = arguments[0];
var findId = this.lastUniqueId++ ;
if (!this.items[findId]) {
this.items[findId] = item;
return findId;
}
}
}
};
this.removeItem = function(id) {
this.items[id] = null;
};
};
/**
* @class SceneJS
* SceneJS name space
* @singleton
*/
var SceneJS = {
/** Version of this release
*/
VERSION: '2.0.0',
/** Names of supported WebGL canvas contexts
*/
SUPPORTED_WEBGL_CONTEXT_NAMES:["webgl", "experimental-webgl", "webkit-3d", "moz-webgl", "moz-glweb20"],
/**
* Node classes
* @private
*/
_nodeTypes: {},
/**
* Map of existing scene nodes
*/
_scenes: {},
/** Extension point to create a new node type.
*
* @param {string} type Name of new subtype
* @param {string} superType Optional name of super-type - {@link SceneJS_node} by default
* @return {class} New node class
*/
createNodeType : function(type, superType) {
if (!type) {
throw SceneJS_errorModule.fatalError("createNodeType param 'type' is null or undefined");
}
if (typeof type != "string") {
throw SceneJS_errorModule.fatalError("createNodeType param 'type' should be a string");
}
if (this._nodeTypes[type]) {
throw SceneJS_errorModule.fatalError("createNodeType node of param 'type' already defined: '" + superType + "'");
}
var supa = this._nodeTypes[superType];
if (!supa) {
supa = SceneJS._Node;
}
var nodeType = function() { // Create class
supa.apply(this, arguments);
this.attr.type = type;
};
SceneJS._inherit(nodeType, supa);
this._nodeTypes[type] = nodeType;
return nodeType;
},
_registerNode : function(type, nodeClass) {
this._nodeTypes[type] = nodeClass;
},
/**
* Factory function to create a "scene" node
*/
createScene : function(json) {
if (!json) {
throw SceneJS_errorModule.fatalError("createScene param 'json' is null or undefined");
}
json.type = "scene";
if (!json.id) {
throw SceneJS_errorModule.fatalError(
SceneJS.errors.ILLEGAL_NODE_CONFIG,
"createScene 'id' is mandatory for 'scene' node");
}
if (this._scenes[json.id]) {
throw SceneJS_errorModule.fatalError(
SceneJS.errors.ILLEGAL_NODE_CONFIG,
"createScene - scene id '" + json.id + "' already taken by another scene");
}
var newNode = this._parseNodeJSON(json, undefined); // Scene references itself as the owner scene
this._scenes[json.id] = newNode;
SceneJS_eventModule.fireEvent(SceneJS_eventModule.NODE_CREATED, { nodeId : newNode.attr.id, json: json });
return SceneJS._selectNode(newNode);
},
/** Returns true if the "scene" node with the given ID exists
*/
sceneExists : function(sceneId) {
return this._scenes[sceneId] ? true : false;
},
/** Select a "scene" node
*/
scene : function(sceneId) {
var scene = this._scenes[sceneId];
if (!scene) {
throw SceneJS_errorModule.fatalError(
SceneJS.errors.ILLEGAL_NODE_CONFIG,
"withScene scene not found: '" + sceneId + "'");
}
return SceneJS._selectNode(scene);
},
_parseNodeJSON : function(json, scene) {
var newNode = this._createNode(json, scene);
scene = scene || newNode;
newNode.scene = scene;
if (json.nodes) {
var len = json.nodes.length;
for (var i = 0; i < len; i++) {
newNode.addNode(SceneJS._parseNodeJSON(json.nodes[i], scene));
}
}
return newNode;
},
_createNode : function(json, scene) {
json.type = json.type || "node";
var nodeType;
if (json.type == "node") {
nodeType = SceneJS._Node;
} else {
nodeType = this._nodeTypes[json.type];
if (!nodeType) {
throw SceneJS_errorModule.fatalError("Scene node type not supported in SceneJS " + SceneJS.VERSION + ": '" + json.type + "'");
}
}
return new nodeType(json, scene);
},
/**
* Shallow copy of JSON node configs, filters out JSON-specific properties like "nodes"
* @private
*/
_copyCfg : function (cfg) {
var cfg2 = {};
for (var key in cfg) {
if (cfg.hasOwnProperty(key) && key != "nodes") {
cfg2[key] = cfg[key];
}
}
return cfg2;
},
/** Nodes that are scheduled to be destroyed. When a node is destroyed it is added here, then at the end of each
* render traversal, each node in here is popped and has {@link SceneJS_node#destroy} called.
* @private
*/
_destroyedNodes : [],
/** Action the scheduled destruction of nodes
* @private
*/
_actionNodeDestroys : function() {
if (this._destroyedNodes.length > 0) {
var node;
for (var i = this._destroyedNodes.length - 1; i >= 0; i--) {
node = this._destroyedNodes[i];
node._doDestroy();
SceneJS_eventModule.fireEvent(SceneJS_eventModule.NODE_DESTROYED, { nodeId : node.attr.id });
}
this._destroyedNodes = [];
}
},
/*----------------------------------------------------------------------------------------------------------------
* Messaging System
*
*
*--------------------------------------------------------------------------------------------------------------*/
Message : new (function() {
/** Sends a message to SceneJS - docs at http://scenejs.wikispaces.com/SceneJS+Messaging+System
*
* @param message
*/
this.sendMessage = function (message) {
if (!message) {
throw SceneJS_errorModule.fatalError("sendMessage param 'message' null or undefined");
}
var commandId = message.command;
if (!commandId) {
throw SceneJS_errorModule.fatalError("Message element expected: 'command'");
}
var commandService = SceneJS.Services.getService(SceneJS.Services.COMMAND_SERVICE_ID);
var command = commandService.getCommand(commandId);
if (!command) {
throw SceneJS_errorModule.fatalError("Message command not supported: '" + commandId + "' - perhaps this command needs to be added to the SceneJS Command Service?");
}
var ctx = {};
command.execute(ctx, message);
};
})(),
/**
* @private
*/
_inherit : function(DerivedClassName, BaseClassName) {
DerivedClassName.prototype = new BaseClassName();
DerivedClassName.prototype.constructor = DerivedClassName;
},
/** Creates a namespace
* @private
*/
_namespace : function() {
var a = arguments, o = null, i, j, d, rt;
for (i = 0; i < a.length; ++i) {
d = a[i].split(".");
rt = d[0];
eval('if (typeof ' + rt + ' == "undefined"){' + rt + ' = {};} o = ' + rt + ';');
for (j = 1; j < d.length; ++j) {
o[d[j]] = o[d[j]] || {};
o = o[d[j]];
}
}
},
/**
* Returns a key for a vacant slot in the given map
* @private
*/
// Add a new function that returns a unique map key.
_last_unique_id: 0,
_createKeyForMap : function(keyMap, prefix) {
while (true) {
var key = prefix ? prefix + SceneJS._last_unique_id++ : SceneJS._last_unique_id++;
if (!keyMap[key]) {
return key;
}
}
},
/** Stolen from GLGE:https://github.com/supereggbert/GLGE/blob/master/glge-compiled.js#L1656
*/
_createUUID:function() {
var data = ["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"];
var data2 = ["8","9","A","B"];
var uuid = [];
for (var i = 0; i < 38; i++) {
switch (i) {
case 8:uuid.push("-");
break;
case 13:uuid.push("-");
break;
case 18:uuid.push("-");
break;
case 14:uuid.push("4");
break;
case 19:uuid.push(data2[Math.round(Math.random() * 3)]);
break;
default:uuid.push(data[Math.round(Math.random() * 15)]);
break;
}
}
return uuid.join("");
},
_getBaseURL : function(url) {
var i = url.lastIndexOf("/");
if (i == 0 || i == -1) {
return "";
}
return url.substr(0, i + 1);
},
/**
* Returns true if object is an array
* @private
*/
_isArray : function(testObject) {
return testObject && !(testObject.propertyIsEnumerable('length'))
&& typeof testObject === 'object' && typeof testObject.length === 'number';
},
_shallowClone : function(o) {
var o2 = {};
for (var name in o) {
if (o.hasOwnProperty(name)) {
o2[name] = o[name];
}
}
return o2;
} ,
/** Add properties of o to o2 where undefined or null on o2
*/
_applyIf : function(o, o2) {
for (var name in o) {
if (o.hasOwnProperty(name)) {
if (o2[name] == undefined || o2[name] == null) {
o2[name] = o[name];
}
}
}
return o2;
},
/** Add properties of o to o2, overwriting them on o2 if already there.
* The optional clear flag causes properties on o2 to be cleared first
*/
_apply : function(o, o2, clear) {
var name;
if (clear) {
for (name in o2) {
if (o2.hasOwnProperty(name)) {
delete o2[name];
}
}
}
for (name in o) {
if (o.hasOwnProperty(name)) {
o2[name] = o[name];
}
}
return o2;
},
/** Lazy-bound state resources published by "suppliers"
*/
_compilationStates : new (function() {
var suppliers = {};
this.setSupplier = function(type, supplier) {
suppliers[type] = supplier;
};
this.getState = function(type, sceneId, id) {
var s = suppliers[type];
if (!s) {
throw SceneJS_errorModule.fatalError("Internal error - Compilation state supplier not found: '" + type + "'");
}
return s.get(sceneId, id);
};
})()
};
/**
* @class The basic scene node type
*/
SceneJS._Node = function(cfg, scene) {
/* Public properties are stored on the _attr map
*/
this.attr = {};
this.attr.type = "node";
this.attr.sid = null;
this.attr.data = {};
if (cfg) {
this.attr.id = cfg.id;
this.attr.type = cfg.type || "node";
this.attr.data = cfg.data;
this.attr.enabled = cfg.enabled === false ? false : true;
this.attr.sid = cfg.sid;
this.attr.info = cfg.info;
this.scene = scene || this;
}
/* Child nodes
*/
this.children = [];
this.parent = null;
this.listeners = {};
this.numListeners = 0; // Useful for quick check whether node observes any events
/* When compiled, a node increments this each time it discovers that it can cache more state, so that it
* knows not to recompute that state when next compiled. Since internal state is usually dependent on the
* states of higher nodes, this is reset whenever the node is attached to a new parent.
*/
this._compileMemoLevel = 0;
/* Deregister default ID
*/
if (this.attr.id) {
// SceneJS_sceneNodeMaps.removeItem(this.attr.id);
}
/* Register again by whatever ID we now have
*/
if (this.scene && this.scene.nodeMap) {
if (this.attr.id) {
this.scene.nodeMap.addItem(this.attr.id, this);
} else {
this.attr.id = this.scene.nodeMap.addItem(this);
}
}
if (cfg) {
this._createCore(cfg.coreId || cfg.resource);
this.setTags(cfg.tags || {});
if (this._init) {
this._init(cfg);
}
}
};
SceneJS._Node.prototype.constructor = SceneJS._Node;
SceneJS._Node.prototype._cores = {};
SceneJS._Node.prototype._createCore = function(coreId) {
var sceneId = this.scene.attr.id;
var sceneCores = this._cores[sceneId];
if (!sceneCores) {
sceneCores = this._cores[sceneId] = {};
}
var nodeCores = sceneCores[this.attr.type];
if (!nodeCores) {
nodeCores = sceneCores[this.attr.type] = {};
}
if (coreId) {
/* Attempt to reuse a core
*/
this.core = nodeCores[coreId];
if (this.core) {
this.core._nodeCount++;
}
} else {
coreId = SceneJS._createUUID();
}
if (!this.core) {
this.core = nodeCores[coreId] = {
_coreId: coreId,
_nodeCount : 1
};
}
// this.state = new SceneJS_State({
// core: this.core
// });
return this.core;
};
/**
* Returns the ID of this node's core
*/
SceneJS._Node.prototype.getCoreId = function() {
return this.core._coreId;
};
/**
* Backwards compatibility
*/
SceneJS._Node.prototype.getResource = SceneJS._Node.prototype.getCoreId;
SceneJS._Node.prototype._tags = {};
SceneJS._Node.prototype.setTags = function(tags) {
};
/**
* Dumps anything that was memoized on this node to reduce recompilation work
*/
SceneJS._Node.prototype._resetCompilationMemos = function() {
this._compileMemoLevel = 0;
};
/**
* Same as _resetCompilationMemos, also called on sub-nodes
*/
SceneJS._Node.prototype._resetTreeCompilationMemos = function() {
this._resetCompilationMemos();
for (var i = 0; i < this.children.length; i++) {
this.children[i]._resetTreeCompilationMemos();
}
};
SceneJS._Node.prototype._flagDirty = function() {
this._compileMemoLevel = 0;
// if (this.attr._childStates && this.attr._dirty) {
// this._flagDirtyState(this.attr);
// }
};
//SceneJS._Node.prototype._flagDirtyState = function(attr) {
// attr._dirty = true;
// if (attr._childStates) {
// for (var i = 0, len = attr._childStates.length; i < len; i++) {
// if (!attr._childStates[i]._dirty) {
// this._flagDirtyState(attr._childStates[i]);
// }
// }
// }
//};
/** @private */
SceneJS._Node.prototype._compile = function() {
this._compileNodes();
};
/** @private
*
* Recursively renders a node's child list.
*/
SceneJS._Node.prototype._compileNodes = function() { // Selected children - useful for Selector node
if (this.listeners["rendered"]) {
SceneJS_nodeEventsModule.preVisitNode(this);
}
var children = this.children; // Set of child nodes we'll be rendering
var numChildren = children.length;
var child;
var i;
if (numChildren > 0) {
for (i = 0; i < numChildren; i++) {
child = children[i];
if (SceneJS_compileModule.preVisitNode(child)) {
child._compile();
}
SceneJS_compileModule.postVisitNode(child);
}
}
if (this.listeners["rendered"]) {
SceneJS_nodeEventsModule.postVisitNode(this);
}
};
//
///**
// * Wraps _compile to fire built-in events either side of rendering.
// * @private */
//SceneJS._Node.prototype._compileWithEvents = function() {
//
// /* As scene is traversed, SceneJS_sceneStatusModule will track the counts
// * of nodes that are still initialising
// *
// * If we are listening to "loading-status" events on this node, then we'll
// * get a snapshot of those stats, then report the difference from that
// * via the event once we have rendered this node.
// */
//// var loadStatusSnapshot;
//// if (this.listeners["loading-status"]) {
//// loadStatusSnapshot = SceneJS_sceneStatusModule.getStatusSnapshot();
//// }
// this._compile();
//// if (this.listeners["loading-status"]) { // Report diff of loading stats that occurred while rending this tree
//// this._fireEvent("loading-status", SceneJS_sceneStatusModule.diffStatus(loadStatusSnapshot));
//// }
//
//};
/**
* Returns the SceneJS-assigned ID of the node.
* @returns {string} Node's ID
*/
SceneJS._Node.prototype.getID = function() {
return this.attr.id;
};
/**
* Alias for {@link #getID()} to assist resolution of the ID by JSON query API
* @returns {string} Node's ID
*/
SceneJS._Node.prototype.getId = SceneJS._Node.prototype.getID;
/**
* Returns the type ID of the node. For the Node base class, it is "node",
* which is overriden in sub-classes.
* @returns {string} Type ID
*/
SceneJS._Node.prototype.getType = function() {
return this.attr.type;
};
/**
* Returns the data object attached to this node.
* @returns {Object} data object
*/
SceneJS._Node.prototype.getData = function() {
return this.attr.data;
};
/**
* Sets a data object on this node.
* @param {Object} data Data object
*/
SceneJS._Node.prototype.setData = function(data) {
this.attr.data = data;
return this;
};
/**
* Returns the node's optional subidentifier, which must be unique within the scope
* of the parent node.
* @returns {string} Node SID
* @deprecated
*/
SceneJS._Node.prototype.getSID = function() {
return this.attr.sid;
};
/** Returns the SceneJS.Scene to which this node belongs.
* Returns node if this is a SceneJS_scene.
* @returns {SceneJS.Scene} Scene node
*/
SceneJS._Node.prototype.getScene = function() {
return this.scene;
};
/**
* Returns the number of child nodes
* @returns {int} Number of child nodes
*/
SceneJS._Node.prototype.getNumNodes = function() {
return this.children.length;
};
/** Returns child nodes
* @returns {Array} Child nodes
*/
SceneJS._Node.prototype.getNodes = function() {
var list = new Array(this.children.length);
var len = this.children.length;
for (var i = 0; i < len; i++) {
list[i] = this.children[i];
}
return list;
};
/** Returns child node at given index. Returns null if no node at that index.
* @param {Number} index The child index
* @returns {Node} Child node, or null if not found
*/
SceneJS._Node.prototype.getNodeAt = function(index) {
if (index < 0 || index >= this.children.length) {
return null;
}
return this.children[index];
};
/** Returns first child node. Returns null if no child nodes.
* @returns {Node} First child node, or null if not found
*/
SceneJS._Node.prototype.getFirstNode = function() {
if (this.children.length == 0) {
return null;
}
return this.children[0];
};
/** Returns last child node. Returns null if no child nodes.
* @returns {Node} Last child node, or null if not found
*/
SceneJS._Node.prototype.getLastNode = function() {
if (this.children.length == 0) {
return null;
}
return this.children[this.children.length - 1];
};
/** Returns child node with the given ID.
* Returns null if no such child node found.
* @param {String} sid The child's SID
* @returns {Node} Child node, or null if not found
*/
SceneJS._Node.prototype.getNode = function(id) {
for (var i = 0; i < this.children.length; i++) {
if (this.children[i].attr.id == id) {
return this.children[i];
}
}
return null;
};
/** Disconnects the child node at the given index from its parent node
* @param {int} index Child node index
* @returns {Node} The disconnected child node if located, else null
*/
SceneJS._Node.prototype.disconnectNodeAt = function(index) {
var r = this.children.splice(index, 1);
this._resetCompilationMemos();
if (r.length > 0) {
r[0].parent = null;
return r[0];
} else {
return null;
}
};
/** Disconnects the child node from its parent, given as a node object
* @param {String | Node} id The target child node, or its ID
* @returns {Node} The removed child node if located
*/
SceneJS._Node.prototype.disconnect = function() {
if (this.parent) {
for (var i = 0; i < this.parent.children.length; i++) {
if (this.parent.children[i] === this) {
return this.parent.disconnectNodeAt(i);
}
}
}
};
/** Removes the child node at the given index
* @param {int} index Child node index
*/
SceneJS._Node.prototype.removeNodeAt = function(index) {
var child = this.disconnectNodeAt(index);
if (child) {
child.destroy();
}
};
/** Removes the child node, given as either a node object or an ID string.
* @param {String | Node} id The target child node, or its ID
* @returns {Node} The removed child node if located
*/
SceneJS._Node.prototype.removeNode = function(node) {
if (!node) {
throw SceneJS_errorModule.fatalError(
SceneJS.errors.ILLEGAL_NODE_CONFIG,
"Node#removeNode - node argument undefined");
}
if (!node._compile) {
if (typeof node == "string") {
var gotNode = this.scene.nodeMap.items[node];
if (!gotNode) {
throw SceneJS_errorModule.fatalError(
SceneJS.errors.NODE_NOT_FOUND,
"Node#removeNode - node not found anywhere: '" + node + "'");
}
node = gotNode;
}
}
if (node._compile) { // instance of node
for (var i = 0; i < this.children.length; i++) {
if (this.children[i] === node) {
//this._resetCompilationMemos(); (removeNodeAt already does this)
return this.removeNodeAt(i);
}
}
}
throw SceneJS_errorModule.fatalError(
SceneJS.errors.NODE_NOT_FOUND,
"Node#removeNode - child node not found: " + (node._compile ? ": " + node.attr.id : node));
};
/** Disconnects all child nodes from their parent node and returns them in an array.
* @returns {Array[Node]} The disconnected child nodes
*/
SceneJS._Node.prototype.disconnectNodes = function() {
for (var i = 0; i < this.children.length; i++) { // Unlink children from this
this.children[i].parent = null;
}
var children = this.children;
this.children = [];
this._resetCompilationMemos();
return children;
};
/** Removes all child nodes and returns them in an array.
* @returns {Array[Node]} The removed child nodes
*/
SceneJS._Node.prototype.removeNodes = function() {
var children = this.disconnectNodes();
for (var i = 0; i < children.length; i++) {
this.children[i].destroy();
}
};
/** Destroys node and moves children up to parent, inserting them where this node resided.
* @returns {Node} The parent
*/
SceneJS._Node.prototype.splice = function() {
var i, len;
if (this.parent == null) {
return null;
}
var parent = this.parent;
var children = this.disconnectNodes();
for (i = 0, len = children.length; i < len; i++) { // Link this node's children to new parent
children[i].parent = this.parent;
}
for (i = 0, len = parent.children.length; i < len; i++) { // Replace node on parent's children with this node's children
if (parent.children[i] === this) {
parent.children.splice.apply(parent.children, [i, 1].concat(children));
this.parent = null;
this.destroy();
parent._resetTreeCompilationMemos();
SceneJS_compileModule.nodeUpdated(parent);
return parent;
}
}
};
/** Appends multiple child nodes
* @param {Array[Node]} nodes Array of nodes
* @return {Node} This node
*/
SceneJS._Node.prototype.addNodes = function(nodes) {
if (!nodes) {
throw SceneJS_errorModule.fatalError(
SceneJS.errors.ILLEGAL_NODE_CONFIG,
"Node#addNodes - nodes argument is undefined");
}
for (var i = nodes.length - 1; i >= 0; i--) {
this.addNode(nodes[i]);
}
this._resetCompilationMemos();
return this;
};
/** Appends a child node
* @param {Node} node Child node
* @return {Node} The child node
*/
SceneJS._Node.prototype.addNode = function(node) {
if (!node) {
throw SceneJS_errorModule.fatalError(
SceneJS.errors.ILLEGAL_NODE_CONFIG,
"Node#addNode - node argument is undefined");
}
if (!node._compile) {
if (typeof node == "string") {
var gotNode = this.scene.nodeMap.items[node];
if (!gotNode) {
throw SceneJS_errorModule.fatalError(
SceneJS.errors.ILLEGAL_NODE_CONFIG,
"Node#addNode - node not found: '" + node + "'");
}
node = gotNode;
} else {
node = SceneJS._parseNodeJSON(node, this.scene);
}
}
if (!node._compile) {
throw SceneJS_errorModule.fatalError(
SceneJS.errors.ILLEGAL_NODE_CONFIG,
"Node#addNode - node argument is not a Node or subclass!");
}
if (node.parent != null) {
throw SceneJS_errorModule.fatalError(
SceneJS.errors.ILLEGAL_NODE_CONFIG,
"Node#addNode - node argument is still attached to another parent!");
}
this.children.push(node);
node.parent = this;
node._resetTreeCompilationMemos();
return node;
};
/** Inserts a subgraph into child nodes
* @param {Node} node Child node
* @param {int} i Index for new child node
* @return {Node} The child node
*/
SceneJS._Node.prototype.insertNode = function(node, i) {
if (!node) {
throw SceneJS_errorModule.fatalError(
SceneJS.errors.ILLEGAL_NODE_CONFIG,
"Node#insertNode - node argument is undefined");
}
if (!node._compile) {
node = SceneJS._parseNodeJSON(node, this.scene);
}
if (!node._compile) {
throw SceneJS_errorModule.fatalError(
SceneJS.errors.ILLEGAL_NODE_CONFIG,
"Node#insertNode - node argument is not a Node or subclass!");
}
if (node.parent != null) {
throw SceneJS_errorModule.fatalError(
SceneJS.errors.ILLEGAL_NODE_CONFIG,
"Node#insertNode - node argument is still attached to another parent!");
}
if (i == undefined || i == null) {
/* Insert node above children when no index given
*/
var children = this.disconnectNodes();
/* Move children to right-most leaf of inserted graph
*/
var leaf = node;
while (leaf.getNumNodes() > 0) {
leaf = leaf.getLastNode();
}
leaf.addNodes(children);
this.addNode(node);
} else if (i < 0) {
throw SceneJS_errorModule.fatalError(
SceneJS.errors.ILLEGAL_NODE_CONFIG,
"Node#insertNode - node index out of range: -1");
} else if (i >= this.children.length) {
this.children.push(node);
} else {
this.children.splice(i, 0, node);
}
node.parent = this;
node._resetTreeCompilationMemos();
return node;
};
/** Calls the given function on each node in the subgraph rooted by this node, including this node.
* The callback takes each node as it's sole argument and traversal stops as soon as the function returns
* true and returns the node.
* @param {function(Node)} func The function
*/
SceneJS._Node.prototype.mapNodes = function(func) {
if (func(this)) {
return this;
}
var result;
for (var i = 0; i < this.children.length; i++) {
result = this.children[i].mapNodes(func);
if (result) {
return result;
}
}
return null;
};
/**
* Registers a listener for a given event on this node. If the event type
* is not supported by this node type, then the listener will never be called.
* <p><b>Example:</b>
* <pre><code>
* var node = new Node();
*
* node.addListener(
*
* // eventName
* "some-event",
*
* // handler
* function(node, // Node we are listening to
* params) { // Whatever params accompany the event type
*
* // ...
* }
* );
*
*
* </code></pre>
*
* @param {String} eventName One of the event types supported by this node
* @param {Function} fn - Handler function that be called as specified
* @param options - Optional options for the handler as specified
* @return {Node} this
*/
SceneJS._Node.prototype.addListener = function(eventName, fn, options) {
var list = this.listeners[eventName];
if (!list) {
list = [];
this.listeners[eventName] = list;
}
list.push({
eventName : eventName,
fn: fn,
options : options || {}
});
this.numListeners++;
this._resetCompilationMemos(); // Need re-render - potentially more state changes
return this;
};
/**
* Destroys this node. It is marked for destruction; when the next scene traversal begins (or the current one ends)
* it will be destroyed and removed from it's parent.
* @return {Node} this
*/
SceneJS._Node.prototype.destroy = function() {
if (!this._destroyed) {
this._destroyed = true;
this._scheduleNodeDestroy();
}
SceneJS_compileModule.nodeUpdated(this, "destroyed"); // Compile again to rebuild display
return this;
};
/** Schedule the destruction of this node
*/
SceneJS._Node.prototype._scheduleNodeDestroy = function() {
this.disconnect();
this.scene.nodeMap.removeItem(this.attr.id);
if (this.children.length > 0) {
var children = this.children.slice(0); // destruction will modify this.children
for (var i = 0; i < children.length; i++) {
children[i]._scheduleNodeDestroy();
}
}
SceneJS._destroyedNodes.push(this);
};
/**
* Performs the actual destruction of this node, calling the node's optional template destroy method
*/
SceneJS._Node.prototype._doDestroy = function() {
if (this._destroy) {
this._destroy();
}
if (--this.core._nodeCount <= 0) {
this._cores[this.scene.attr.id][this.attr.type][this.core._coreId] = null;
}
return this;
};
/**
* Fires an event at this node, immediately calling listeners registered for the event
* @param {String} eventName Event name
* @param {Object} params Event parameters
* @param {Object} options Event options
*/
SceneJS._Node.prototype._fireEvent = function(eventName, params, options) {
var list = this.listeners[eventName];
if (list) {
if (!params) {
params = {};
}
var event = {
name: eventName,
params : params,
options: options || {}
};
var listener;
for (var i = 0, len = list.length; i < len; i++) {
listener = list[i];
if (listener.options.scope) {
listener.fn.call(listener.options.scope, event);
} else {
listener.fn.call(this, event);
}
}
}
};
/**
* Removes a handler that is registered for the given event on this node.
* Does nothing if no such handler registered.
*
* @param {String} eventName Event type that handler is registered for
* @param {function} fn - Handler function that is registered for the event
* @return {function} The handler, or null if not registered
*/
SceneJS._Node.prototype.removeListener = function(eventName, fn) {
var list = this.listeners[eventName];
if (!list) {
return null;
}
for (var i = 0; i < list.length; i++) {
if (list[i].fn == fn) {
list.splice(i, 1);
return fn;
}
}
this.numListeners--;
return null;
};
/**
* Returns true if this node has any listeners for the given event .
*
* @param {String} eventName Event type
* @return {boolean} True if listener present
*/
SceneJS._Node.prototype.hasListener = function(eventName) {
return this.listeners[eventName];
};
/**
* Returns true if this node has any listeners at all.
*
* @return {boolean} True if any listener present
*/
SceneJS._Node.prototype.hasListeners = function() {
return (this.numListeners > 0);
};
/** Removes all listeners registered on this node.
* @return {Node} this
*/
SceneJS._Node.prototype.removeListeners = function() {
this.listeners = {};
this.numListeners = 0;
return this;
};
/** Returns the parent node
* @return {Node} The parent node
*/
SceneJS._Node.prototype.getParent = function() {
return this.parent;
};
/** Returns either all child or all sub-nodes of the given type, depending on whether search is recursive or not.
* @param {string} type Node type
* @param {boolean} [recursive=false] When true, will return all matching nodes in subgraph, otherwise returns just children (default)
* @return {SceneJS.node[]} Array of matching nodes
*/
SceneJS._Node.prototype.findNodesByType = function(type, recursive) {
return this._findNodesByType(type, [], recursive);
};
/** @private */
SceneJS._Node.prototype._findNodesByType = function(type, list, recursive) {
var i;
for (i = 0; i < this.children; i++) {
var node = this.children[i];
if (node.type == type) {
list.add(node);
}
}
if (recursive) {
for (i = 0; i < this.children; i++) {
this.children[i]._findNodesByType(type, list, recursive);
}
}
return list;
};
/**
* Returns an object containing the attributes that were given when creating the node. Obviously, the map will have
* the current values, plus any attributes that were later added through set/add methods on the node
*
*/
SceneJS._Node.prototype.getJSON = function() {
return this.attr;
};
var SceneJS_State = function(cfg) {
this.core = cfg.core || {};
this.state = cfg.state || {};
if (cfg.parent) {
cfg.parent.addChild(this);
}
this.children = [];
this.dirty = true;
this._cleanFunc = cfg.cleanFunc;
};
SceneJS_State.prototype.addChild = function(state) {
state.parent = this;
this.children.push(state);
};
SceneJS_State.prototype.setDirty = function() {
if (this.dirty) {
return;
}
this.dirty = true;
if (this.children.length > 0) {
var child;
for (var i = 0, len = this.children.length; i < len; i++) {
child = this.children[i];
if (!child.dirty) {
child.setDirty();
}
}
}
};
SceneJS_State.prototype._cleanStack = [];
SceneJS_State.prototype._cleanStackLen = 0;
SceneJS_State.prototype.setClean = function() {
if (!this.dirty) {
return;
}
if (!this._cleanFunc) {
return;
}
if (!this.parent) {
this._cleanFunc(this.parent ? this.parent : null, this);
this.dirty = false;
return;
}
this._cleanStackLen = 0;
/* Stack dirty states on path to root
*/
var state = this;
while (state && state.dirty) {
this._cleanStack[this._cleanStackLen++] = state;
state = state.parent;
}
/* Stack last clean state if existing
*/
if (state && state.parent) {
this._cleanStack[this._cleanStackLen++] = state.parent;
}
/* Clean states down the path
*/
var parentState;
for (var i = this._cleanStackLen - 1; i > 0; i--) {
parentState = this._cleanStack[i - 1];
state = this._cleanStack[i];
this._cleanFunc(parentState, state);
parentState.dirty = false;
state.dirty = false;
}
};
SceneJS_State.prototype.reset = function() {
this.children = [];
this.dirty = true;
};
/**
* SceneJS IOC service container
*/
SceneJS.Services = new (function() {
this.NODE_LOADER_SERVICE_ID = "node-loader";
this.GEO_LOADER_SERVICE_ID = "geo-loader";
this.MORPH_GEO_LOADER_SERVICE_ID = "morph-geo-loader";
this.COMMAND_SERVICE_ID = "command";
this._services = {};
this.addService = function(name, service) {
this._services[name] = service;
};
this.hasService = function(name) {
var service = this._services[name];
return (service != null && service != undefined);
};
this.getService = function(name) {
return this._services[name];
};
/*----------------------------------------------------
* Install stub services
*---------------------------------------------------*/
this.addService(this.NODE_LOADER_SERVICE_ID, {
/** Loads node and attaches to parent
*/
loadNode: function(parentId, nodeId) {
}
});
this.addService(this.GEO_LOADER_SERVICE_ID, {
loadGeometry: function (id, params, cb) {
throw SceneJS_errorModule.fatalError("SceneJS.Services service not installed: SceneJS.Services.GEO_LOADER_SERVICE_ID");
}
});
this.addService(this.MORPH_GEO_LOADER_SERVICE_ID, {
loadMorphGeometry: function (id, params, cb) {
throw SceneJS_errorModule.fatalError("SceneJS.Services service not installed: SceneJS.Services.MORPH_GEO_LOADER_SERVICE_ID");
}
});
})();
(function() {
var NodeSelector = function(node) {
this._targetNode = node;
this._methods = {
};
};
SceneJS._selectNode = function(node) {
if (!node.__selector) {
node.__selector = new NodeSelector(node);
}
return node.__selector;
};
/** Selects the parent of the selected node
*/
NodeSelector.prototype.parent = function() {
var parent = this._targetNode.parent;
if (!parent) {
return null;
}
return SceneJS._selectNode(parent);
};
/** Selects a child node matching given ID or index
* @param {Number|String} node Child node index or ID
*/
NodeSelector.prototype.node = function(node) {
if (node === null || node === undefined) {
throw SceneJS_errorModule.fatalError("node param 'node' is null or undefined");
}
var type = typeof node;
var nodeGot;
if (type == "number") {
nodeGot = this._targetNode.getNodeAt(node);
} else if (type == "string") {
nodeGot = this._targetNode.getNode(node);
} else {
throw SceneJS_errorModule.fatalError("node param 'node' should be either an index number or an ID string");
}
if (!nodeGot) {
throw "node not found: '" + node + "'";
}
return SceneJS._selectNode(nodeGot);
};
NodeSelector.prototype.findNode = function (nodeId) {
if (this._targetNode.attr.type != "scene") {
throw SceneJS_errorModule.fatalError("findNode attempted on node that is not a \"scene\" type: '" + this._targetNode.attr.id + "'");
}
return this._targetNode.findNode(nodeId);
};
/** Find nodes in scene that have IDs matching the given regular expression
*
*/
NodeSelector.prototype.findNodes = function (nodeIdRegex) {
if (this._targetNode.attr.type != "scene") {
throw SceneJS_errorModule.fatalError("findNode attempted on node that is not a \"scene\" type: '" + this._targetNode.attr.id + "'");
}
return this._targetNode.findNodes(nodeIdRegex);
};
/** Returns the scene to which the node belongs
*/
NodeSelector.prototype.scene = function() {
return SceneJS._selectNode(this._targetNode.scene);
};
/** Returns true if a child node matching given ID or index existis on the selected node
* @param {Number|String} node Child node index or ID
*/
NodeSelector.prototype.hasNode = function(node) {
if (node === null || typeof(node) === "undefined") {
throw SceneJS_errorModule.fatalError("hasNode param 'node' is null or undefined");
}
var type = typeof node;
var nodeGot;
if (type == "number") {
nodeGot = this._targetNode.getNodeAt(node);
} else if (type == "string") {
nodeGot = this._targetNode.getNode(node);
} else {
throw SceneJS_errorModule.fatalError("hasNode param 'node' should be either an index number or an ID string");
}
return (nodeGot != undefined && nodeGot != null);
};
/**
* Iterates over parent nodes on the path from the selected node to the root, executing a function
* for each.
* If the function returns true at any node, then traversal stops and a selector is
* returned for that node.
* @param {Function(node, index)} fn Function to execute on each instance node
* @return {Object} Selector for selected node, if any
*/
NodeSelector.prototype.eachParent = function(fn) {
if (!fn) {
throw SceneJS_errorModule.fatalError("eachParent param 'fn' is null or undefined");
}
var selector;
var count = 0;
var node = this._targetNode;
while (node.parent) {
selector = SceneJS._selectNode(node.parent);
if (fn.call(selector, count++) === true) {
return selector;
}
node = node.parent;
}
return undefined;
};
/**
* Iterates over sub-nodes of the selected node, executing a function
* for each. With the optional options object we can configure is depth-first or breadth-first order.
* If neither, then only the child nodes are iterated.
* If the function returns true at any node, then traversal stops and a selector is
* returned for that node.
* @param {Function(index, node)} fn Function to execute on each child node
* @return {Object} Selector for selected node, if any
*/
NodeSelector.prototype.eachNode = function(fn, options) {
if (!fn) {
throw SceneJS_errorModule.fatalError("eachNode param 'fn' is null or undefined");
}
if (typeof fn != "function") {
throw SceneJS_errorModule.fatalError("eachNode param 'fn' should be a function");
}
var stoppedNode;
options = options || {};
var count = 0;
if (options.andSelf) {
if (fn.call(this, count++) === true) {
return this;
}
}
if (!options.depthFirst && !options.breadthFirst) {
stoppedNode = this._iterateEachNode(fn, this._targetNode, count);
} else if (options.depthFirst) {
stoppedNode = this._iterateEachNodeDepthFirst(fn, this._targetNode, count, false); // Not below root yet
} else {
// TODO: breadth-first
}
if (stoppedNode) {
return stoppedNode;
}
return undefined; // IDE happy now
};
NodeSelector.prototype.numNodes = function() {
return this._targetNode.children.length;
};
/** Sets an attribute of the selected node
*/
NodeSelector.prototype.set = function(attr, value) {
if (!attr) {
throw SceneJS_errorModule.fatalError("set param 'attr' null or undefined");
}
if (typeof attr == "string") {
this._callNodeMethod("set", attr, value, this._targetNode);
} else {
this._callNodeMethods("set", attr, this._targetNode);
}
return this;
};
/** Adds an attribute to the selected node
*/
NodeSelector.prototype.add = function(attr, value) {
if (!attr) {
throw SceneJS_errorModule.fatalError("add param 'attr' null or undefined");
}
if (typeof attr == "string") {
this._callNodeMethod("add", attr, value, this._targetNode);
} else {
this._callNodeMethods("add", attr, this._targetNode);
}
return this;
};
/** Increments an attribute to the selected node
*/
NodeSelector.prototype.inc = function(attr, value) {
if (!attr) {
throw SceneJS_errorModule.fatalError("inc param 'attr' null or undefined");
}
if (typeof attr == "string") {
this._callNodeMethod("inc", attr, value, this._targetNode);
} else {
this._callNodeMethods("inc", attr, this._targetNode);
}
return this;
};
/** Inserts an attribute or child node into the selected node
*/
NodeSelector.prototype.insert = function(attr, value) {
if (!attr) {
throw SceneJS_errorModule.fatalError("insert param 'attr' null or undefined");
}
if (typeof attr == "string") {
this._callNodeMethod("insert", attr, value, this._targetNode);
} else {
this._callNodeMethods("insert", attr, this._targetNode);
}
return this;
};
/** Removes an attribute from the selected node
*/
NodeSelector.prototype.remove = function(attr, value) {
if (!attr) {
throw SceneJS_errorModule.fatalError("remove param 'attr' null or undefined");
}
if (typeof attr == "string") {
this._callNodeMethod("remove", attr, value, this._targetNode);
} else {
this._callNodeMethods("remove", attr, this._targetNode);
}
return this;
};
/** Returns the value of an attribute of the selected node
*/
NodeSelector.prototype.get = function(attr) {
if (!attr) {
return this._targetNode.getJSON();
}
var funcName = "get" + attr.substr(0, 1).toUpperCase() + attr.substr(1);
var func = this._targetNode[funcName];
if (!func) {
throw SceneJS_errorModule.fatalError("Attribute '" + attr + "' not found on node '" + this._targetNode.attr.id + "'");
}
return func.call(this._targetNode);
};
/** Binds a listener to an event on the selected node
*
* @param {String} name Event name
* @param {Function} handler Event handler
*/
NodeSelector.prototype.bind = function(name, handler) {
if (!name) {
throw SceneJS_errorModule.fatalError("bind param 'name' null or undefined");
}
if (typeof name != "string") {
throw SceneJS_errorModule.fatalError("bind param 'name' should be a string");
}
if (!handler) {
throw SceneJS_errorModule.fatalError("bind param 'handler' null or undefined");
}
if (typeof handler != "function") {
throw SceneJS_errorModule.fatalError("bind param 'handler' should be a function");
} else {
this._targetNode.addListener(name, handler, { scope: this });
SceneJS_compileModule.nodeUpdated(this._targetNode, "bind", name);
}
//else {
// var commandService = SceneJS.Services.getService(SceneJS.Services.COMMAND_SERVICE_ID);
// if (!handler.target) {
// handler.target = this._targetNode.attr.id;
// }
// this._targetNode.addListener(
// name,
// function(params) {
// commandService.executeCommand(handler);
// }, { scope: this });
// }
return this;
};
/** Unbinds a listener for an event on the selected node
*
* @param {String} name Event name
* @param {Function} handler Event handler
*/
NodeSelector.prototype.unbind = function(name, handler) {
if (!name) {
throw SceneJS_errorModule.fatalError("bind param 'name' null or undefined");
}
if (typeof name != "string") {
throw SceneJS_errorModule.fatalError("bind param 'name' should be a string");
}
if (!handler) {
throw SceneJS_errorModule.fatalError("bind param 'handler' null or undefined");
}
if (typeof handler != "function") {
throw SceneJS