'use strict';
/**
* The main scripting engine.
* @class
* @param {wcPlay~Options} [options] - Custom options.
*/
function wcPlay(options) {
var lib = null;
/**
* Contains a library of node types. Accessible via {@link wcPlay#nodeLibrary}. This
* is used by the editor tool to determine what nodes a user is allowed to use
* within a script. By default, this contains all nodes loaded by the browser. After
* updating this library, you will need to re-load the script within the editor
* to update the palette.
* @class wcNodeLibrary
*/
function wcNodeLibrary() {}
wcNodeLibrary.prototype = {
/**
* Retrieves the library list of nodes.
* @function wcNodeLibrary#get
* @returns {wcNode[]} - The node list.
*/
get: function() {
if (lib === null) {
this.all();
}
return lib;
},
/**
* Adds all node types to the library.
* @function wcNodeLibrary#all
*/
all: function() {
lib = [];
for (var i = 0; i < wcPlay.NODE_LIBRARY.length; ++i) {
lib.push(wcPlay.NODE_LIBRARY[i]);
}
},
/**
* Clears all nodes from the library.
* @function wcNodeLibrary#clear
*/
clear: function() {
lib = [];
},
/**
* Adds a node of a given class name to the library.
* @function wcNodeLibrary#add
* @param {string} nodeName - The node to add.
* @returns {boolean} - True if the node was added.
*/
add: function(nodeName) {
if (lib === null) {
this.all();
}
for (var i = 0; i < wcPlay.NODE_LIBRARY.length; ++i) {
if (wcPlay.NODE_LIBRARY[i].className === nodeName) {
if (lib.indexOf(wcPlay.NODE_LIBRARY[i]) === -1) {
lib.push(wcPlay.NODE_LIBRARY[i]);
}
return true;
}
}
return false;
},
/**
* Remove a node of a given class name from the library
* @function wcNodeLibrary#remove
* @param {string} nodeName - The node to remove.
* @returns {boolean} - True if the node was removed.
*/
remove: function(nodeName) {
if (lib === null) {
this.all();
}
for (var i = 0; i < lib.length; ++i) {
if (lib[i].className === nodeName) {
lib.splice(i, 1);
return true;
}
}
return false;
}
};
this._nodeLibrary = new wcNodeLibrary();
this._nodeLibrary.all();
this._compositeNodes = [];
this._entryNodes = [];
this._processNodes = [];
this._storageNodes = [];
this._properties = [];
this._waitingChain = [];
this._queuedChain = [];
this._queuedProperties = [];
this._importedScripts = [];
this._nodeId = 0;
this._flowTrackers = 0;
this._hasWarnedTrackLimit = false;
this._updateInterval = 0;
this._isRunning = false;
this._isPaused = false;
this._isPausing = false;
this._isStepping = false;
this._customData = null;
this._editors = [];
// Setup our options.
this._options = {
silent: false,
updateRate: 25,
updateLimit: 100,
flowTrackerLimit: 1000,
debugging: true
};
for (var prop in options) {
this._options[prop] = options[prop];
}
if (!this._updateInterval) {
var self = this;
this._updateInterval = setInterval(function() {
self.update();
}, this._options.updateRate);
}
wcPlay.INSTANCE_LIBRARY.push(this);
}
/**
* Determines how a property's control should be rendered within the editor view.
* @enum {string}
*/
wcPlay.PROPERTY = {
/** Displays the property as a string, but does not enforce or convert its type. [String options]{@link wcNode~StringOptions} are used. */
DYNAMIC: 'dynamic',
/** Displays the property as a checkbox. [Default options]{@link wcNode~PropertyOptions} are used. */
TOGGLE: 'toggle',
/** Displays the property as a number control. [Number options]{@link wcNode~NumberOptions} are used. */
NUMBER: 'number',
/** Displays the property as a text field. [String options]{@link wcNode~StringOptions} are used. */
STRING: 'string',
/** Displays the property as a combo box control. [Select options]{@link wcNode~SelectOptions} are used. */
SELECT: 'select',
/** Displays the property as a custom control. [Custom options]{@link wcNode~CustomOptions} are used. */
CUSTOM: 'custom'
};
/**
* The different types of nodes.
* @enum {string}
*/
wcPlay.NODE = {
/** Entry nodes mark the beginning of an execution flow chain and are usually triggered by some type of event that happens outside of the script. */
ENTRY: 'entry',
/** Process nodes perform a process, often very simple, and make up the bulk of a flow chain within the script. */
PROCESS: 'process',
/** Storage nodes are designed with a single purpose of storing data for use within the script. */
STORAGE: 'storage',
/** Composite nodes are a group of nodes combined into a single visible node. They appear in the composite section of the node palette for easy duplication within your script. */
COMPOSITE: 'composite'
};
/**
* A global list of nodes that exist. All node types must add themselves into this list when they are coded.
* @member
*/
wcPlay.NODE_LIBRARY = [];
/**
* A global list of play engine instances that exist.
* @member
*/
wcPlay.INSTANCE_LIBRARY = [];
/**
* A global function that registers a new node type into the library. This is called automatically when a new extended node type is defined, you should not have to do this manually.
* @param {string} name - The name of the node constructor.
* @param {string} displayName - The display name.
* @param {string} category - The display category name.
* @param {wcPlay.NODE} nodeType - The node's type.
* @returns {boolean} - Success or failure.
*/
wcPlay.registerNodeType = function(name, displayName, category, nodeType) {
var data = {
className: name,
displayName: displayName,
category: category,
nodeType: nodeType
};
for (var i = 0; i < wcPlay.NODE_LIBRARY.length; ++i) {
if (wcPlay.NODE_LIBRARY[i].className === name) {
wcPlay.NODE_LIBRARY[i] = data;
for (var a = 0; a < wcPlay.INSTANCE_LIBRARY.length; ++a) {
var play = wcPlay.INSTANCE_LIBRARY[a];
for (var b = 0; b < play._editors.length; ++b) {
var editor = play._editors[b];
editor.__setupPalette();
}
}
return true;
}
}
wcPlay.NODE_LIBRARY.push(data);
return true;
};
/**
* A global function that unregisters a node type from the library.
* @param {string} name - The name of the node constructor.
* @returns {boolean} - True if the node type has been found and removed.
*/
wcPlay.unregisterNodeType = function(name) {
for (var i = 0; i < wcPlay.NODE_LIBRARY.length; ++i) {
if (wcPlay.NODE_LIBRARY[i].className === name) {
wcPlay.NODE_LIBRARY.splice(i, 1);
for (var a = 0; a < wcPlay.INSTANCE_LIBRARY.length; ++a) {
var play = wcPlay.INSTANCE_LIBRARY[a];
for (var b = 0; b < play._editors.length; ++b) {
var editor = play._editors[b];
editor.__setupPalette();
}
}
return true;
}
}
return false;
};
wcPlay.prototype = {
/**
* Retrieves the node library for this script. This is an object that allows
* you to manage the nodes you are allowed to use in this script.
* @function wcPlay#nodeLibrary
* @returns {wcNodeLibrary} - The node library list.
*/
nodeLibrary: function() {
return this._nodeLibrary;
},
/**
* Initializes the script and begins the update process.
* @function wcPlay#start
*/
start: function() {
this.reset();
this._isRunning = true;
this._isPaused = false;
this._isPausing = false;
this._isStepping = false;
this.notifyNodes('onStart', []);
},
/**
* Stops the script.
* @function wcPlay#stop
*/
stop: function() {
this._isRunning = false;
this.notifyNodes('onStop', []);
},
/**
* Retrieves whether the script engine is running.
* @function wcPlay#isRunning
* @returns {boolean} - Whether the script engine is running.
*/
isRunning: function() {
return this._isRunning;
},
/**
* Resets all volotile data in the script.
* @function wcPlay#reset
*/
reset: function() {
var i = 0;
for (i = 0; i < this._compositeNodes.length; ++i) {
this._compositeNodes[i].reset();
}
for (i = 0; i < this._entryNodes.length; ++i) {
this._entryNodes[i].reset();
}
for (i = 0; i < this._processNodes.length; ++i) {
this._processNodes[i].reset();
}
for (i = 0; i < this._storageNodes.length; ++i) {
this._storageNodes[i].reset();
}
this._queuedChain = [];
this._queuedProperties = [];
for (i = 0; i < this._properties.length; ++i) {
this.property(this._properties[i].name, this._properties[i].initialValue, true);
}
},
/**
* Clears all nodes from the script.
* @function wcPlay#clear
*/
clear: function() {
this._queuedChain = [];
this._queuedProperties = [];
this._waitingChain = [];
this._properties = [];
while (this._compositeNodes.length) {
this._compositeNodes[0].destroy();
}
while (this._entryNodes.length) {
this._entryNodes[0].destroy();
}
while (this._processNodes.length) {
this._processNodes[0].destroy();
}
while (this._storageNodes.length) {
this._storageNodes[0].destroy();
}
},
/**
* Serializes the script into a string that can be saved into a file and [restored]{@link wcPlay#load}.
* @function wcPlay#save
* @returns {string} - A serialized string with the entire script.
*/
save: function() {
var data = {
version: '1.0.0'
};
data.custom = this.customData();
data.properties = this.listProperties();
var i = 0;
data.nodes = [];
for (i = 0; i < this._compositeNodes.length; ++i) {
data.nodes.push(this._compositeNodes[i].export(true));
}
for (i = 0; i < this._entryNodes.length; ++i) {
data.nodes.push(this._entryNodes[i].export(true));
}
for (i = 0; i < this._processNodes.length; ++i) {
data.nodes.push(this._processNodes[i].export(true));
}
for (i = 0; i < this._storageNodes.length; ++i) {
data.nodes.push(this._storageNodes[i].export(true));
}
return JSON.stringify(data, function(key, value) {
if (value == Infinity) {
return 'Infinity';
}
return value;
});
},
/**
* Loads a script from previously serialized data generated by [save]{@link wcPlay#save}.
* @function wcPlay#load
* @param {string} serialData - The serialized data to load.
* @returns {boolean} - Success or failure.
*/
load: function(serialData) {
// For safety in case the imported file does not load property, save the current state of the script so we can restore it if necessary.
var saveData = this.save();
var i = 0;
try {
var data = JSON.parse(serialData, function(key, value) {
if (value === 'Infinity') {
return Infinity;
}
return value;
});
this.customData(data.custom);
this.clear();
for (i = 0; i < data.properties.length; ++i) {
this.createProperty(data.properties[i].name, data.properties[i].type, data.properties[i].initialValue, data.properties[i].options);
}
// First pass, create all nodes.
var nodes = [];
for (i = 0; i < data.nodes.length; ++i) {
if (window.wcPlayNodes[data.nodes[i].className]) {
try {
var newNode = new window.wcPlayNodes[data.nodes[i].className](this, data.nodes[i].pos, data.nodes[i].name);
newNode.id = data.nodes[i].id;
nodes.push({
node: newNode,
data: data.nodes[i]
});
} catch (e) {
this.error('Attempted to load node "' + data.nodes[i].className + '" with error :' + e);
}
} else {
this.error('Attempted to load node "' + data.nodes[i].className + '", but the constructor could not be found!');
}
}
// Second pass, import each node's serialized data.
for (i = 0; i < nodes.length; ++i) {
nodes[i].node.import(nodes[i].data);
}
this.reset();
return true;
} catch (err) {
// Something went wrong, restore the previous script.
this.error(err.stack);
this.load(saveData);
}
return false;
},
/**
* Gets, or Sets, a custom data object to the script that will be saved and restored with the output file data.
* <br>NOTE: Binding new data will always replace any data that may have been previously bound.
* @function wcPlay#customData
* @param {Object|Function} [data] - If supplied, will assign a new custom data. If you supply a function, it will be invoked when retrieving the data. If not supplied, will retrieve the currently bound data.
* @returns {Object} - The current custom data object.
*/
customData: function(data) {
if (typeof data !== 'undefined') {
this._customData = data;
}
if (typeof this._customData === 'function') {
return this._customData();
}
return this._customData;
},
/**
* Imports a script as a new composite node that can be retrieved with {@link wcPlay#importedComposites}.
* @function wcPlay#import
* @param {string} serialData - The serialized data to import.
* @param {string} name - The name of the composite node to create.
* @returns {boolean} - Whether the new composite node was created.
*/
import: function(serialData, name) {
var newNode = null;
try {
var data = JSON.parse(serialData, function(key, value) {
if (value === 'Infinity') {
return Infinity;
}
return value;
});
// TODO: Ignore properties on the script?
data.pos = {x: 0, y: 0};
newNode = new wcPlayNodes.wcNodeCompositeScript(this, data.pos);
newNode.nodeType = wcPlay.NODE.COMPOSITE;
newNode.category = 'Imported';
data.id = newNode.id;
data.name = name;
data.color = newNode.color;
data.collapsed = false;
data.breakpoint = false;
data.properties = [];
data.entryChains = [];
data.exitChains = [];
data.inputChains = [];
data.outputChains = [];
newNode.import(data, []);
newNode._parent = null;
this.__removeNode(newNode);
this._importedScripts.push(newNode);
return true;
} catch (err) {
this.error(err.stack);
if (newNode) {
newNode.destroy();
}
}
return false;
},
/**
* Retrieves the list of all imported composite nodes.
* @function wcPlay#importedComposites
* @returns {wcNodeCompositeScript[]} - An array of imported composite nodes.
* @see wcPlay#importComposite
*/
importedComposites: function() {
return this._importedScripts;
},
/**
* Update handler.
* @function wcPlay#update
*/
update: function() {
// Skip updates on pause.
if (this._isPaused) {
return;
}
var item = null;
var count = Math.min(this._queuedProperties.length, this._options.updateLimit);
if (this._isRunning && !count && this._waitingChain.length) {
// If no properties are queued, but we have waiting flow nodes, add them to the queue for next update.
for (var i = 0; i < this._waitingChain.length; ++i) {
item = this._waitingChain[i];
this.queueNodeEntry(item.node, item.name, item.fromNode, item.fromName, true, item.tracker);
}
this._waitingChain = [];
return;
}
// Update a queued properties if any
var index = count;
while (index) {
index--;
item = this._queuedProperties.shift();
item.node._meta.flash = true;
if (item.node._meta.broken > 0) {
item.node._meta.broken--;
}
item.node.property(item.name, item.value, (item.upstream? false: undefined), item.upstream);
}
// Update a queued node entry only if there are no more properties to update.
if (this._isRunning && !count) {
count = Math.min(this._queuedChain.length, this._options.updateLimit - count);
index = count;
while (index) {
index--;
item = this._queuedChain.shift();
item.node._meta.flash = true;
if (item.node._meta.broken > 0) {
item.node._meta.broken--;
}
item.node._activeTracker = item.tracker;
item.node.onActivated(item.name);
item.node._activeTracker = null;
}
}
// If we are step debugging, pause the script here.
if (this._isStepping) {
this._isPausing = true;
}
},
/**
* Retrieves a node from a given ID, if it exists in this script.
* @function wcPlay#nodeById
* @param {number} id - The ID of the node.
* @returns {wcNode|null} - Either the found node, or null.
*/
nodeById: function(id) {
var i = 0;
for (i = 0; i < this._compositeNodes.length; ++i) {
if (this._compositeNodes[i].id === id) {
return this._compositeNodes[i];
}
}
for (i = 0; i < this._entryNodes.length; ++i) {
if (this._entryNodes[i].id === id) {
return this._entryNodes[i];
}
}
for (i = 0; i < this._processNodes.length; ++i) {
if (this._processNodes[i].id === id) {
return this._processNodes[i];
}
}
for (i = 0; i < this._storageNodes.length; ++i) {
if (this._storageNodes[i].id === id) {
return this._storageNodes[i];
}
}
for (i = 0; i < this._compositeNodes.length; ++i) {
if (this._compositeNodes[i].instanceOf('wcNodeCompositeScript')) {
var found = this._compositeNodes[i].nodeById(id);
if (found) {
return found;
}
}
}
return null;
},
/**
* Retrieves a list of nodes that match a given class name, if they exists in this script.
* @function wcPlay#nodesByClassName
* @param {string} className - The className of the nodes to retrieve.
* @returns {wcNode[]} - A list of all found nodes.
*/
nodesByClassName: function(className) {
var i = 0;
var result = [];
for (i = 0; i < this._compositeNodes.length; ++i) {
if (this._compositeNodes[i].className === className) {
result.push(this._compositeNodes[i]);
}
}
for (i = 0; i < this._entryNodes.length; ++i) {
if (this._entryNodes[i].className === className) {
result.push(this._entryNodes[i]);
}
}
for (i = 0; i < this._processNodes.length; ++i) {
if (this._processNodes[i].className === className) {
result.push(this._processNodes[i]);
}
}
for (i = 0; i < this._storageNodes.length; ++i) {
if (this._storageNodes[i].className === className) {
result.push(this._storageNodes[i]);
}
}
for (i = 0; i < this._compositeNodes.length; ++i) {
if (this._compositeNodes[i].instanceOf('wcNodeCompositeScript')) {
var found = this._compositeNodes[i].nodesByClassName(className);
if (found.length) {
result = result.concat(found);
}
}
}
return result;
},
/**
* Retrieves a list of nodes that match a given search filter.
* @function wcPlay#nodesBySearch
* @param {string} search - The search value.
* @returns {wcNode[]} - A list of all found nodes.
*/
nodesBySearch: function(search) {
var i = 0;
var result = [];
for (i = 0; i < this._compositeNodes.length; ++i) {
if (this._compositeNodes[i].search(search)) {
result.push(this._compositeNodes[i]);
}
}
for (i = 0; i < this._entryNodes.length; ++i) {
if (this._entryNodes[i].search(search)) {
result.push(this._entryNodes[i]);
}
}
for (i = 0; i < this._processNodes.length; ++i) {
if (this._processNodes[i].search(search)) {
result.push(this._processNodes[i]);
}
}
for (i = 0; i < this._storageNodes.length; ++i) {
if (this._storageNodes[i].search(search)) {
result.push(this._storageNodes[i]);
}
}
for (i = 0; i < this._compositeNodes.length; ++i) {
if (this._compositeNodes[i].instanceOf('wcNodeCompositeScript')) {
var found = this._compositeNodes[i].nodesBySearch(search);
if (found.length) {
result = result.concat(found);
}
}
}
return result;
},
/**
* Gets, or Sets whether the script is running in [silent mode]{@link wcPlay~Options}.
* @function wcPlay#silent
* @param {boolean} silent - If supplied, assigns a new silent state of the script.
* @returns {boolean} - The current silent state of the script.
*/
silent: function(silent) {
if (silent !== undefined) {
this._options.silent = silent? true: false;
}
return this._options.silent;
},
/**
* Gets, or Sets the debugging state of the script.
* @function wcPlay#debugging
* @param {boolean} [debug] - If supplied, will assign the debugging state of the script.
* @returns {boolean} - The current debugging state of the script.
*/
debugging: function(debug) {
if (debug !== undefined) {
this._options.debugging = debug? true: false;
}
return this._options.debugging;
},
/**
* Gets, or Sets the pause state of the script.
* @function wcPlay#paused
* @param {boolean} [paused] - If supplied, will assign the paused state of the script.
* @returns {boolean} - The current pause state of the script.
*/
paused: function(paused) {
if (paused !== undefined) {
paused = paused? true: false;
var i = 0;
if (this._isPaused !== paused) {
for (i = 0; i < this._compositeNodes.length; ++i) {
this._compositeNodes[i].paused(paused);
}
for (i = 0; i < this._entryNodes.length; ++i) {
this._entryNodes[i].paused(paused);
}
for (i = 0; i < this._processNodes.length; ++i) {
this._processNodes[i].paused(paused);
}
for (i = 0; i < this._storageNodes.length; ++i) {
this._storageNodes[i].paused(paused);
}
this._isPaused = paused;
this._isPausing = false;
}
}
return this._isPaused;
},
/**
* Gets, or Sets the stepping state of the script.
* @function wcPlay#stepping
* @param {boolean} [stepping] - If supplied, will assign the stepping state of the script.
* @returns {boolean} - The current stepping state of the script.
*/
stepping: function(stepping) {
if (stepping !== undefined) {
this._isStepping = stepping? true: false;
}
return this._isStepping;
},
/**
* Outputs a log message.
* @function wcPlay#log
* @param {...string} args - The log messages.
*/
log: function(args) {
/* eslint-disable no-console */
args = Array.prototype.slice.call(arguments);
args.splice(0, 0, 'wcPlay:');
console.log.apply(console, args);
/* eslint-enable no-console */
},
/**
* Outputs an error message.
* @function wcPlay#error
* @param {...string} args - The log messages.
*/
error: function(args) {
/* eslint-disable no-console */
args = Array.prototype.slice.call(arguments);
args.splice(0, 0, 'wcPlay ERROR:');
if (console.error) {
console.error.apply(console, args);
} else {
console.log.apply(console, args);
}
/* eslint-enable no-console */
},
/**
* Creates a new global property (can be used with the global storage node).
* @function wcPlay#createProperty
* @param {string} name - The name of the property.
* @param {wcPlay.PROPERTY} type - The type of property.
* @param {Object} [initialValue] - A default value for this property.
* @param {Object} [options] - Additional options for this property, see {@link wcPlay.PROPERTY}.
* @returns {boolean} - Fails if the property does not exist.
*/
createProperty: function(name, type, initialValue, options) {
// Make sure this property doesn't already exist.
for (var i = 0; i < this._properties.length; ++i) {
if (this._properties[i].name === name) {
return false;
}
}
if (initialValue === undefined) {
initialValue = 0;
}
this._properties.push({
name: name,
value: initialValue,
initialValue: initialValue,
type: type,
options: options || {}
});
return true;
},
/**
* Renames an existing global property.
* @function wcPlay#renameProperty
* @param {string} name - The current name of the global property to rename.
* @param {string} newName - The new desired name of the global property.
* @returns {boolean} - Fails if the property was not found or if the new name is already used.
*/
renameProperty: function(name, newName) {
var prop = null;
for (var i = 0; i < this._properties.length; ++i) {
if (this._properties[i].name === newName) {
return false;
}
if (this._properties[i].name === name) {
prop = this._properties[i];
}
}
if (!prop) {
return false;
}
prop.name = newName;
this.notifyNodes('onGlobalPropertyRenamed', [name, newName]);
},
/**
* Removes a global property.
* @function wcPlay#removeProperty
* @param {string} name - The name of the property to remove.
* @returns {boolean} - Fails if the property does not exist.
*/
removeProperty: function(name) {
for (var i = 0; i < this._properties.length; ++i) {
if (this._properties[i].name === name) {
this.notifyNodes('onGlobalPropertyRemoved', [name]);
this._properties.splice(i, 1);
return true;
}
}
return false;
},
/**
* Gets, or Sets a global property value.
* @function wcPlay#property
* @param {string} name - The name of the property.
* @param {Object} [value] - If supplied, will assign a new value to the property.
* @returns {Object} - The current value of the property, or undefined if not found.
*/
property: function(name, value) {
var prop = null;
for (var i = 0; i < this._properties.length; ++i) {
if (this._properties[i].name === name) {
prop = this._properties[i];
break;
}
}
if (!prop) {
return;
}
if (value !== undefined && value !== prop.value) {
var oldValue = prop.value;
prop.value = value;
this.notifyNodes('onGlobalPropertyChanged', [prop.name, oldValue, prop.value]);
}
return prop.value;
},
/**
* Gets, or Sets a global property initial value.
* @function wcPlay#initialProperty
* @param {string} name - The name of the property.
* @param {Object} [value] - If supplied, will assign a new value to the property.
* @returns {Object} - The current value of the property, or undefined if not found.
*/
initialProperty: function(name, value) {
var prop = null;
for (var i = 0; i < this._properties.length; ++i) {
if (this._properties[i].name === name) {
prop = this._properties[i];
break;
}
}
if (!prop) {
return;
}
if (value !== undefined && value !== prop.initialValue) {
var oldValue = prop.initialValue;
prop.initialValue = value;
this.notifyNodes('onGlobalInitialPropertyChanged', [prop.name, oldValue, prop.initialValue]);
if (prop.value == oldValue) {
prop.value = value;
this.notifyNodes('onGlobalPropertyChanged', [prop.name, oldValue, prop.value]);
}
}
return prop.initialValue;
},
/**
* Retrieves a list of all global properties and their values for this script.
* @function wcPlay#listProperties
* @returns {wcNode~PropertyData[]} - A list of all property data.
*/
listProperties: function() {
var result = [];
for (var i = 0; i < this._properties.length; ++i) {
var myProp = this._properties[i];
result.push({
name: myProp.name,
type: myProp.type,
value: myProp.value,
initialValue: myProp.initialValue,
options: myProp.options
});
}
return result;
},
/**
* Triggers an event into the Play script.
* @function wcPlay#triggerEvent
* @param {string} type - The type name of the node (as displayed in the title).
* @param {wcPlay~TriggerEventOptions} [options] - Optional parameters.
*/
triggerEvent: function(type, options) {
options = options || {};
var activeTracker = null;
if (typeof options.done === 'function') {
activeTracker = this.beginFlowTracker({}, null, options.done);
}
for (var i = 0; i < this._entryNodes.length; ++i) {
var node = this._entryNodes[i];
if (node.type === type && (!options.hasOwnProperty('name') || node.name === options.name)) {
node._activeTracker = activeTracker;
node.onActivated(options.data);
node._activeTracker = null;
}
}
var self = this;
setTimeout(function() {
self.endFlowTracker(activeTracker);
}, 0);
},
/**
* Sends a custom notification event to all nodes.
* @function wcPlay#notifyNodes
* @param {string} func - The node function to call.
* @param {Object[]} args - A list of arguments to forward into the function call.
* @param {boolean} [includeEditorPalette] - If true, will also notify all nodes generated for use with editor palette views.
*/
notifyNodes: function(func, args, includeEditorPalette) {
var i = 0;
var self = null;
for (i = 0; i < this._compositeNodes.length; ++i) {
self = this._compositeNodes[i];
if (typeof self[func] === 'function') {
self[func].apply(self, args);
}
}
for (i = 0; i < this._entryNodes.length; ++i) {
self = this._entryNodes[i];
if (typeof self[func] === 'function') {
self[func].apply(self, args);
}
}
for (i = 0; i < this._processNodes.length; ++i) {
self = this._processNodes[i];
if (typeof self[func] === 'function') {
self[func].apply(self, args);
}
}
for (i = 0; i < this._storageNodes.length; ++i) {
self = this._storageNodes[i];
if (typeof self[func] === 'function') {
self[func].apply(self, args);
}
}
for (i = 0; i < this._compositeNodes.length; ++i) {
if (this._compositeNodes[i].instanceOf('wcNodeCompositeScript')) {
this._compositeNodes[i].notifyNodes(func, args);
}
}
if (includeEditorPalette) {
this.notifyEditors('notifyPaletteNodes', [func, args]);
}
},
/**
* Sends a custom notification event to all renderers.
* @function wcPlay#notifyEditors
* @param {string} func - The renderer function to call.
* @param {Object[]} args - A list of arguments to forward into the function call.
*/
notifyEditors: function(func, args) {
var self = null;
for (var i = 0; i < this._editors.length; ++i) {
self = this._editors[i];
if (typeof self[func] === 'function') {
self[func].apply(self, args);
}
}
},
/**
* Queues a node entry link to trigger on the next update.
* @function wcPlay#queueNodeEntry
* @param {wcNode} node - The node being queued.
* @param {string} name - The entry link name.
* @param {wcNode} fromNode - The node causing the queue.
* @param {string} fromName - The exit link name.
* @param {boolean} [forceQueue] - If true, will force the event into the queue rather than the waiting list.
* @param {wcPlay~FlowTracker} [tracker] - Optional flow tracker.
*/
queueNodeEntry: function(node, name, fromNode, fromName, forceQueue, tracker) {
// Skip node queueing if the script is not even running.
if (!this._isRunning) {
return;
}
if (!forceQueue && this._isStepping && this._isPaused) {
if (node.enabled()) {
this._waitingChain.push({
node: node,
name: name,
fromNode: fromNode,
fromName: fromName,
tracker: tracker
});
}
return;
}
if (node.enabled()) {
this._queuedChain.push({
node: node,
name: name,
tracker: tracker
});
if (node.debugBreak() || this._isStepping) {
node._meta.flash = true;
node._meta.broken++;
this._isPausing = true;
}
// Flash the entry link.
var i = 0;
for (i = 0; i < node.chain.entry.length; ++i) {
if (node.chain.entry[i].name == name) {
node.chain.entry[i].meta.flash = true;
if (node.debugBreak() || this._isStepping) {
node.chain.entry[i].meta.broken++;
}
break;
}
}
// Flash the exit link.
if (fromNode && fromName) {
for (i = 0; i < fromNode.chain.exit.length; ++i) {
var exitLink = fromNode.chain.exit[i];
if (exitLink.name == fromName) {
fromNode.chain.exit[i].meta.flash = true;
fromNode._meta.flash = true;
if (node.debugBreak() || this._isStepping) {
fromNode.chain.exit[i].meta.broken++;
}
break;
}
}
}
if (this._isPausing) {
this.paused(true);
this._isPausing = false;
}
} else {
this.endFlowTracker(tracker);
}
},
/**
* Queues a node property value change to trigger on the next update.
* @function wcPlay#queueNodeProperty
* @param {wcNode} node - The node being queued.
* @param {string} name - The property name.
* @param {Object} value - The property value.
* @param {boolean} [upstream] - If true, we are propagating the property change in reverse.
*/
queueNodeProperty: function(node, name, value, upstream) {
// Skip node queueing if the script is not even running.
if (!this._isRunning) {
return;
}
if (node.enabled() || name === 'enabled') {
this._queuedProperties.push({
node: node,
name: name,
value: value,
upstream: upstream
});
// if (this._queuedChain.length > 0) {
// this._waitingChain = this._queuedChain;
// this._queuedChain = [];
// }
if (node.debugBreak()) {
node._meta.flash = true;
}
if (this._isStepping) {
node._meta.flash = true;
node._meta.broken++;
this._isPausing = true;
}
if (this._isPausing) {
this.paused(true);
this._isPausing = false;
}
}
},
/**
* Creates a chain tracker for a callback method.
* @function wcPlay#beginFlowTracker
* @param {wcNode} node - The node invoking this tracker.
* @param {wcPlay~FlowTracker} [parent] - Parent tracker object.
* @param {Function} [callback] - Optional callback handler to call when this tracked chain has finished.
* @returns {wcPlay~FlowTracker} - A chain tracker object.
*/
beginFlowTracker: function(node, parent, callback) {
// No need to track if we have nothing listening.
if (!parent && !callback) {
return null;
}
if (this._flowTrackers >= this._options.flowTrackerLimit) {
if (!this._hasWarnedTrackLimit) {
this._hasWarnedTrackLimit = true;
if (this._editors.length) {
alert('Flow Trackers have exceeded the limit, please ensure that you are not creating an infinite flow loop.\n\nThe chain will be forced to stop.\n\nThis message will only appear once.');
}
}
this.error('Flow Trackers have exceeded the limit, please ensure that you are not creating an infinite flow loop. The chain will be forced to stop.');
this.endFlowTracker(parent);
return null;
}
var tracker = {
node: node,
parent: parent,
callback: callback,
children: []
};
this._flowTrackers++;
if (parent) {
parent.children.push(tracker);
}
return tracker;
},
/**
* Finishes a chain tracker.
* @function wcPlay#endFlowTracker
* @param {wcPlay~FlowTracker} tracker - The tracker to finish.
*/
endFlowTracker: function(tracker) {
// Ignore if there is no tracker, or the tracker is dead.
if (!tracker || tracker.dead) {
return;
}
// Cannot end a tracker that still has children.
if (tracker.children.length) {
return;
}
this._flowTrackers--;
if (this._flowTrackers < 0) {
this.error('Flow tracker count reduced below zero!');
}
// Kill this tracker, in case anything else is still referencing it.
tracker.dead = true;
// Call any callbacks on this finished flow tracker.
if (tracker.callback) {
tracker.node._activeTracker = tracker.parent;
tracker.callback.call(tracker.node);
tracker.node._activeTracker = null;
}
// Now remove this tracker from its parent, if able.
if (tracker.parent) {
var index = tracker.parent.children.indexOf(tracker);
if (index > -1) {
tracker.parent.children.splice(index, 1);
}
// If there are no more children to track for this parent,
// we can end this parent as well.
if (tracker.parent.children.length === 0) {
this.endFlowTracker(tracker.parent);
}
}
},
/**
* Destroys this instance.
* @function wcPlay#destroy
*/
destroy: function() {
this.clear();
this._importedScripts = [];
while (this._editors.length) {
this._editors[0].engine(null);
}
if (this._updateInterval) {
clearInterval(this._updateInterval);
}
var index = wcPlay.INSTANCE_LIBRARY.indexOf(this);
if (index > -1) {
wcPlay.INSTANCE_LIBRARY.splice(index, 1);
}
},
/**
* Retrieves the class name of this object.
* @function wcPlay#isA
* @param {string} name - The class name to test.
* @returns {boolean} - True if this named class is this class.
*/
isA: function(name) {
return name === 'wcPlay';
},
/**
* Retrieves the class name of this object.
* @function wcPlay#instanceOf
* @param {string} name - The class name to test.
* @returns {boolean} - True if the named class is an instanced of this class.
*/
instanceOf: function(name) {
return name === 'wcPlay';
},
/**
* Adds a node into the known node stacks.
* @function wcPlay#__addNode
* @private
* @param {wcNode} node - The node to add.
*/
__addNode: function(node) {
if (node.instanceOf('wcNodeComposite')) {
this._compositeNodes.push(node);
} else if (node.instanceOf('wcNodeEntry')) {
this._entryNodes.push(node);
} else if (node.instanceOf('wcNodeProcess')) {
this._processNodes.push(node);
} else if (node.instanceOf('wcNodeStorage')) {
this._storageNodes.push(node);
}
},
/**
* Removes a node from the known node stacks.
* @function wcPlay#__removeNode
* @private
* @param {wcNode} node - The node to remove.
* @returns {boolean} - Fails if the node was not found in this script.
*/
__removeNode: function(node) {
var index = -1;
if (node.instanceOf('wcNodeComposite')) {
index = this._compositeNodes.indexOf(node);
if (index > -1) {
this._compositeNodes.splice(index, 1);
}
} else if (node.instanceOf('wcNodeEntry')) {
index = this._entryNodes.indexOf(node);
if (index > -1) {
this._entryNodes.splice(index, 1);
}
} else if (node.instanceOf('wcNodeProcess')) {
index = this._processNodes.indexOf(node);
if (index > -1) {
this._processNodes.splice(index, 1);
}
} else if (node.instanceOf('wcNodeStorage')) {
index = this._storageNodes.indexOf(node);
if (index > -1) {
this._storageNodes.splice(index, 1);
}
}
// If the node was not found, propagate the removal to all composite nodes.
if (index === -1) {
for (var i = 0; i < this._compositeNodes.length; ++i) {
if (this._compositeNodes[i].instanceOf('wcNodeCompositeScript') &&
this._compositeNodes[i].__removeNode(node)) {
return true;
}
}
}
return false;
},
/**
* Retrieves the next node id.
* @function wcPlay#__nextNodeId
* @private
* @returns {number} - The next available node id.
*/
__nextNodeId: function() {
return ++this._nodeId;
},
/**
* Gets or sets the current node id.
* @function wcPlay#__curNodeId
* @private
* @param {number} [cur] - If supplied, will assign the current node id.
* @returns {number} - The current node id.
*/
__curNodeId: function(cur) {
if (typeof cur === 'number') {
this._nodeId = cur;
}
return this._nodeId;
}
};
function wcNodeTimeoutEvent(node, callback, delay) {
this._node = node;
this._timerId = 0;
this._callback = callback;
this._remaining = delay;
this._marker = 0;
}
wcNodeTimeoutEvent.prototype = {
pause: function() {
clearTimeout(this._timerId);
this._remaining -= new Date().getTime() - this._marker;
},
resume: function() {
this._marker = new Date().getTime();
clearTimeout(this._timerId);
var self = this;
this._timerId = setTimeout(function() {
self._node.finishThread(self);
self._callback && self._callback.call(self._node);
self.__clear();
}, this._remaining);
},
__clear: function() {
this._node = null;
this._callback = null;
clearTimeout(this._timerId);
}
};