UNPKG

cannon

Version:

A lightweight 3D physics engine written in JavaScript.

1,612 lines (1,456 loc) 810 kB
/* * 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